Zwei Fehler. Der zweite war der schlimmere.
1) Die Liste war handgepflegt und damit garantiert veraltet.
Sie kannte "Opus (aktuell: Opus 5)" — waehrend es laengst Opus 5.5,
Sonnet 5.5 und Fable 5.1 gab. Der Knopf "↻ Aktualisieren" las nur eine
lokale Datei neu und aktualisierte damit gar nichts.
Jetzt kommt die Liste aus Anthropics eigenem Modell-Katalog
(downloads.claude.ai/model-catalog/v1/catalog.json, Surface 'cc') —
dieselbe Quelle, aus der sich Claude Code selbst bedient. Oeffentlich,
ohne API-Key (die Models-API braucht einen; mit dem Max-Abo gibt es keinen).
6h-Cache plus Datei-Cache, der auch offline und bei abgelaufenem TTL
einspringt: eine etwas alte echte Liste ist besser als keine.
Die Tier-Aliase (opus/sonnet/haiku/fable) bleiben vorne und bleiben die
Empfehlung — sie folgen automatisch der neuesten Version. Darunter die
exakten Versionen, aeltere ausdruecklich als solche markiert.
2) Eine feste Version liess sich faktisch nicht waehlen.
extractModel() hatte eine MODEL_MAP und fiel fuer alles Unbekannte STILL
auf "opus" zurueck. Wer "claude-opus-5-5" setzte, bekam ohne jede Meldung
das Tier-Default. Schlimmer noch: claude-opus-5 und claude-fable-5 standen
selbst in der Map und wurden auf ihr Tier eingeschmolzen — eine bewusst
gewaehlte aeltere Version wurde also durch die neueste ersetzt.
Volle Modellnamen gehen jetzt unveraendert an die CLI. Die versteht sie
("--model: an alias for the latest model or a model's full name") und
meldet sich selbst klar, wenn sie einen Namen nicht kennt — ehrlicher als
ein stilles Downgrade. Nur echter Muell faellt noch auf "opus", und das
wird geloggt.
Nachgewiesen gegen den LIVE-Katalog und die echte CLI im Proxy-Container:
20 Pruefungen gruen, u.a. dass alle 12 gelieferten exakten Versionen den Proxy
unveraendert passieren. `claude --print --model claude-opus-5-5` antwortet.
Stefans aktive Einstellung ist der Alias "opus" — am laufenden Betrieb aendert
sich dadurch nichts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
264 lines
11 KiB
JavaScript
264 lines
11 KiB
JavaScript
/**
|
|
* ARIA-patched openai-to-cli adapter.
|
|
*
|
|
* Erweitert die npm-Version von claude-max-api-proxy:
|
|
* - Multimodal-Content (Array von text-Parts) wird zu String reduziert.
|
|
* - Wenn die Anfrage ein `tools`-Feld enthaelt: die Tool-Definitionen
|
|
* werden in den Prompt als <system>-Block injiziert, mit klarer
|
|
* Anweisung das <tool_call name="...">{...}</tool_call> Format
|
|
* zu verwenden statt freiem Text.
|
|
* - Wenn Messages role=tool enthalten: deren Inhalt wird als
|
|
* <tool_result tool_call_id="...">…</tool_result> ins Prompt-Fragment
|
|
* eingewoben damit Claude den Loop-Step bekommt.
|
|
*
|
|
* Wird zur Container-Startzeit ueber die npm-Version geschrieben
|
|
* (siehe docker-compose.yml proxy-Block).
|
|
*/
|
|
|
|
// NUR echte Tier-Aliase. Die CLI loest sie selbst auf die aktuelle Version des
|
|
// Tiers auf.
|
|
//
|
|
// Hier standen frueher auch volle Versionen (claude-opus-5 → "opus",
|
|
// claude-fable-5 → "fable", ...). Das war falsch: wer bewusst eine BESTIMMTE
|
|
// Version waehlte, bekam still das Tier-Default — also unter Umstaenden ein
|
|
// ganz anderes Modell als ausgewaehlt. Volle Namen gehen jetzt unveraendert an
|
|
// die CLI (siehe extractModel), die versteht sie.
|
|
const MODEL_MAP = {
|
|
"opus": "opus",
|
|
"sonnet": "sonnet",
|
|
"haiku": "haiku",
|
|
"fable": "fable",
|
|
"claude-code-cli/opus": "opus",
|
|
"claude-code-cli/sonnet": "sonnet",
|
|
"claude-code-cli/haiku": "haiku",
|
|
"claude-code-cli/fable": "fable",
|
|
};
|
|
|
|
// Echte Claude-Modellnamen: claude-<familie>-<zahlen/suffixe>.
|
|
// Die CLI nimmt laut `claude --help` "an alias for the latest model (e.g.
|
|
// 'fable', 'opus', or 'sonnet') or a model's full name" — exakte Versionen
|
|
// darf man also direkt durchreichen.
|
|
const FULL_MODEL_RE = /^claude-[a-z]+-[0-9][0-9a-z.-]*$/;
|
|
|
|
export function extractModel(model) {
|
|
if (MODEL_MAP[model]) return MODEL_MAP[model];
|
|
const stripped = (model || "").replace(/^claude-code-cli\//, "");
|
|
if (MODEL_MAP[stripped]) return MODEL_MAP[stripped];
|
|
// Exakte Version durchreichen statt sie auf einen Tier einzuschmelzen.
|
|
//
|
|
// Vorher landete JEDER unbekannte Name still auf "opus". Damit war eine
|
|
// neuere Version faktisch nicht waehlbar: wer "claude-opus-5-5" setzte,
|
|
// bekam ohne jede Meldung das Tier-Default — die Einstellung sah gesetzt
|
|
// aus und tat nichts. Kennt die CLI den Namen nicht, sagt SIE das deutlich
|
|
// ("isn't described by this version's model catalog"), und das ist die
|
|
// ehrlichere Antwort als ein stilles Downgrade.
|
|
if (FULL_MODEL_RE.test(stripped)) return stripped;
|
|
if (stripped) {
|
|
console.error(`[aria-model] Unbekannter Modellname ${JSON.stringify(stripped)} — nutze 'opus'`);
|
|
}
|
|
return "opus";
|
|
}
|
|
|
|
/** Multimodal: content kann String oder Array von Parts sein. */
|
|
function _text(c) {
|
|
if (typeof c === "string") return c;
|
|
if (Array.isArray(c)) {
|
|
return c
|
|
.filter((b) => b && b.type === "text")
|
|
.map((b) => b.text || "")
|
|
.join("");
|
|
}
|
|
return String(c == null ? "" : c);
|
|
}
|
|
|
|
/**
|
|
* Baut den Tool-Use-Block fuer den System-Prompt.
|
|
* Anweisung: Claude soll <tool_call name="X">{json args}</tool_call>
|
|
* ausgeben statt das Tool intern via Bash zu simulieren.
|
|
*/
|
|
function _toolsBlock(tools) {
|
|
if (!Array.isArray(tools) || tools.length === 0) return "";
|
|
const lines = [];
|
|
lines.push("# Verfuegbare Tools");
|
|
lines.push("");
|
|
lines.push(
|
|
"Du hast neben deinen eigenen internen Tools (Bash, Read, etc.) auch " +
|
|
"diese externen Tools, die im Backend-System angesiedelt sind. " +
|
|
"Sie sind die EINZIGE Moeglichkeit Aktionen auszuloesen wie Trigger anlegen, " +
|
|
"Skills aufrufen, oder Konfiguration aendern. Simuliere sie NICHT mit Bash/sleep — " +
|
|
"rufe sie sauber auf:"
|
|
);
|
|
lines.push("");
|
|
for (const t of tools) {
|
|
if (!t || t.type !== "function" || !t.function) continue;
|
|
const fn = t.function;
|
|
const name = fn.name || "";
|
|
const desc = fn.description || "";
|
|
const params = fn.parameters || {};
|
|
lines.push(`## ${name}`);
|
|
if (desc) lines.push(desc);
|
|
try {
|
|
lines.push("Schema: " + JSON.stringify(params));
|
|
} catch (_) {
|
|
lines.push("Schema: (nicht serialisierbar)");
|
|
}
|
|
lines.push("");
|
|
}
|
|
lines.push("# Tool-Call-Format");
|
|
lines.push("");
|
|
lines.push(
|
|
"Wenn du eines der OBIGEN externen Tools aufrufen willst, antworte " +
|
|
"**ausschliesslich** mit einem oder mehreren Bloecken in genau dieser Form, " +
|
|
"JEDER fuer sich auf einer eigenen Zeile:"
|
|
);
|
|
lines.push("");
|
|
lines.push('<tool_call name="TOOL_NAME">{"arg1":"value","arg2":123}</tool_call>');
|
|
lines.push("");
|
|
lines.push(
|
|
"Regeln: (1) Innerhalb des Blocks steht NUR gueltiges JSON mit den Argumenten. " +
|
|
"(2) Kein Text drumherum. (3) Keine Code-Fences, kein Markdown. " +
|
|
"(4) Mehrere Tool-Calls = mehrere Bloecke untereinander. " +
|
|
"(5) Nach den Bloecken aufhoeren — der Server fuehrt die Tools aus und " +
|
|
"schickt dir die Ergebnisse fuer den naechsten Turn. " +
|
|
"(6) Wenn KEIN externes Tool noetig ist, antworte normal als Text fuer den User. " +
|
|
"(7) Nutze Bash/sleep NICHT als Ersatz fuer trigger_timer — das ist genau " +
|
|
"der Bug den wir damit fixen."
|
|
);
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Wandelt OpenAI-messages in einen Single-String-Prompt um.
|
|
* - system/user/assistant wie bisher
|
|
* - tool-role: als <tool_result tool_call_id="..." name="..."> eingewoben
|
|
*/
|
|
export function messagesToPrompt(messages, tools) {
|
|
const parts = [];
|
|
const toolsBlock = _toolsBlock(tools);
|
|
if (toolsBlock) {
|
|
parts.push(`<system>\n${toolsBlock}\n</system>\n`);
|
|
}
|
|
for (const msg of messages) {
|
|
if (!msg) continue;
|
|
switch (msg.role) {
|
|
case "system":
|
|
parts.push(`<system>\n${_text(msg.content)}\n</system>\n`);
|
|
break;
|
|
case "user":
|
|
parts.push(_text(msg.content));
|
|
break;
|
|
case "assistant": {
|
|
const txt = _text(msg.content);
|
|
const tcs = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
const tcParts = tcs.map((tc) => {
|
|
const name = tc?.function?.name || tc?.name || "";
|
|
let args = tc?.function?.arguments ?? tc?.arguments ?? "{}";
|
|
if (typeof args !== "string") {
|
|
try { args = JSON.stringify(args); } catch (_) { args = "{}"; }
|
|
}
|
|
return `<tool_call name="${name}">${args}</tool_call>`;
|
|
}).join("\n");
|
|
const combined = [txt, tcParts].filter(Boolean).join("\n").trim();
|
|
if (combined) parts.push(`<previous_response>\n${combined}\n</previous_response>\n`);
|
|
break;
|
|
}
|
|
case "tool": {
|
|
const name = msg.name || "";
|
|
const id = msg.tool_call_id || "";
|
|
parts.push(
|
|
`<tool_result tool_call_id="${id}" name="${name}">\n${_text(msg.content)}\n</tool_result>\n`
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return parts.join("\n").trim();
|
|
}
|
|
|
|
/**
|
|
* Extrahiert NUR den System-Anteil (System-Messages + Tool-Use-Block) als
|
|
* rohen Text — OHNE <system>-Tags. Fuer den ECHTEN System-Prompt-Kanal der
|
|
* Claude-CLI (--system-prompt, VOLLER Replace — nicht --append). Damit ist
|
|
* die ARIA-Persona DIE Identitaet des Modells und nicht ein Anhaengsel hinter
|
|
* Claude Codes eigener "You are Claude Code"-Identitaet (die bei duennem
|
|
* Kontext sonst gewinnt und die Persona als Injection abwehrt). Der Output
|
|
* muss deshalb SELBSTTRAGEND sein — er ersetzt Claude Codes System-Prompt
|
|
* komplett inkl. dynamischer Sektionen (cwd, git, platform).
|
|
* Reihenfolge: erst der Tool-Use-Block (Format-Anweisung), dann die
|
|
* System-Messages in Original-Reihenfolge.
|
|
*/
|
|
export function extractSystemPrompt(messages, tools) {
|
|
const chunks = [];
|
|
const toolsBlock = _toolsBlock(tools);
|
|
if (toolsBlock) chunks.push(toolsBlock);
|
|
for (const msg of messages || []) {
|
|
if (msg && msg.role === "system") {
|
|
const t = _text(msg.content).trim();
|
|
if (t) chunks.push(t);
|
|
}
|
|
}
|
|
return chunks.join("\n\n").trim();
|
|
}
|
|
|
|
/**
|
|
* Wie messagesToPrompt, aber OHNE System-Messages und OHNE Tool-Block — nur der
|
|
* eigentliche Verlauf (user/assistant/tool). Fuer den Modus, in dem der
|
|
* System-Prompt ueber --append-system-prompt separat zugestellt wird.
|
|
*/
|
|
export function conversationToPrompt(messages) {
|
|
const parts = [];
|
|
for (const msg of messages || []) {
|
|
if (!msg) continue;
|
|
switch (msg.role) {
|
|
case "system":
|
|
break; // geht ueber --append-system-prompt
|
|
case "user":
|
|
parts.push(_text(msg.content));
|
|
break;
|
|
case "assistant": {
|
|
const txt = _text(msg.content);
|
|
const tcs = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
const tcParts = tcs.map((tc) => {
|
|
const name = tc?.function?.name || tc?.name || "";
|
|
let args = tc?.function?.arguments ?? tc?.arguments ?? "{}";
|
|
if (typeof args !== "string") {
|
|
try { args = JSON.stringify(args); } catch (_) { args = "{}"; }
|
|
}
|
|
return `<tool_call name="${name}">${args}</tool_call>`;
|
|
}).join("\n");
|
|
const combined = [txt, tcParts].filter(Boolean).join("\n").trim();
|
|
if (combined) parts.push(`<previous_response>\n${combined}\n</previous_response>\n`);
|
|
break;
|
|
}
|
|
case "tool": {
|
|
const name = msg.name || "";
|
|
const id = msg.tool_call_id || "";
|
|
parts.push(
|
|
`<tool_result tool_call_id="${id}" name="${name}">\n${_text(msg.content)}\n</tool_result>\n`
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return parts.join("\n").trim();
|
|
}
|
|
|
|
export function openaiToCli(request) {
|
|
// Persona/System + Tool-Block gehen ueber den ECHTEN System-Prompt-Kanal
|
|
// (--system-prompt = VOLLER Replace, siehe manager.js buildArgs-Patch in
|
|
// docker-compose.yml). Der Prompt enthaelt nur noch den Gespraechsverlauf.
|
|
// Voller Replace statt --append, weil Anhaengen Claude Codes eingebaute
|
|
// "You are Claude Code"-Identitaet stehen laesst — die bei duennem Kontext
|
|
// (Hauptchat) gewinnt und die ARIA-Persona als Injection abwehrt.
|
|
// systemPrompt ist immer ein String (extractSystemPrompt liefert "" statt
|
|
// undefined). ACHTUNG: bei --system-prompt darf er NIE leer sein, sonst
|
|
// laeuft das Modell ganz ohne System-Prompt — der Brain schickt aber immer
|
|
// eine System-Message + Tool-Block, also ist er real nie leer.
|
|
return {
|
|
prompt: conversationToPrompt(request.messages),
|
|
systemPrompt: extractSystemPrompt(request.messages, request.tools),
|
|
model: extractModel(request.model),
|
|
sessionId: request.user,
|
|
};
|
|
}
|