From 81ab596897dbf321f004045d95a724f7ca9b2231 Mon Sep 17 00:00:00 2001 From: Elton Turing Date: Sat, 5 Sep 2026 08:09:46 +0200 Subject: [PATCH] llmrouter: Namen 42i/marvin-2609:*, Durchreiche unbekannter Namen an LiteLLM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caddy zeigt llm.lan künftig auf den Router; alles Unbekannte (2608, role/*) geht unverändert mit dem Client-Key an LiteLLM :4000. Umstellung je Agent = Modellname, kein base_url-Wechsel (Andreas 2026-09-05). altnamen entfällt, bis LiteLLM abgeschaltet wird. Co-Authored-By: Claude Fable 5.1 --- README.md | 17 ++++++++++---- config.json | 42 +++++++++++++-------------------- key-acls.json | 64 +++++++++++++++++++++++++-------------------------- src/config.ts | 9 +++++--- src/server.ts | 18 +++++++++++---- 5 files changed, 80 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 7fd202b..7adbf91 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,16 @@ Zwei Arten: Embeddings, kira auf deizo). Optional ein `body`-Patch, etwa `chat_template_kwargs.enable_thinking` für die Qwen-Stufen. -`altnamen` übersetzt die LiteLLM-Namen (`42i/marvin-2608:low` …), damit kein -Container angefasst werden muss; geloggt wird der neue Name. Abbauen, sobald -die Clients umgestellt sind. +Die Namen sind versioniert wie bei LiteLLM: `42i/marvin-2609:low` … `:max`, +`42i/marvin-2609-core:none|medium`. **Unbekannte Namen** (alles aus der +LiteLLM-Zeit, `42i/marvin-2608:*`) reicht der Router unverändert mit dem +Client-Key an LiteLLM auf :4000 durch (`fallback`). Caddy zeigt `llm.lan` +auf den Router; ein Agent wechselt also per Modellname 2608 → 2609, nicht +per base_url, und rollt per Umbenennen zurück. Durchgereichte Anfragen stehen +im Log mit Upstream `litellm`, ohne Kosten (die kennt nur LiteLLM). + +Wird LiteLLM abgeschaltet, entfällt `fallback`, und `altnamen` (Übersetzung +2608 → 2609) übernimmt, damit auch Nachzügler nicht brechen. ## Auflösung und Takt @@ -81,8 +88,8 @@ PORT=4010 KEYS_FILE=/srv/llmrouter/keys.json ADMIN_KEY=… OPENROUTER_API_KEY= bun run build # dist/llmrouter, ein statisches Linux-Binary ``` -Parallelbetrieb neben LiteLLM (Port 4000) im LXC 185020 auf Port 4010; die -Umschaltung ist ein Wechsel der `base_url` in den Client-Configs. +Parallelbetrieb neben LiteLLM (Port 4000) im LXC 185020 auf Port 4010; Caddy +zeigt `llm.lan` auf 4010, die Umschaltung je Agent ist der Modellname. Bedient: `/v1/chat/completions`, `/v1/completions`, `/v1/responses`, `/v1/embeddings`, `/v1/audio/speech`, `/v1/audio/transcriptions`, diff --git a/config.json b/config.json index 50ff13a..e7427b2 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "_doku": "Versionierte Quelle des llmrouter. Secrets kommen NUR aus der Umgebung: OPENROUTER_API_KEY, MISTRAL_API_KEY, KIRA_REASONING_KEY, KIRA_STT_KEY, ARTIFICIALANALYSIS_API_KEY, ADMIN_KEY; Client-Keys aus KEYS_FILE (token -> name), Rechte aus key-acls.json (name -> aliasse). Port 4010, weil 4001 im LXC von nginx belegt ist. MODEL_BLOCKLIST zeigt auf die Sperrliste (Host: /srv/llmrouter/model-blocklist.json).", + "_doku": "Versionierte Quelle des llmrouter. Secrets kommen NUR aus der Umgebung: OPENROUTER_API_KEY, MISTRAL_API_KEY, KIRA_REASONING_KEY, KIRA_STT_KEY, ARTIFICIALANALYSIS_API_KEY, ADMIN_KEY; Client-Keys aus KEYS_FILE (token -> name), Rechte aus key-acls.json (name -> aliasse). Port 4010 (4001 im LXC von nginx belegt); Caddy zeigt llm.lan auf 4010, LiteLLM bleibt auf 4000 und bekommt alles, was hier unbekannt ist (fallback). MODEL_BLOCKLIST zeigt auf die Sperrliste (Host: /srv/llmrouter/model-blocklist.json).", "port": 4010, "logDir": "./log", "refreshHours": 12, @@ -31,25 +31,29 @@ "kira-stt": { "base": "http://10.6.42.111:8001/v1", "keyEnv": "KIRA_STT_KEY" + }, + "litellm": { + "base": "http://127.0.0.1:4000/v1", + "clientKey": true } }, "aliases": { - "marvin:low": { + "42i/marvin-2609:low": { "art": "schwelle", "index": 25, "minContext": 256000 }, - "marvin:medium": { + "42i/marvin-2609:medium": { "art": "schwelle", "index": 45, "minContext": 256000 }, - "marvin:high": { + "42i/marvin-2609:high": { "art": "schwelle", "index": 49, "minContext": 500000 }, - "marvin:max": { + "42i/marvin-2609:max": { "art": "schwelle", "index": 53, "minContext": 500000 @@ -59,7 +63,7 @@ "upstream": "openrouter", "model": "openrouter/free" }, - "marvin-core:none": { + "42i/marvin-2609-core:none": { "art": "fest", "upstream": "qwen", "model": "qwen36:35b", @@ -69,7 +73,7 @@ } } }, - "marvin-core:medium": { + "42i/marvin-2609-core:medium": { "art": "fest", "upstream": "qwen", "model": "qwen36:35b", @@ -120,23 +124,9 @@ "model": "mistralai/mistral-embed-2312" } }, - "_altnamen_doku": "Übergangs-Aliasse aus der LiteLLM-Zeit, damit kein Container angefasst werden muss. Loggen unter dem NEUEN Namen. Nach und nach abbauen. Stufen 2026-09-05 (Andreas): low=Haiku 4.5, medium=Sonnet 5, high=Opus 5 medium, max=Fable 5.1 medium; none/extra/ultra/mass abgeschafft.", - "altnamen": { - "42i/marvin-2608": "marvin:low", - "42i/marvin-2608:none": "marvin:low", - "42i/marvin-2608:low": "marvin:low", - "42i/marvin-2608:medium": "marvin:medium", - "42i/marvin-2608:high": "marvin:high", - "42i/marvin-2608:extra": "marvin:high", - "42i/marvin-2608:ultra": "marvin:max", - "42i/marvin-2608-auto": "marvin:medium", - "42i/marvin-2608-mass": "marvin:low", - "42i/marvin-2608-mass:none": "marvin:low", - "42i/marvin-2608-mass:medium": "marvin:low", - "42i/marvin-2608-core:none": "marvin-core:none", - "42i/marvin-2608-core:medium": "marvin-core:medium", - "marvin-reasoning": "marvin-core:medium", - "marvin-core": "marvin-core:none" - }, - "_stufen_doku": "Stufen als Index-Schwelle (Artificial-Analysis-Intelligence-Index, 0-100) mit Mindestkontext (Andreas 2026-09-05): low 25/256k, medium 45/256k, high 49/500k, max 53/500k. Die Art \"alternative\" mit Referenzmodell (z.B. anthropic/claude-opus-5:medium) bleibt möglich." + "_stufen_doku": "Stufen als Index-Schwelle (Artificial-Analysis-Intelligence-Index, 0-100) mit Mindestkontext (Andreas 2026-09-05): low 25/256k, medium 45/256k, high 49/500k, max 53/500k. Die Art \"alternative\" mit Referenzmodell (z.B. anthropic/claude-opus-5:medium) bleibt möglich. Namen versioniert wie bei LiteLLM (2608): 42i/marvin-2609:*.", + "fallback": { + "upstream": "litellm", + "_doku": "Unbekannte Modellnamen (z.B. 42i/marvin-2608:*) gehen unverändert mit dem Client-Key an LiteLLM. Umstellung je Agent = Modellname 2608 -> 2609. Fällt weg, wenn LiteLLM abgeschaltet wird; dann kommen die 2608-Namen als altnamen zurück." + } } diff --git a/key-acls.json b/key-acls.json index 5d04419..ce2bff8 100644 --- a/key-acls.json +++ b/key-acls.json @@ -1,41 +1,41 @@ { - "_note": "Rechte je Client-Key: Name -> erlaubte Aliasse (neue Namen; Altnamen werden vor der Pruefung uebersetzt). [] = alles. Die Token selbst liegen NICHT hier, sondern in KEYS_FILE auf dem Host (token -> name). Abgeleitet aus llmlite/key-acls.json am 2026-09-05: role/* und -auto gestrichen (Andreas), -auto -> :medium. Stand 2026-09-05 abends: Stufen low/medium/high/max; none->low, extra->high, ultra->max, mass->low.", + "_note": "Rechte je Client-Key: Name -> erlaubte Aliasse (neue Namen; Altnamen werden vor der Pruefung uebersetzt). [] = alles. Die Token selbst liegen NICHT hier, sondern in KEYS_FILE auf dem Host (token -> name). Abgeleitet aus llmlite/key-acls.json am 2026-09-05: role/* und -auto gestrichen (Andreas), -auto -> :medium. Stand 2026-09-05 abends: Stufen low/medium/high/max; none->low, extra->high, ultra->max, mass->low. Namen ab 2026-09-05: 42i/marvin-2609:*; die Rechte gelten nur fuer Router-Aliasse, unbekannte Namen (2608) reicht der Router an LiteLLM durch, dort gelten dessen ACLs.", "keys": { "persona-spark": [ - "marvin:medium", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts" ], "persona-buzz": [ - "marvin:medium", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts" ], "persona-jessie": [ - "marvin:medium", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts" ], "persona-klara": [ - "marvin:medium", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-stt", "marvin-tts" ], "persona-elton": [ - "marvin:low", - "marvin:medium", - "marvin:high", - "marvin:max", - "marvin-core:none", - "marvin-core:medium", + "42i/marvin-2609:low", + "42i/marvin-2609:medium", + "42i/marvin-2609:high", + "42i/marvin-2609:max", + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", "kira-reasoning", "kira-stt", "marvin-stt", @@ -44,24 +44,24 @@ "marvin-tts-turbo" ], "persona-festus": [ - "marvin-core:none", - "marvin-core:medium", - "marvin:medium", + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt" ], "persona-aria": [ - "marvin-core:none", - "marvin-core:medium", - "marvin:medium", + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt" ], "persona-inka": [ - "marvin-core:none", - "marvin-core:medium", - "marvin:low", - "marvin:medium", + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:low", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-stt", @@ -70,10 +70,10 @@ "marvin-tts-turbo" ], "persona-inka-mx": [ - "marvin-core:none", - "marvin-core:medium", - "marvin:low", - "marvin:medium", + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:low", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-stt", @@ -83,8 +83,8 @@ ], "persona-andreas": [], "nightjob": [ - "marvin-core:none", - "marvin:medium", + "42i/marvin-2609-core:none", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt", "marvin-embedding", @@ -95,15 +95,15 @@ "marvin-tts-turbo" ], "lab-xo": [ - "marvin:low", - "marvin:high", - "marvin:medium", + "42i/marvin-2609:low", + "42i/marvin-2609:high", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt" ], "team-copilot": [ - "marvin:low", - "marvin:medium", + "42i/marvin-2609:low", + "42i/marvin-2609:medium", "kira-reasoning", "kira-stt" ] diff --git a/src/config.ts b/src/config.ts index 1b63a09..5a7167b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,7 +10,7 @@ import { readFileSync } from "fs"; import { dirname, resolve } from "path"; -export type Upstream = { base: string; keyEnv?: string; maxParallel?: number }; +export type Upstream = { base: string; keyEnv?: string; maxParallel?: number; /** Client-Token unveraendert weitergeben (Durchreiche an LiteLLM) */ clientKey?: boolean }; export type Alias = | { art: "alternative"; referenz: string } | { art: "schwelle"; index: number; minContext: number } @@ -23,7 +23,9 @@ export type Config = { lasttest: boolean; upstreams: Record; aliases: Record; - altnamen: Record; + altnamen?: Record; + /** Unbekannte Modellnamen unveraendert an diesen Upstream (Parallelbetrieb mit LiteLLM). */ + fallback?: { upstream: string }; }; export const BASE = resolve(dirname(import.meta.path), ".."); @@ -34,7 +36,8 @@ export function configLaden(): Config { for (const [name, a] of Object.entries(c.aliases)) { if (a.art === "fest" && c.upstreams[a.upstream] === undefined) throw new Error(`Alias ${name}: Upstream ${a.upstream} fehlt`); } - for (const [alt, ziel] of Object.entries(c.altnamen)) { + if (c.fallback !== undefined && c.upstreams[c.fallback.upstream] === undefined) throw new Error(`fallback: Upstream ${c.fallback.upstream} fehlt`); + for (const [alt, ziel] of Object.entries(c.altnamen ?? {})) { if (c.aliases[ziel] === undefined) throw new Error(`Altname ${alt} zeigt auf unbekannten Alias ${ziel}`); } c.logDir = resolve(dirname(CONFIG_PATH), c.logDir); diff --git a/src/server.ts b/src/server.ts index 8d25ba0..8021709 100644 --- a/src/server.ts +++ b/src/server.ts @@ -53,9 +53,18 @@ async function belegen(name: string, u: Upstream): Promise<() => void> { // --- Aufloesung Alias -> Ziel ------------------------------------------------ type Ziel = { alias: string; upstreamName: string; upstream: Upstream; model: string; effort: string | null; body?: Record; alternative: boolean }; function zielBestimmen(angefragt: string, keyName: string): Ziel | { fehler: string; status: number } { - const alias = config.altnamen[angefragt] ?? angefragt; + const alias = config.altnamen?.[angefragt] ?? angefragt; const def = config.aliases[alias]; - if (def === undefined) return { fehler: `Unbekanntes Modell ${angefragt}`, status: 404 }; + if (def === undefined) { + // Durchreiche: unbekannter Name geht unveraendert an LiteLLM, mit dem + // Client-Key -- dort gelten dessen ACLs. So wechselt ein Agent per + // Modellname (2608 -> 2609), nicht per base_url. + if (config.fallback !== undefined) { + const name = config.fallback.upstream; + return { alias: `(durchreiche)`, upstreamName: name, upstream: config.upstreams[name], model: angefragt, effort: null, alternative: false }; + } + return { fehler: `Unbekanntes Modell ${angefragt}`, status: 404 }; + } const erlaubt = acl.get(keyName) ?? []; if (erlaubt.length > 0 && !erlaubt.includes(alias)) return { fehler: `${keyName} darf ${alias} nicht rufen`, status: 403 }; if (def.art === "fest") { @@ -126,7 +135,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(jsonBody(orig, ziel)); + upstreamBody = JSON.stringify(ziel.upstream.clientKey === true ? orig : jsonBody(orig, ziel)); headers.set("content-type", "application/json"); } else { const form = await req.formData(); @@ -134,11 +143,12 @@ Bun.serve({ const z = zielBestimmen(angefragt, keyName); if ("fehler" in z) return Response.json({ error: { message: z.fehler } }, { status: z.status }); ziel = z; - form.set("model", ziel.model); + if (ziel.upstream.clientKey !== true) form.set("model", ziel.model); upstreamBody = form; } const k = upstreamKey(ziel.upstream); if (k !== undefined) headers.set("authorization", `Bearer ${k}`); + if (ziel.upstream.clientKey === true) headers.set("authorization", `Bearer ${auth}`); 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);