/** * ARIA-patched cli-to-openai adapter. * * Erweitert die npm-Version von claude-max-api-proxy: * - normalizeModelName ist null-safe (Original-Patch der vorher per sed lief). * - Parser fuer {json}-Bloecke im Result-Text: * Wenn welche gefunden werden, wandert das in `message.tool_calls` * (OpenAI-Format) und finish_reason=tool_calls. Der restliche Text * (alles ausserhalb der Bloecke) wird verworfen, weil das interner * Tool-Use-Schritt war, nicht User-facing. * * Wird zur Container-Startzeit ueber die npm-Version geschrieben * (siehe docker-compose.yml proxy-Block). */ import { randomUUID } from "crypto"; export function extractTextContent(message) { return message.message.content .filter((c) => c.type === "text") .map((c) => c.text) .join(""); } export function cliToOpenaiChunk(message, requestId, isFirst = false) { const text = extractTextContent(message); return { id: `chatcmpl-${requestId}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: normalizeModelName(message.message.model), choices: [ { index: 0, delta: { role: isFirst ? "assistant" : undefined, content: text, }, finish_reason: message.message.stop_reason ? "stop" : null, }, ], }; } export function createDoneChunk(requestId, model) { return { id: `chatcmpl-${requestId}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: normalizeModelName(model), choices: [ { index: 0, delta: {}, finish_reason: "stop", }, ], }; } /** * Sucht im Result-Text alle {json} * Bloecke. Gibt [{id, name, arguments(json-string)}, restText] zurueck. * * Defensiv: * - "name"-Attribut sowohl in Doppel- als auch Einzelhochkommata * - Whitespace beim JSON tolerant * - Bei JSON-Parse-Fehler: das Argument wird als _raw weitergereicht * (unser Brain-Side-Parser kennt das) */ function _parseToolCalls(text) { if (!text || typeof text !== "string") return { tool_calls: [], rest: text || "" }; const re = /([\s\S]*?)<\/tool_call>/gi; const tcs = []; let lastIndex = 0; const restParts = []; let m; while ((m = re.exec(text)) !== null) { restParts.push(text.slice(lastIndex, m.index)); const name = m[1]; let argsBody = (m[2] || "").trim(); // Fences entfernen falls Claude welche eingebaut hat argsBody = argsBody.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "").trim(); if (!argsBody) argsBody = "{}"; // Validieren — aber in OpenAI-Format ist arguments immer ein STRING try { JSON.parse(argsBody); } catch (_) { // Behalten als Roh-String — Brain-Side toleriert das via {_raw:...} } tcs.push({ id: `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`, type: "function", function: { name, arguments: argsBody }, }); lastIndex = re.lastIndex; } restParts.push(text.slice(lastIndex)); return { tool_calls: tcs, rest: restParts.join("").trim() }; } export function cliResultToOpenai(result, requestId) { const modelName = result.modelUsage ? Object.keys(result.modelUsage)[0] : "claude-sonnet-4"; const rawText = result.result || ""; const { tool_calls, rest } = _parseToolCalls(rawText); const message = { role: "assistant" }; let finishReason = "stop"; if (tool_calls.length > 0) { message.tool_calls = tool_calls; // Wenn Claude neben den Tool-Calls noch Text geschrieben hat, behalten // wir den im content — Brain-Seite kann ihn als Pre-Tool-Plaintext sehen. // Wenn nur Tool-Calls da waren (rest leer), content explizit null. message.content = rest || null; finishReason = "tool_calls"; } else { message.content = rawText; } return { id: `chatcmpl-${requestId}`, object: "chat.completion", created: Math.floor(Date.now() / 1000), model: normalizeModelName(modelName), choices: [ { index: 0, message, finish_reason: finishReason }, ], usage: { prompt_tokens: result.usage?.input_tokens || 0, completion_tokens: result.usage?.output_tokens || 0, total_tokens: (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0), }, }; } 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; }