OpenAI-kompatibler Reverse-Proxy in Bun: Client-Key -> ACL, Alias -> Modell (alternative: günstigstes Modell derselben Klasse über llmlite/openrouter-auswahl inkl. Effort-Achse; fest: lokale Upstreams, TTS/STT, Embeddings), Usage/Kosten aus der OpenRouter-Antwort (auch im Stream), JSONL + SQLite-Log, Tagesstatistik je Key. Altnamen der LiteLLM-Zeit werden übersetzt. Kein 3456/stdcmpt, kein role/*, kein -auto (Andreas 2026-09-05). Lokal verifiziert: Chat, Stream, ACL, Altname, Kosten im Log. Parallelbetrieb auf :4001 geplant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.9 KiB
llmrouter
Leichtgewichtiger Ersatz für den LiteLLM-Gateway llm.lan (siehe ../llmlite).
Ein OpenAI-kompatibler Reverse-Proxy in Bun/TypeScript mit genau vier
Eingriffen je Anfrage: Client-Key prüfen, Alias auf ein Modell auflösen,
Usage und Kosten aus der Antwort mitlesen, eine Log-Zeile schreiben.
Keine Format-Übersetzung, keine Datenbank, kein Admin-UI. Alle Upstreams sprechen OpenAI-Format (OpenRouter, llama-server, Ollama, Mistral), deshalb reicht Durchreichen. Entscheidung Andreas, 2026-09-05.
Aliasse (config.json)
Zwei Arten:
alternative— der Alias nennt ein Referenzmodell mit optionaler Effort-Stufe (openai/gpt-5.6-luna:low,anthropic/claude-opus-5:medium). Gerufen wird das günstigste OpenRouter-Modell derselben Klasse, nach der Regel in../llmlite/openrouter-auswahl.ts(Intelligence-Index je Stufe von Artificial Analysis, Kontext, Tool-Calls, Sperrliste, Live-Probe, Last-Probe bei Wechsel). Findet sich nichts Billigeres, wird die Referenz selbst gerufen. Die Effort-Stufe des Ziels wird alsreasoning.effortgesetzt; ein vom Client mitgeschicktesreasoning_effortwird verworfen, die Klasse gehört zum Alias.fest— Upstream und Modell stehen fest (lokales Qwen, TTS/STT, Embeddings, kira auf deizo). Optional einbody-Patch, etwachat_template_kwargs.enable_thinkingfü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.
Auflösung und Takt
src/resolver.ts löst alle alternative-Aliasse auf und schreibt
log/state.json. OpenRouter-Preise ändern sich oft: alle refreshHours (12)
neu. AA-Indexe ändern sich nie, nur neue Modelle kommen dazu: log/aa-cache.json,
alle aaRefreshDays (7) neu. Der Server frischt selbst auf (beim Start, wenn
veraltet, und im 15-Minuten-Takt geprüft); Wechsel stehen in log/changes.log.
bun run src/resolver.ts --force # jetzt auflösen, Tabelle zeigen
LLMROUTER_LASTTEST=0 bun run src/resolver.ts --force # ohne Last-Probe (Tests; spart ~210k Token je Wechsel)
Der erste Lauf mit leerem State prüft jeden Alias unter Last, das ist gewollt.
Keys und Rechte
key-acls.json(versioniert): Name → erlaubte Aliasse,[]= alles.KEYS_FILE(nur auf dem Host, 0600): Token → Name. Pflege mitKEYS_FILE=… bun run src/keys.ts add <name> [--token <bestehend>]; mit--tokenlässt sich der LiteLLM-Key eines Agenten übernehmen, sodass der Container nichts merkt.- Upstream-Schlüssel aus der Umgebung:
OPENROUTER_API_KEY,MISTRAL_API_KEY. ADMIN_KEYschütztGET /stats?days=N,GET /zuordnung,POST /auffrischen.
Log und Statistik
Je Anfrage eine Zeile in log/requests.jsonl und in log/requests.sqlite:
Key, Alias, angefragter Name, tatsächliches Modell, Effort, Tokens
(prompt/completion/cached), Kosten wie von OpenRouter gemeldet
(usage.cost, auch im Stream über den letzten Chunk). Lokale Upstreams haben
keine Kosten, das Feld bleibt leer, wir erfinden keine Preise.
bun run src/stats.ts 7 # Tag × Key: Anfragen, Tokens, Kosten, Modelle
curl -H "Authorization: Bearer $ADMIN_KEY" http://llm.lan:4001/stats?days=7
Die Antwort trägt x-llmrouter-model, damit ein Client sieht, was wirklich lief.
Betrieb
PORT=4001 KEYS_FILE=/srv/llmrouter/keys.json ADMIN_KEY=… OPENROUTER_API_KEY=… bun run src/server.ts
bun run build # dist/llmrouter, ein statisches Linux-Binary
Parallelbetrieb neben LiteLLM (Port 4000) im LXC 185020 auf Port 4001; die
Umschaltung ist ein Wechsel der base_url in den Client-Configs.
Bedient: /v1/chat/completions, /v1/completions, /v1/responses,
/v1/embeddings, /v1/audio/speech, /v1/audio/transcriptions,
/v1/audio/translations, /v1/models (zeigt je Key die erlaubten Aliasse),
/health. Der Präfix /v1 ist optional.