llmrouter: Anthropic-Messages-Pfad (/messages, count_tokens) für Claude Code
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 <noreply@anthropic.com>
This commit is contained in:
+8
-4
@@ -63,11 +63,15 @@ export class Log {
|
||||
|
||||
/** Zieht Tokens und Kosten aus einer OpenAI-/OpenRouter-Usage. */
|
||||
export function usageLesen(u: unknown): Pick<Eintrag, "prompt_tokens" | "completion_tokens" | "cached_tokens" | "cost"> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
+22
-4
@@ -116,11 +116,19 @@ async function zielBestimmen(angefragt: string, keyName: string): Promise<Ziel |
|
||||
}
|
||||
|
||||
// --- Anfrage bauen -------------------------------------------------------------
|
||||
const JSON_PFADE = new Set(["/chat/completions", "/completions", "/embeddings", "/audio/speech", "/responses"]);
|
||||
// /messages + /messages/count_tokens: Anthropic-Messages-Format (Claude Code). OpenRouter
|
||||
// bietet denselben Endpunkt fuer alle Modelle -- die einzige Format-Uebersetzung, die wir
|
||||
// nicht selbst schreiben. Nur openrouter-Ziele; lokale llama-server koennen das nicht.
|
||||
const JSON_PFADE = new Set(["/chat/completions", "/completions", "/embeddings", "/audio/speech", "/responses", "/messages", "/messages/count_tokens"]);
|
||||
const FORM_PFADE = new Set(["/audio/transcriptions", "/audio/translations"]);
|
||||
|
||||
function jsonBody(orig: Record<string, unknown>, ziel: Ziel): Record<string, unknown> {
|
||||
function jsonBody(orig: Record<string, unknown>, ziel: Ziel, pfad: string): Record<string, unknown> {
|
||||
const b: Record<string, unknown> = { ...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 */ }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user