From 752e2abc5f90d6f6c81da9d2bc9dfbbbbda6e5ba Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 3 Oct 2026 10:54:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(models):=20Test-Knopf=20=E2=80=94=20zeigt,?= =?UTF-8?q?=20welches=20Modell=20wirklich=20geantwortet=20hat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher liess sich nicht nachsehen, ob eine Modell-Einstellung ueberhaupt greift. Der Proxy hat die Information sogar aktiv weggeworfen: normalizeModelName() nahm den echten Namen aus modelUsage (z.B. "claude-opus-5-5") und schrieb ihn auf "claude-opus-4" um. Das `model`-Feld der Antwort log also β€” und damit war jede Ueberpruefung unmoeglich. - normalizeModelName gibt den echten Namen durch. Im OpenAI-Protokoll soll `model` das Modell nennen, das geantwortet HAT; ein erfundener Name ist schlechter als keiner. - Zusatzfelder aria_model / aria_error / aria_context_window / aria_max_output aus modelUsage.canonicalModel. Noetig, weil die CLI bei einem unbekannten Modell gar nicht bis zum Call kommt und ihre Begruendung einfach in den Antworttext legt β€” ohne Flag waere ein Fehlschlag nicht von einer Antwort zu unterscheiden. - Knopf "πŸ”¬ Testen" neben Dropdown und Freitextfeld. Er schickt eine echte Mini-Anfrage durch den GANZEN Weg (Diagnostic β†’ Proxy β†’ extractModel β†’ claude CLI β†’ Anthropic). Ausdruecklich NICHT gemacht: das Modell nach seiner eigenen Version fragen. Darauf ist kein Verlass β€” Modelle kennen ihren Versionsstring oft nicht und raten. Und den Dropdown-Wert zurueckzugeben waere sinnlos. Die Wahrheit kommt aus modelUsage der CLI. Vier unterscheidbare Ergebnisse: Tier-Alias gruen β€” zeigt die aufgeloeste Version ("opus" β†’ Opus 5.5) exakte Version gruen β€” angefragt == geantwortet nicht erkannt GELB β€” der Proxy hat still "opus" eingesetzt; warnt ausdruecklich davor, die ID so zu setzen ungueltig ROT β€” CLI-Begruendung im Klartext Der gelbe Fall war wichtig: ein Name, der nicht wie ein Claude-Modell aussieht, faellt auf "opus" zurueck und haette sonst als Erfolg gegolten β€” man setzt dann eine ID, die nie benutzt wird, und merkt es nie. Geprueft gegen ECHTE CLI-Ausgaben aus dem Proxy-Container (9 Faelle: Alias, exakte Version, Tippfehler) plus 7 Faelle fuer die Anzeige-Logik. Nebenbefund: `--model opus` loest derzeit auf claude-opus-5-5 auf. Co-Authored-By: Claude Opus 5 (1M context) --- diagnostic/index.html | 79 +++++++++++++++++++++++++++++++++- diagnostic/server.js | 75 ++++++++++++++++++++++++++++++++ proxy-patches/cli-to-openai.js | 39 +++++++++++++---- 3 files changed, 184 insertions(+), 9 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 445325f..5d8bb90 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -632,17 +632,24 @@ +
+
Erweitert: freie Model-ID
- + +
+
+ Erst testen, dann setzen β€” ein Tippfehler im Namen fΓ€llt sonst erst + auf, wenn ARIA nicht mehr antwortet. +
@@ -2716,6 +2723,49 @@ return; } + if (msg.type === 'model_test_result') { + const box = document.getElementById('model-test-result'); + const btn = document.getElementById('btn-test-model'); + if (btn) { btn.disabled = false; btn.textContent = 'πŸ”¬ Testen'; } + if (box) { + box.style.display = 'block'; + const secs = msg.tookMs ? ' Β· ' + (msg.tookMs / 1000).toFixed(1) + 's' : ''; + if (msg.error) { + box.textContent = '❌ "' + msg.requested + '" hat nicht funktioniert' + + secs + '\n' + msg.error; + box.style.color = '#FF6B6B'; + } else { + // Zeigt ausdruecklich BEIDES: was angefragt wurde und was + // tatsaechlich lief. + const TIERS = ['opus', 'sonnet', 'haiku', 'fable']; + const differs = msg.actual !== msg.requested; + const isTier = TIERS.includes(msg.requested); + const win = msg.contextWindow + ? '\nKontext: ' + Math.round(msg.contextWindow / 1000) + 'k Token' + + (msg.maxOutput ? ', max. Ausgabe ' + Math.round(msg.maxOutput / 1000) + 'k' : '') + : ''; + const tail = win + (msg.reply ? '\nAntwort: ' + msg.reply : ''); + if (differs && !isTier) { + // Ein nicht erkannter Name wird vom Proxy durch "opus" ersetzt. + // Das darf NICHT wie ein Erfolg aussehen β€” sonst setzt man eine + // Modell-ID, die nie benutzt wird, und merkt es nie. + box.textContent = '⚠ "' + msg.requested + '" wurde nicht erkannt' + secs + + '\nStattdessen geantwortet hat: ' + msg.actual + + '\nDiese ID so NICHT setzen β€” tippe den vollen Namen (z.B. ' + + 'claude-opus-5-5) oder nimm einen Eintrag aus der Liste.' + tail; + box.style.color = '#FFD60A'; + } else { + box.textContent = 'βœ… Geantwortet hat: ' + msg.actual + secs + + (differs ? '\n(angefragt war der Tier "' + msg.requested + + '" β€” aufgelΓΆst auf diese Version)' : '') + + tail; + box.style.color = '#3FFF9F'; + } + } + } + return; + } + if (msg.type === 'attachment_limit_info') { const inp = document.getElementById('attachment-limit-input'); const st = document.getElementById('attachment-limit-status'); @@ -7969,6 +8019,33 @@ } } + /** + * Testet ein Modell mit einer echten Mini-Anfrage. + * fromFreeText=true nimmt die handgetippte ID, sonst das Dropdown. + * + * Was hier NICHT passiert: den Dropdown-Wert zurueckgeben. Angezeigt wird, + * was die CLI als tatsaechlich benutztes Modell meldet β€” bei einem Alias + * also die aufgeloeste Version, bei einem Tippfehler der Fehler. + */ + function testModel(fromFreeText) { + const model = fromFreeText + ? (document.getElementById('setting-model')?.value || '').trim() + : (document.getElementById('setting-model-select')?.value || ''); + const box = document.getElementById('model-test-result'); + if (!box) return; + box.style.display = 'block'; + if (!model) { + box.textContent = 'Kein Modell ausgewΓ€hlt.'; + box.style.color = '#FF6B6B'; + return; + } + box.textContent = `Teste "${model}" β€” schicke eine echte Anfrage …`; + box.style.color = '#8888AA'; + const btn = document.getElementById('btn-test-model'); + if (btn && !fromFreeText) { btn.disabled = true; btn.textContent = 'πŸ”¬ lΓ€uft …'; } + send({ action: 'test_model', model }); + } + function onModelSelectChange() { const sel = document.getElementById('setting-model-select'); const desc = document.getElementById('model-desc'); diff --git a/diagnostic/server.js b/diagnostic/server.js index 5d172dd..9b9103f 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -3215,6 +3215,8 @@ wss.on("connection", (ws) => { handleGetModel(ws); } else if (msg.action === "set_model") { handleSetModel(ws, msg.model); + } else if (msg.action === "test_model") { + handleTestModel(ws, msg.model); } else if (msg.action === "get_attachment_limit") { handleGetAttachmentLimit(ws); } else if (msg.action === "set_attachment_limit") { @@ -3641,6 +3643,79 @@ function handleSetModel(clientWs, model) { return; } +// ── Einstellungen: Model-Test ─────────────────────────────────────── +// +// Schickt eine winzige ECHTE Anfrage durch den kompletten Weg +// (Diagnostic -> Proxy -> extractModel -> claude CLI -> Anthropic) und meldet, +// welches Modell tatsaechlich geantwortet hat. +// +// Wichtig: wir fragen NICHT das Modell selbst ("welche Version bist du?") β€” +// darauf kann man sich nicht verlassen, Modelle kennen ihren eigenen +// Versionsstring oft nicht und raten. Die Wahrheit kommt aus modelUsage der +// CLI (Feld canonicalModel): das ist der Name, unter dem der Call wirklich +// lief. Bei einem Alias sieht man damit die aufgeloeste Version +// ("opus" -> "claude-opus-5-5"), bei einem ungueltigen Namen den Fehler. +function handleTestModel(clientWs, model) { + const m = String(model || "").trim(); + if (!m) { + clientWs.send(JSON.stringify({ + type: "model_test_result", error: "Kein Modell angegeben" })); + return; + } + const started = Date.now(); + (async () => { + try { + const r = await fetch(`${PROXY_URL}/v1/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: m, + // Kurz halten: der Test soll Sekunden kosten, nicht Minuten. + messages: [{ role: "user", content: "Antworte nur mit: OK" }], + max_tokens: 16, + }), + signal: AbortSignal.timeout(180000), + }); + const d = await r.json().catch(() => null); + if (!r.ok || !d) { + throw new Error(`Proxy antwortete HTTP ${r.status}`); + } + const tookMs = Date.now() - started; + const reply = (d.choices && d.choices[0] && d.choices[0].message + && d.choices[0].message.content) || ""; + // Kein Modell in modelUsage ODER is_error => der Call kam nie + // zustande. Die CLI legt ihre Begruendung dann in den Text. + if (d.aria_error || !d.aria_model) { + clientWs.send(JSON.stringify({ + type: "model_test_result", + requested: m, + error: (reply || "Modell hat nicht geantwortet").slice(0, 400), + tookMs, + })); + return; + } + clientWs.send(JSON.stringify({ + type: "model_test_result", + requested: m, + actual: d.aria_model, + contextWindow: d.aria_context_window || 0, + maxOutput: d.aria_max_output || 0, + reply: reply.slice(0, 120), + tookMs, + })); + log("info", "server", `Model-Test: ${m} -> ${d.aria_model} (${tookMs}ms)`); + } catch (err) { + const msg = err && err.name === "TimeoutError" + ? "ZeitΓΌberschreitung (3 min) β€” Proxy oder CLI antwortet nicht" + : String((err && err.message) || err); + clientWs.send(JSON.stringify({ + type: "model_test_result", requested: m, error: msg, + tookMs: Date.now() - started, + })); + } + })(); +} + // ── Einstellungen: Groessen-Limit fuer Memory-Anhaenge ────────────── // Steht in runtime.json als `memoryAttachmentMaxMb`. Das Brain liest es bei // JEDEM Upload neu (memory_attachments.max_bytes()) β€” eine Aenderung greift diff --git a/proxy-patches/cli-to-openai.js b/proxy-patches/cli-to-openai.js index 57dc437..b944c5b 100644 --- a/proxy-patches/cli-to-openai.js +++ b/proxy-patches/cli-to-openai.js @@ -100,9 +100,13 @@ function _parseToolCalls(text) { } export function cliResultToOpenai(result, requestId) { - const modelName = result.modelUsage - ? Object.keys(result.modelUsage)[0] - : "claude-sonnet-4"; + // modelUsage ist die Wahrheit: die CLI tragt dort den Namen ein, unter dem + // der Request wirklich lief β€” bei einem Alias also die aufgeloeste Version + // ("opus" -> "claude-opus-5-5"). Leer bleibt es, wenn gar kein Call + // zustande kam (z.B. unbekanntes Modell). + const usage = result.modelUsage || {}; + const usedKey = Object.keys(usage)[0]; + const modelName = (usedKey && (usage[usedKey]?.canonicalModel || usedKey)) || ""; const rawText = result.result || ""; const { tool_calls, rest } = _parseToolCalls(rawText); @@ -134,13 +138,32 @@ export function cliResultToOpenai(result, requestId) { total_tokens: (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0), }, + // ARIA-Zusatzfelder (ausserhalb des OpenAI-Schemas, Clients ignorieren + // sie). Damit kann die Diagnostic pruefen, ob ein Modell wirklich + // gelaufen ist: bei einem unbekannten Namen kommt die CLI gar nicht bis + // zum Call, dann ist modelUsage leer und is_error gesetzt. Ohne das + // koennte man einen Fehlschlag nicht von einer Antwort unterscheiden β€” + // die Fehlermeldung steht naemlich einfach im content. + aria_model: modelName, + aria_error: result.is_error === true, + aria_context_window: (usedKey && usage[usedKey]?.contextWindow) || 0, + aria_max_output: (usedKey && usage[usedKey]?.maxOutputTokens) || 0, }; } +/** + * Modellname fuer die Antwort. + * + * Hier wurde der Name FRUEHER umgeschrieben: alles mit "opus" drin wurde zu + * "claude-opus-4", Sonnet zu "claude-sonnet-4" usw. Damit log das Feld β€” die + * CLI liefert in modelUsage den tatsaechlich benutzten Namen (z.B. + * "claude-opus-5-5"), und genau der wurde weggeworfen. Im OpenAI-Protokoll + * soll `model` das Modell nennen, das geantwortet HAT; ein erfundener Name + * macht jede Ueberpruefung unmoeglich ("welches Modell lief gerade?"). + * + * Jetzt bleibt der echte Name stehen; nur wenn gar keiner da ist, gibt es + * einen neutralen Platzhalter. + */ function normalizeModelName(model) { - const m = model || "claude-sonnet-4"; - if (m.includes("opus")) return "claude-opus-4"; - if (m.includes("sonnet")) return "claude-sonnet-4"; - if (m.includes("haiku")) return "claude-haiku-4"; - return m; + return model || "unknown"; }