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>
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.
Namensräume
| Name | Bedeutung |
|---|---|
42i/marvin-2609:low|medium|high|max |
konfigurierte Stufen (aliases, Art schwelle: Index ≥ 25/45/49/53, Kontext ≥ 256k/256k/500k/500k) |
42i/marvin-2609-core:none|medium |
lokales Qwen, fest |
42i/marvin-tts|stt|embedding[-turbo], 42i/marvin-free |
TTS/STT/Embeddings lokal bzw. Mistral/OpenRouter, fest |
deizo/kira-reasoning, deizo/kira-stt |
Modelle im Prod-RZ deizo, fest |
openrouter/<id>[:effort] |
1:1 an OpenRouter, Effort als reasoning.effort |
alt/<id>[:effort] |
günstigste Alternative derselben Klasse wie <id> auf dieser Stufe |
role/<name> |
Rolle aus rollen, zeigt auf einen der obigen Namen |
| alles andere | Durchreiche an LiteLLM (fallback), unverändert mit Client-Key |
Klasse (alt/): Intelligence-, Coding- und Agentic-Index jeweils mindestens
95 % der Referenz, Kontext mindestens 90 %, gewichtet billiger. Intelligence und
Coding kommen je Effort-Stufe von Artificial Analysis, Agentic nur je Modell (AA
misst ihn nicht je Stufe). Hat die Referenz einen Wert, den ein Kandidat nicht
hat, fällt der Kandidat raus. Je Kandidat zählt die niedrigste Stufe, die die
Klasse erreicht; die Stufe des Ziels wird als reasoning.effort gesetzt, ein
vom Client mitgeschicktes reasoning_effort verworfen. Findet sich nichts
Billigeres, wird die Referenz selbst gerufen.
alt/ wird bei der ersten Anfrage aufgelöst (Latenz- und Live-Probe, keine
Last-Probe) und dann im 12-h-Takt mit erneuert. Die konfigurierten 42i/*-Stufen
fahren bei einem Wechsel zusätzlich die Last-Probe (78k Kontext, dreimal).
Rollen entkoppeln Agenten von Modellnamen: ein Agent ruft role/worker,
welches Modell dahintersteht, stellt man in config.json um, ohne Container.
Vorbelegung: xo/architect/teamleader/lead → 42i/marvin-2609:high, alle
anderen → :medium.
Rechte (key-acls.json): Name → erlaubte Namen, * am Ende ist ein
Präfix-Muster (openrouter/*, alt/*, role/*); [] = alles. Geprüft wird
der angefragte Name, vor der Rollenauflösung.
/v1/models zeigt je Key alles Erlaubte: Aliasse, Rollen, alle OpenRouter-
Modelle des Kontos als openrouter/… und alt/…, jeweils auch mit den bei AA
gemessenen Effort-Stufen, plus die LiteLLM-Liste im Parallelbetrieb.
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:4010/stats?days=7
Die Antwort trägt x-llmrouter-model, damit ein Client sieht, was wirklich lief.
Betrieb
PORT=4010 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 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,
/v1/audio/translations, /v1/models (zeigt je Key die erlaubten Aliasse),
/health. Der Präfix /v1 ist optional.