feat(models): Test-Knopf — zeigt, welches Modell wirklich geantwortet hat
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) <noreply@anthropic.com>
This commit is contained in:
+78
-1
@@ -632,17 +632,24 @@
|
|||||||
<option value="">(lade Liste…)</option>
|
<option value="">(lade Liste…)</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn secondary" onclick="loadModelList(true)" title="Modell-Liste frisch von Anthropic holen (Modell-Katalog)" style="padding:4px 8px;font-size:10px;">↻ Aktualisieren</button>
|
<button class="btn secondary" onclick="loadModelList(true)" title="Modell-Liste frisch von Anthropic holen (Modell-Katalog)" style="padding:4px 8px;font-size:10px;">↻ Aktualisieren</button>
|
||||||
|
<button class="btn secondary" onclick="testModel()" id="btn-test-model" title="Schickt eine echte Mini-Anfrage und zeigt, welches Modell geantwortet hat" style="padding:4px 8px;font-size:10px;">🔬 Testen</button>
|
||||||
<button class="btn" onclick="saveModel()" style="padding:4px 8px;font-size:10px;">Setzen</button>
|
<button class="btn" onclick="saveModel()" style="padding:4px 8px;font-size:10px;">Setzen</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="model-desc" style="font-size:10px;color:#6a6a88;margin-bottom:6px;min-height:12px;"></div>
|
<div id="model-desc" style="font-size:10px;color:#6a6a88;margin-bottom:6px;min-height:12px;"></div>
|
||||||
|
<div id="model-test-result" style="font-size:11px;color:#8888AA;margin-bottom:8px;padding:6px 8px;background:#12121E;border:1px solid #2A2A3E;border-radius:6px;display:none;white-space:pre-wrap;"></div>
|
||||||
<div id="model-status" style="font-size:10px;color:#8888AA;margin-bottom:8px;"></div>
|
<div id="model-status" style="font-size:10px;color:#8888AA;margin-bottom:8px;"></div>
|
||||||
<details style="font-size:11px;color:#8888AA;">
|
<details style="font-size:11px;color:#8888AA;">
|
||||||
<summary style="cursor:pointer;">Erweitert: freie Model-ID</summary>
|
<summary style="cursor:pointer;">Erweitert: freie Model-ID</summary>
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-top:6px;">
|
<div style="display:flex;align-items:center;gap:8px;margin-top:6px;">
|
||||||
<input type="text" id="setting-model" placeholder="z.B. claude-opus-4" style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;">
|
<input type="text" id="setting-model" placeholder="z.B. claude-opus-5-5" style="flex:1;background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:6px 8px;color:#E0E0F0;font-family:inherit;font-size:12px;">
|
||||||
<button class="btn secondary" onclick="loadModel()" style="padding:4px 8px;font-size:10px;">Laden</button>
|
<button class="btn secondary" onclick="loadModel()" style="padding:4px 8px;font-size:10px;">Laden</button>
|
||||||
|
<button class="btn secondary" onclick="testModel(true)" style="padding:4px 8px;font-size:10px;" title="Diese ID testen, bevor du sie setzt">🔬 Testen</button>
|
||||||
<button class="btn" onclick="saveModelFreeText()" style="padding:4px 8px;font-size:10px;">Setzen</button>
|
<button class="btn" onclick="saveModelFreeText()" style="padding:4px 8px;font-size:10px;">Setzen</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="color:#555570;font-size:10px;margin-top:6px;">
|
||||||
|
Erst testen, dann setzen — ein Tippfehler im Namen fällt sonst erst
|
||||||
|
auf, wenn ARIA nicht mehr antwortet.
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2716,6 +2723,49 @@
|
|||||||
return;
|
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') {
|
if (msg.type === 'attachment_limit_info') {
|
||||||
const inp = document.getElementById('attachment-limit-input');
|
const inp = document.getElementById('attachment-limit-input');
|
||||||
const st = document.getElementById('attachment-limit-status');
|
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() {
|
function onModelSelectChange() {
|
||||||
const sel = document.getElementById('setting-model-select');
|
const sel = document.getElementById('setting-model-select');
|
||||||
const desc = document.getElementById('model-desc');
|
const desc = document.getElementById('model-desc');
|
||||||
|
|||||||
@@ -3215,6 +3215,8 @@ wss.on("connection", (ws) => {
|
|||||||
handleGetModel(ws);
|
handleGetModel(ws);
|
||||||
} else if (msg.action === "set_model") {
|
} else if (msg.action === "set_model") {
|
||||||
handleSetModel(ws, msg.model);
|
handleSetModel(ws, msg.model);
|
||||||
|
} else if (msg.action === "test_model") {
|
||||||
|
handleTestModel(ws, msg.model);
|
||||||
} else if (msg.action === "get_attachment_limit") {
|
} else if (msg.action === "get_attachment_limit") {
|
||||||
handleGetAttachmentLimit(ws);
|
handleGetAttachmentLimit(ws);
|
||||||
} else if (msg.action === "set_attachment_limit") {
|
} else if (msg.action === "set_attachment_limit") {
|
||||||
@@ -3641,6 +3643,79 @@ function handleSetModel(clientWs, model) {
|
|||||||
return;
|
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 ──────────────
|
// ── Einstellungen: Groessen-Limit fuer Memory-Anhaenge ──────────────
|
||||||
// Steht in runtime.json als `memoryAttachmentMaxMb`. Das Brain liest es bei
|
// Steht in runtime.json als `memoryAttachmentMaxMb`. Das Brain liest es bei
|
||||||
// JEDEM Upload neu (memory_attachments.max_bytes()) — eine Aenderung greift
|
// JEDEM Upload neu (memory_attachments.max_bytes()) — eine Aenderung greift
|
||||||
|
|||||||
@@ -100,9 +100,13 @@ function _parseToolCalls(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function cliResultToOpenai(result, requestId) {
|
export function cliResultToOpenai(result, requestId) {
|
||||||
const modelName = result.modelUsage
|
// modelUsage ist die Wahrheit: die CLI tragt dort den Namen ein, unter dem
|
||||||
? Object.keys(result.modelUsage)[0]
|
// der Request wirklich lief — bei einem Alias also die aufgeloeste Version
|
||||||
: "claude-sonnet-4";
|
// ("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 rawText = result.result || "";
|
||||||
const { tool_calls, rest } = _parseToolCalls(rawText);
|
const { tool_calls, rest } = _parseToolCalls(rawText);
|
||||||
@@ -134,13 +138,32 @@ export function cliResultToOpenai(result, requestId) {
|
|||||||
total_tokens:
|
total_tokens:
|
||||||
(result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0),
|
(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) {
|
function normalizeModelName(model) {
|
||||||
const m = model || "claude-sonnet-4";
|
return model || "unknown";
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user