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 mit KEYS_FILE=… bun run src/keys.ts add <name> [--token <bestehend>]; mit --token lä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_KEY schützt GET /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.

S
Description
No description provided
Readme
374 KiB
Languages
TypeScript 95.1%
Python 4.1%
Dockerfile 0.8%