llmrouter: leichtgewichtiger Gateway-Ersatz mit Alternativ-Auflösung

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>
This commit is contained in:
2026-09-05 07:03:12 +02:00
co-authored by Claude Fable 5.1
commit 7897fa9651
11 changed files with 667 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
# 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 als `reasoning.effort` gesetzt; ein vom
Client mitgeschicktes `reasoning_effort` wird verworfen, die Klasse gehört
zum Alias.
- **`fest`** — Upstream und Modell stehen fest (lokales Qwen, TTS/STT,
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.
## 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 <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.
```bash
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
```bash
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.