+ 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";
}