DeepSeek V4.1 Flash: native Bilderkennung, GLM/OpenRouter entfernt

- deepseek-flash als einziges Modell mit Vision, Preise aktualisiert
- OpenRouter/GLM samt Code, Config und Env-Variablen entfernt
- Legacy-Modell-IDs in Sessions werden automatisch migriert
- browse lädt Produktbilder lokal nach dashboards/ (Dedupe + Cleanup)
- Vision-Prompt: Workflow für visuelle Produktsuche, max. ~5 Screenshots
This commit is contained in:
nico committed 2026-10-05 12:55:31 +02:00
1 parent 12f01b86d6
commit b740ce1316
9 files changed
+130 -82

No files matched your search

+22 -31
View File
@@ -80,7 +80,7 @@ const TOOL_DEFINITIONS = [
function: {
name: 'browse',
description:
'Besucht eine URL und liest den sichtbaren Text der Seite (Titel + Inhalt). Nutze das zum Lesen von Artikeln, Projektseiten und Fallback-Suchmaschinen (Startpage: https://www.startpage.com/do/dsearch?query=..., Google: https://www.google.com/search?q=...).',
'Besucht eine URL, liest den sichtbaren Text und lädt Produktbilder der Seite lokal herunter. Das Feld `images` enthält die fertigen Bild-URLs (z.B. /dashboards/...) fürs Dashboard. Nutze das zum Lesen von Artikeln, Projektseiten und Fallback-Suchmaschinen (Startpage: https://www.startpage.com/do/dsearch?query=..., Google: https://www.google.com/search?q=...).',
parameters: {
type: 'object',
properties: {
@@ -175,7 +175,7 @@ const SCREENSHOT_TOOL = {
function: {
name: 'screenshot',
description:
'Macht einen Screenshot einer Seite (JPEG) und liefert das Bild in den Kontext. Nutze das bei bildlastigen Seiten, wenn der Text nicht ausreicht (z.B. Anzeigen-Listen, Produktfotos, Infografiken). Maximal 2-3 Screenshots pro Session.',
'Macht einen Screenshot einer Seite (JPEG) und liefert das Bild in den Kontext. Nutze das bei bildlastigen Seiten, wenn der Text nicht ausreicht (z.B. Anzeigen-Listen, Produktfotos, Infografiken) oder um Produkte visuell zu vergleichen (Farbe, Form). Maximal ~5 Screenshots pro Session.',
parameters: {
type: 'object',
properties: {
@@ -204,7 +204,9 @@ function summarizeTool(toolName, output) {
(data.results || []).slice(0, 5).map((r) => `${r.title} (${r.url})`).join(', ') || (data.note || '')
}`;
case 'browse':
return `Seite: ${data.title || data.url || ''} – ${String(data.text || '').slice(0, 500)}`;
return `Seite: ${data.title || data.url || ''} – ${String(data.text || '').slice(0, 500)}${
data.images?.length ? ` [${data.images.length} Bilder extrahiert]` : ''
}`;
case 'verify':
return `Link ${data.ok ? 'VERIFIZIERT ✓' : 'FEHLGESCHLAGEN ✗'}: ${data.url}${data.reason ? ' – ' + data.reason : ''}`;
case 'save_dashboard':
@@ -228,7 +230,7 @@ function summarizeTool(toolName, output) {
const MAX_ITERATIONS = 40;
// Abbruch, wenn der API-Stream länger hängt (z.B. OpenRouter ohne Keep-Alive)
// Abbruch, wenn der API-Stream länger hängt (z.B. Upstream ohne Keep-Alive)
const REQUEST_TIMEOUT_MS = 10 * 60 * 1000;
// Leere Modell-Antworten (kein Inhalt, keine Tool-Calls) einmal wiederholen
@@ -252,10 +254,13 @@ Eine bloße Erklärung oder Beschreibung der Änderungen im Chat ist KEINE Umset
if (model.vision) {
prompt += `
## Bilderkennung (Vision)
- Wenn der Nutzer ein Bild hochgeladen hat (z.B. ein Foto eines Schuhs): Analysiere es SORGFÄLTIG (Marke, Farbe, Material, Form, Zustand, erkennbare Details) und suche gezielt nach ähnlichen Produkten und Angeboten. Beschreibe im Dashboard, worauf du beim Vergleich geachtet hast.
- Das hochgeladene Bild liegt dir als Data-URL im Verlauf vor – du kannst es bei Bedarf direkt ins Dashboard einbetten.
- Nutze das screenshot-Tool, um bildlastige Seiten (z.B. Anzeigen-Listen, Produktfotos) visuell zu beurteilen, wenn die Text-Extraktion nicht ausreicht. Maximal 2-3 Screenshots pro Session.`;
## Bilderkennung & visuelle Produktsuche (Vision)
- Hat der Nutzer ein Bild hochgeladen (z.B. einen Schuh): Analysiere es SORGFÄLTIG – Gattung/Modelltyp (z.B. Sneaker, Loafer, Stiefelette), Farben (Haupt-, Neben- und Sohlenfarbe), Material (Leder, Mesh, Wildleder), Form/Details (Sohle, Schnürung, Logo, Muster), Marke und Zustand.
- Leite daraus 4-8 Suchbegriffe ab, Deutsch UND Englisch (z.B. "weißer minimalistischer leder-sneaker" / "white minimalist leather sneaker"), und suche gezielt nach ähnlichen Produkten und Angeboten. Priorität: Farbe, Form und Modelltyp.
- Das browse-Tool lädt Produktbilder der Seite automatisch lokal herunter und liefert im Feld \`images\` fertige Dashboard-URLs (z.B. /dashboards/session-img-abc123.jpg). Bette sie direkt ein: <img src="..." alt="..." loading="lazy">. Nur diese URLs verwenden – niemals erfinden.
- Baue eine Sektion "Visuelle Treffer" ins Dashboard: pro Treffer Produktbild, Name, ggf. Preis, verifizierter Link und eine kurze Begründung, warum er passt (Farbe/Form/Material).
- Nutze das screenshot-Tool, um bildlastige Seiten (Anzeigen-Listen, Produktseiten) visuell zu vergleichen, wenn der Text nicht ausreicht – achte auf Farbton, Silhouette und Sohlenform. Maximal ~5 Screenshots pro Session; priorisiere die aussichtsreichsten Treffer.
- Das hochgeladene Bild liegt dir als Data-URL im Verlauf vor – du kannst es als Referenz oben im Dashboard einbetten und beschreiben, worauf du beim Vergleich geachtet hast.`;
}
return prompt;
}
@@ -292,15 +297,9 @@ async function chatCompletion(config, messages, systemPrompt, model, tools, sign
stream_options: { include_usage: true },
};
const isOpenRouter = model.provider === 'openrouter';
const baseUrl = isOpenRouter
? 'https://openrouter.ai/api/v1'
: config.baseUrl || 'https://api.deepseek.com';
const baseUrl = config.baseUrl || 'https://api.deepseek.com';
if (isOpenRouter) {
// GLM: Reasoning ist Pflicht; Effort über OpenRouter-Param "reasoning"
body.reasoning = { effort: effortOverride || model.reasoningEffort?.() || 'high' };
} else if (effortOverride === 'none' || config.thinking === 'disabled') {
if (effortOverride === 'none' || config.thinking === 'disabled') {
// DeepSeek: Denken pro Request abschaltbar (Liefer-Runde des Dashboards)
body.thinking = { type: 'disabled' };
} else {
@@ -308,17 +307,10 @@ async function chatCompletion(config, messages, systemPrompt, model, tools, sign
body.reasoning_effort = effortOverride || model.reasoningEffort?.() || 'max';
}
const headers = isOpenRouter
? {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.openrouterApiKey}`,
'X-Title': 'Research Chat',
'HTTP-Referer': 'https://github.com/nico/researchchat',
}
: {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
};
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`,
};
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
@@ -363,9 +355,8 @@ async function chatCompletion(config, messages, systemPrompt, model, tools, sign
const choice = json.choices?.[0];
if (!choice) return;
const d = choice.delta || {};
// DeepSeek liefert reasoning_content, OpenRouter/GLM reasoning
// DeepSeek liefert den Denk-Text als reasoning_content
if (d.reasoning_content) delta.reasoning += d.reasoning_content;
else if (d.reasoning) delta.reasoning += d.reasoning;
if (d.content) delta.content += d.content;
if (d.tool_calls) {
for (const tc of d.tool_calls) {
@@ -467,7 +458,7 @@ async function runResearchJob(config, session, userMessageText, emit, db, signal
},
async browse({ url }) {
emit.progress('🌐', `Besuche: ${url}`);
const result = await tools.browse(url, (m) => emit.step(m));
const result = await tools.browse(url, (m) => emit.step(m), { sessionId: session.id });
return JSON.stringify(result);
},
async screenshot({ url, full_page }) {
@@ -492,7 +483,7 @@ async function runResearchJob(config, session, userMessageText, emit, db, signal
}
// Neuer Weg: nur vorbereiten, HTML kommt in der finalen Antwort
pendingDashboard = { title: title || 'Dashboard', filename: filename || null };
deliveryEffort = model.deliveryEffort || (model.provider === 'openrouter' ? 'low' : 'none');
deliveryEffort = model.deliveryEffort || 'none';
emit.progress(
'📊',
`Dashboard vorbereitet (${(filename || title || 'dashboard').toLowerCase().replace(/[^a-z0-9äöü-]+/g, '-')}.html) – sende jetzt das komplette HTML in deiner finalen Antwort als \`\`\`html\`\`\`-Codeblock…`