Files
llmrouter/README.md
T
eltonandClaude Fable 5.1 34d6c08a3d llmrouter: Namensräume openrouter/, alt/, role/; Klasse über drei Indexe relativ
- openrouter/<id>[:effort] 1:1, alt/<id>[:effort] günstigste Alternative auf
  Abruf (ohne Last-Probe, im Takt erneuert), role/<name> aus config.rollen
- Klasse: Intelligence, Coding, Agentic je >= 95 % der Referenz (relativ statt
  3 Punkte), Kontext >= 90 %, billiger; Kandidaten auch ohne OpenRouter-Index,
  wenn AA Stufen kennt (OpenRouter führt Intelligence nur für 53 Modelle)
- ACL-Muster mit *, /models mit allen Konto-Modellen je Namensraum und Stufe
- Rollen aus der LiteLLM-Zeit übernommen (Führung high, Rest medium)
Andreas 2026-09-05.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 09:47:22 +02:00

103 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
| `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`.
```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: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.