From 0d971b81dcd1e247863ddce4b068ce2442417083 Mon Sep 17 00:00:00 2001 From: Elton Turing Date: Sat, 5 Sep 2026 12:25:58 +0200 Subject: [PATCH] =?UTF-8?q?llmrouter:=20Anthropic-Messages-Pfad=20(/messag?= =?UTF-8?q?es,=20count=5Ftokens)=20f=C3=BCr=20Claude=20Code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Weiterleitung an OpenRouters Messages-Endpunkt (x-api-key, anthropic-version, anthropic-beta), Effort als reasoning.effort, Usage in Anthropic-Form gelesen (auch im Stream aus message_start/message_delta). Nur openrouter-Ziele; lokale llama-server sprechen kein Messages-Format. Verifiziert: claude -p über http://llm.lan:4010 mit alt/anthropic/claude-sonnet-5 -> glm-5.3-flash. Co-Authored-By: Claude Fable 5.1 --- src/log.ts | 12 ++++++++---- src/server.ts | 26 ++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/log.ts b/src/log.ts index afe97db..7633b11 100644 --- a/src/log.ts +++ b/src/log.ts @@ -63,11 +63,15 @@ export class Log { /** Zieht Tokens und Kosten aus einer OpenAI-/OpenRouter-Usage. */ export function usageLesen(u: unknown): Pick { - const usage = (u ?? {}) as { prompt_tokens?: number; completion_tokens?: number; cost?: number; prompt_tokens_details?: { cached_tokens?: number } }; + // OpenAI-Form (prompt_tokens/completion_tokens) und Anthropic-Form (input_tokens/output_tokens) + const usage = (u ?? {}) as { + prompt_tokens?: number; completion_tokens?: number; cost?: number; prompt_tokens_details?: { cached_tokens?: number }; + input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number; + }; return { - prompt_tokens: usage.prompt_tokens ?? null, - completion_tokens: usage.completion_tokens ?? null, - cached_tokens: usage.prompt_tokens_details?.cached_tokens ?? null, + prompt_tokens: usage.prompt_tokens ?? usage.input_tokens ?? null, + completion_tokens: usage.completion_tokens ?? usage.output_tokens ?? null, + cached_tokens: usage.prompt_tokens_details?.cached_tokens ?? usage.cache_read_input_tokens ?? null, cost: typeof usage.cost === "number" ? usage.cost : null, }; } diff --git a/src/server.ts b/src/server.ts index d42a07c..2bd880b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -116,11 +116,19 @@ async function zielBestimmen(angefragt: string, keyName: string): Promise, ziel: Ziel): Record { +function jsonBody(orig: Record, ziel: Ziel, pfad: string): Record { const b: Record = { ...orig, model: ziel.model, ...(ziel.body ?? {}) }; + if (pfad.startsWith("/messages")) { + // Anthropic-Format: kein usage.include, Effort als reasoning.effort (OpenRouter nimmt es dort an) + if (ziel.effortSetzen) { delete b.thinking; if (ziel.effort !== null) b.reasoning = { effort: ziel.effort }; } + return b; + } if (ziel.upstreamName === "openrouter") { b.usage = { include: true }; // Kosten kommen in der Antwort mit if (b.stream === true) b.stream_options = { ...(b.stream_options as object ?? {}), include_usage: true }; @@ -201,7 +209,7 @@ Bun.serve({ if ("fehler" in z) return Response.json({ error: { message: z.fehler } }, { status: z.status }); ziel = z; stream = orig.stream === true; - upstreamBody = JSON.stringify(ziel.upstream.clientKey === true ? orig : jsonBody(orig, ziel)); + upstreamBody = JSON.stringify(ziel.upstream.clientKey === true ? orig : jsonBody(orig, ziel, pfad)); headers.set("content-type", "application/json"); } else { const form = await req.formData(); @@ -215,6 +223,12 @@ Bun.serve({ const k = upstreamKey(ziel.upstream); if (k !== undefined) headers.set("authorization", `Bearer ${k}`); if (ziel.upstream.clientKey === true) headers.set("authorization", `Bearer ${auth}`); + if (pfad.startsWith("/messages")) { + if (ziel.upstreamName !== "openrouter") return Response.json({ error: { type: "invalid_request_error", message: `${angefragt} liegt auf ${ziel.upstreamName}, das kein Messages-Format spricht` } }, { status: 400 }); + if (k !== undefined) headers.set("x-api-key", k); + headers.set("anthropic-version", req.headers.get("anthropic-version") ?? "2023-06-01"); + const beta = req.headers.get("anthropic-beta"); if (beta) headers.set("anthropic-beta", beta); + } if (ziel.upstreamName === "openrouter") { headers.set("http-referer", "https://llm.lan"); headers.set("x-title", `llmrouter/${keyName}`); } const accept = req.headers.get("accept"); if (accept) headers.set("accept", accept); @@ -255,7 +269,11 @@ Bun.serve({ while ((i = rest.indexOf("\n")) >= 0) { const zeile = rest.slice(0, i).trim(); rest = rest.slice(i + 1); if (zeile.startsWith("data:") && zeile.includes('"usage"')) { - try { const j = JSON.parse(zeile.slice(5)); if (j.usage) usage = j.usage; } catch { /* Teilzeile */ } + try { + const j = JSON.parse(zeile.slice(5)); + const u = j.usage ?? j.message?.usage; // OpenAI: usage | Anthropic: message_start.message.usage, message_delta.usage + if (u) usage = { ...(usage as object ?? {}), ...u }; + } catch { /* Teilzeile */ } } } },