# 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 | | `deizo/kira-reasoning`, `deizo/kira-stt` | Modelle im Prod-RZ deizo, fest | | `openrouter/[:effort]` | 1:1 an OpenRouter, Effort als `reasoning.effort` | | `alt/[:effort]` | **günstigste Alternative derselben Klasse** wie `` auf dieser Stufe | | `role/` | 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`. ```bash 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 [--token ]`; 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. ```bash 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 ```bash 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.