README als Einstieg mit Docker-Schnellstart; die bisherige README lebt als docs/referenz.md weiter, der Betriebsstand ist aus info nach docs/betrieb.md portiert.
14 KiB
llmrouter — Betrieb (llm.lan, seit 2026-09-05)
Der schlanke Nachfolger des LiteLLM-Gateways: ein OpenAI-kompatibler
Reverse-Proxy in Bun/TypeScript. llm.lan zeigt auf ihn. Er kennt vier
Eingriffe je Anfrage: Client-Key prüfen, Modellnamen auflösen, Kosten aus der
Antwort mitlesen, eine Log-Zeile schreiben. Keine Datenbank, kein Admin-UI,
keine Format-Übersetzung — alle Upstreams sprechen OpenAI-Format.
Wofür wichtig: Alle Agenten (Spark, Buzz, Klara, Jessie, Inka …), opencode auf dem Mac und Claude Code im Terminal sprechen nur diesen Endpunkt. Fällt er aus, steht die Agenten-Arbeit. Provider-Keys (OpenRouter, Mistral, deizo) liegen nur hier.
Diese Seite ist der Betriebsstand für Störung, Wartung und Nachfolge; die technische Referenz steht in referenz.md.
Wo läuft was
| Was | Wo |
|---|---|
Router (llmrouter) |
LXC 185020 (llmlite, 10.18.5.20) auf s18p1, systemd-Unit llmrouter, Port 4010 |
| Alter Port 4000 | seit 2026-09-07 von Caddy auf 4010 geleitet (vorher LiteLLM). Hart verdrahtete Clients — Agenten-Images, spark-desktop, ältere Skripte — laufen darüber weiter |
| LiteLLM | abgeschaltet am 2026-09-07 (systemctl stop/disable llmlite). Dateien und Container liegen unangetastet in /srv/llmlite, letzte Kosten-Datenbank als spend-final-20260907.sql.gz |
Caddy (llm.lan, TLS über acme.lan) |
derselbe LXC, eigener oci-Dienst caddy (/srv/caddy, systemd-Unit caddy.service), reverse_proxy localhost:4010 |
| Dateien | /srv/llmrouter/: Binaries llmrouter, llmrouter-resolve, llmrouter-stats, llmrouter-keys; config.json, key-acls.json, model-blocklist.json; keys.json (Token → Name, 0600); .env (0600); log/ |
| Docker-Image (Demo/Basis) | git.42i.org/spine/llmrouter:latest, gebaut von CI; docker compose up mit ./data-Mount als Demo |
| Lokales Qwen | LLM-Knoten LXC 189080, 10.18.5.30:11507 |
| Sprachdienste lokal | 10.18.5.30:8803 (Qwen3-TTS + whisper.cpp), Ollama :11434 (Embeddings) |
| Kira im Prod-RZ (deizo) | 10.6.42.111:8000/8001/8002 über den WireGuard-Peer des LXC |
Normalzustand: curl http://llm.lan:4010/health liefert {"ok":true,"zuordnung":"<Zeitstempel>"};
der Zeitstempel ist jünger als 12 Stunden.
Namensräume — was ein Client rufen kann
Die vollständige Tabelle der Namensräume (Stufen, Profile, Rollen, alt/,
Claude-Code-Pfad) steht in referenz.md. Kurzfassung:
| Name | Bedeutung |
|---|---|
role/<name> |
Rolle (xo, architect, teamleader, worker, qa …) — Agenten kennen nur Rollen |
auto/<profil>[:stufe] |
Profile auf drei Achsen: mass, mass:fast, allround, coding, leader je :junior/:senior |
| `42i/marvin-2609:low | medium |
alt/<id>[:effort] |
günstigste Alternative derselben Klasse; aufgelöst bei der ersten Anfrage, dann alle 12 h |
openrouter/<id>[:effort] |
1:1 an OpenRouter |
deizo/kira-*, 42i/marvin-* |
feste Ziele (Prod-RZ, lokal, Cloud-Sprachdienste) |
| alles andere | 404 — kein stiller Fallback mehr seit der LiteLLM-Abschaltung |
Ein Modellwechsel steht in log/changes.log (im LXC:
/srv/llmrouter/log/changes.log).
Rollen und Profile — drei Ebenen
Ein Agent nennt nie ein Modell. Er fragt seine Rolle, die Rolle zeigt auf
ein Profil, und das Profil sucht sich das billigste Modell über den
geforderten Untergrenzen. Umgestellt wird auf jeder Ebene in config.json —
ohne Deploy, ohne Container anzufassen (42i/intern#1252).
| Profil | Regel | trifft am 2026-09-07 | $/1M gew. |
|---|---|---|---|
auto/mass |
fest: lokales Qwen, Ausweich auf mass:fast |
qwen36:35b | 0 |
auto/mass:fast |
Index ≥ 25 | ling-3.0-flash | 0,27 |
auto/allround:junior |
Index ≥ 40 | glm-5.3-flash | 0,95 |
auto/allround:senior |
Index ≥ 49 | grok-4.6:high | 26,00 |
auto/coding:junior |
Coding ≥ 68, Agentic ≥ 45 | glm-5.3-flash | 0,95 |
auto/coding:senior |
Coding ≥ 76, Agentic ≥ 50 | grok-4.6:high | 26,00 |
auto/leader:junior |
Index ≥ 45, Agentic ≥ 50 | glm-5.3-flash | 0,95 |
auto/leader:senior |
Coding ≥ 74, Agentic ≥ 53 | glm-5.3:max | 15,66 |
Warum drei Achsen und nicht eine Zahl: gemini-3.8-flash hat Coding 76,3 und
Agentic 41,2 — es schreibt guten Text und trägt den Werkzeug-Rundlauf nicht.
Ein einzelner Index sieht diesen Unterschied strukturell nicht.
Rollenbelegung: xo, teamleader, lead → auto/leader:junior;
worker, engineer, qa, perf → auto/coding:junior; alle übrigen →
auto/allround:junior; inka fest auf openrouter/openai/gpt-5.6-luna.
Keine Rolle steht auf einer Senior-Stufe (Andreas, 2026-09-07) — hochgestuft
wird einzeln und mit Anlass, nicht vorsorglich.
Der Ausweich gilt für Ziele ohne eigenen Rückfall — festes Ziel wie
auto/mass (lokales Qwen) oder Schwelle auf einem Ausfall-Modell wie
auto/mass:fast. Steht der Knoten, hält eine fremde Arbeit die Karte oder
liefert der Upstream 429/5xx, weicht der Router einmal auf den
konfigurierten Ausweich aus; die Log-Zeile trägt dann
auto/mass->auto/mass:fast, der Wechsel ist also sichtbar. Die Kette:
auto/mass → auto/mass:fast (ling) → auto/coding:junior
(glm-5.3-flash) — ling erfüllt die coding:junior-Schwellen strukturell nie,
der Rückfall landet also garantiert auf einem tauglichen Modell. Ohne das
hinge jede Titelzeile jedes Agenten, weil auto/mass überall als
small_model hängt. Wichtig: Die Ausweich-Auflösung läuft durch die
ACL-Prüfung des aufrufenden Keys — die Container-Personas brauchen die
Kettenziele deshalb in key-acls.json, sonst schlägt der Fallback still
fehl (403) und der ursprüngliche Fehler geht durch.
Wer läuft worauf
| Wer | Rolle | Ziel heute |
|---|---|---|
| agent-spark | role/engineer |
glm-5.3-flash |
| agent-buzz | role/ops |
glm-5.3-flash |
| agent-jessie | role/assistant |
glm-5.3-flash |
| agent-klara | role/service |
glm-5.3-flash |
| Leiter auf spark-desktop (xo, xo-chat, teamleiter, worker, kanon, thema, diagnose, logbuch) | role/xo, role/teamleader, role/worker, role/architect, role/archivist, role/ops |
glm-5.3-flash |
| Inka — Hirn | role/inka |
fest openai/gpt-5.6-luna |
| Inka — STT / TTS | 42i/marvin-stt-turbo / -tts-turbo |
Voxtral Small / Grok Voice |
latenztolerante Masse, small_model |
auto/mass |
lokales Qwen, 0 $ |
role/inka ist bewusst ein fester Modellname statt eines Profils: Die drei
Achsen messen keine Antwortlatenz, und am Telefon entscheidet genau die.
Gemessen am 2026-09-07 mit Inkas Zuschnitt (17–19k Token Systemprompt, vier
Züge): Luna 0,9–1,9 s bis zum ersten Wort, glm-5.3-flash 1,9–5,6 s. Der Cache
greift bei beiden; die Zeit geht bei GLM für 70–133 Denk-Token drauf, und
Reasoning lässt sich dort nicht abschalten ("Reasoning is mandatory for this endpoint"). Nachmessen: tools/telefon-latenz.py in diesem Repo.
Keys und Rechte
- Client-Token:
keys.json(Token → Name; im LXC/srv/llmrouter/keys.json, in der Docker-Demo./data/keys.json). Die Werte der Personas liegen in OpenBaoagents/<name>/litellmFeldkey; es sind dieselben Token wie bei LiteLLM. Noch ohne Token im Router:inka-mx,nightjob,lab-xo,team-copilot(LiteLLM kennt nur Hashes). Anlegen:KEYS_FILE=keys.json bun run src/keys.ts add <name> [--token <bestehend>]. - Rechte:
key-acls.json(Name → erlaubte Namen,*als Präfix-Muster), versioniert in diesem Repo. Elton und Andreas dürfenopenrouter/*undalt/*, die Agenten nur ihre Rollen und:medium. - Upstream-Keys in
.env(LXC:/srv/llmrouter/.env):OPENROUTER_API_KEY,MISTRAL_API_KEY,KIRA_REASONING_KEY,KIRA_STT_KEY,KIRA_TTS_KEY,ARTIFICIALANALYSIS_API_KEY,ADMIN_KEY. Quellen in OpenBao:agents/jessie/litellm(OpenRouter, AA, Kira) undagents/jessie/llmrouter(admin_key). - OpenRouter-Konto: ZDR-Guardrail bleibt bewusst (Andreas 05.09.2026). Alibaba- und Meta-gehostete Modelle sind dadurch nicht wählbar; der Router baut nur auf der Kontoliste auf und kann nichts wählen, was dagegen verstößt.
Log und Kosten
Je Anfrage eine Zeile: Key, angefragter Name, aufgelöster Alias, tatsächliches Modell, Effort, Tokens, Kosten wie von OpenRouter gemeldet. Lokale Ziele haben keine Kosten.
# Tagesstatistik je Key
curl -s "http://llm.lan:4010/stats?days=7" \
-H "Authorization: Bearer $(~/.agents/bin/secret get jessie llmrouter admin_key)" | python3 -m json.tool
# aktuelle Zuordnung aller Stufen und alt/-Referenzen
curl -s http://llm.lan:4010/zuordnung -H "Authorization: Bearer <ADMIN_KEY>" | python3 -m json.tool
# einzelne Anfragen / Modellwechsel / Journal
ssh s18p1.lan 'pct exec 185020 -- tail -20 /srv/llmrouter/log/requests.jsonl'
ssh s18p1.lan 'pct exec 185020 -- cat /srv/llmrouter/log/changes.log'
ssh s18p1.lan 'pct exec 185020 -- journalctl -u llmrouter -n 50 --no-pager -o cat'
Die Antwort trägt x-llmrouter-model, damit ein Client sieht, was wirklich
lief. Weitere Log-Wege (SQLite, Stats-Binary): referenz.md.
Wartung und Deploy
Quelle dieses Projekts ist dieses Repo (spine/llmrouter). Der Prod-Einsatz
läuft weiter als statisches Binary im LXC (Stand 2026-09-08); ein Umstieg auf
das Docker-Image ist offen und nicht dringend.
- Änderung in diesem Repo (Config, Code), committen, pushen. CI baut und
published danach
git.42i.org/spine/llmrouter:latest— für den Binary-Weg irrelevant, aber der Stand, den jede Demo zieht. - Binaries bauen:
bun run build(dazubun build --compile --target=bun-linux-x64 src/resolver.ts --outfile dist/llmrouter-resolve). - Ausrollen:
scpnachs18p1, dannpct push 185020 <datei> /srv/llmrouter/<datei>.neu, im LXCmv,chmod 755,systemctl restart llmrouter. - Nach Config-Änderungen an Stufen oder
alt/:rm /srv/llmrouter/log/state.jsonvor dem Neustart, sonst bleibt die alte Zuordnung. Der Erstlauf fährt für jede Stufe die Last-Probe (≈210k Token je Stufe). - Caddyfile-Falle: die Datei ist als Bind-Mount im Container; ein
mvtauscht den Inode undcaddy reloadsieht die alte Datei. Nach Änderungoci restart caddy. Quelle: caddy/Caddyfile.
Störung
| Symptom | Prüfen | Tun |
|---|---|---|
| Agenten bekommen 502/Timeout | curl http://llm.lan:4010/health; systemctl status llmrouter im LXC |
systemctl restart llmrouter; Journal lesen |
… ist noch nicht aufgelöst (503) |
/zuordnung leer oder alt |
POST /auffrischen mit Admin-Key, oder rm log/state.json + Neustart; braucht OpenRouter- und AA-Zugang |
| 401 für einen Agenten | Journal zeigt 401 … (Key n Zeichen) |
Token fehlt in keys.json → mit keys.ts add --token nachtragen |
403 darf … nicht rufen |
key-acls.json |
Recht ergänzen (versioniert in diesem Repo), Neustart |
| Zahlen im TTS falsch gesprochen | Sprechtext-Regel greift nur für PLZ/Nummern | Fish über OpenRouter zerlegt deutsche Beträge/Daten — deshalb Grok Voice bis Fish bei Uwe läuft (live/live#2247) |
llm.lan antwortet mit LiteLLM-Fehlern statt Router |
Caddy zeigt auf 4000 | Caddyfile prüfen, oci restart caddy |
Rückfall ohne Router: Caddyfile auf localhost:4000 stellen und Caddy neu
starten — LiteLLM bedient die 2608-Namen weiter, die 2609-, alt/- und
role/-Namen fehlen dann.
Zugehörige Werkzeuge
~/src/tools/openrouter-models.ts(git.homeandreas/tools): zeigt die Stufentabelle oder die Alternative zu einer Modell-ID (bun run ./openrouter-models.ts anthropic/claude-opus-5:medium).~/.agents/bin/opencode-normalize: erzeugt denllmlan-Provider in der opencode-Config aus/v1/modelsdes Routers, Effort-Stufen alsvariants.claude-lan(Funktion in~/.zshrc): Claude Code im Terminal über den Router; die Desktop-App kann das nicht (sie erzwingtapi.anthropic.com).- Auswahl-Regel: src/openrouter-auswahl.ts —
seit 2026-09-08 eine eigenständige Kopie (vendored aus
42i/agentsllmlite/, wo die llmlite-Fassung für den Tier-Updater weiterlebt).
Verlauf
- 2026-09-08: Umzug nach
spine/llmrouter(git.42i.org). Das komplette Projekt verließ das Repo42i/agents(Historie per subtree split bewahrt); dazu Docker-Image, Compose-Demo mit./data-Mount und Gitea-Actions-CI nach dem live-Muster. Die Referenz-Doku liegt im Repo (README.md,docs/), die Betriebsstand-Seite ininfobleibt als Zeiger. - 2026-09-08: Ausweich-Kette für die Massenarbeit (42i/agents 4e92f17,
42i/intern#1252). Anlass: ling-3.0-flash fiel am Vorabend aus (23:20–23:52,
5× 502 nach ~270 s hängendem Upstream, 117× 429).
auto/mass:fasthat jetzt selbst einen Ausweich aufauto/coding:junior, und der Ausweich greift auch bei 429/5xx-Antworten — vorher nur bei fetch-Fehlern und nur für fest-Ziele. ACLs der Container-Personas um die Kettenziele ergänzt. - 2026-09-08: Live-Probe reasoning-tolerant (42i/agents 312e43c). ling
denkt vor jedem Werkzeugaufruf mit variabler Länge; bei
max_tokens: 400scheiterte die Probe willkürlich und kipptemarvin-2609:lowundalt/claude-haiku-4.5auf teurere Modelle (solar-pro4 bzw. glm-5.3-flash), ohne dass ling defekt war. Budget auf 2000 Token, Timeout auf 90 s. - 2026-09-07: LiteLLM abgeschaltet, alles läuft über Rollen. Alle Agenten
(Container spark/buzz/jessie/klara, die Leiter auf spark-desktop, der XO) rufen
role/*statt eines Modellnamens; der XO und das Logbuch kamen dabei vom Claude-Max-Abo auf opencode. Derfallbackan LiteLLM ist aus der Config entfernt — unbekannte Namen sind jetzt 404 statt einer stillen Weiterleitung. Keine Rolle steht mehr auf der teuren Stufe (42i/intern#1252). - 2026-09-07: Caddy aus dem LiteLLM-Stack gelöst und eigener oci-Dienst
(
/srv/caddy, Quellellmrouter/caddy/, ausgerollt überagents-pull). Vorher hing er inllmlite/compose.yml— einpodman-compose downfür LiteLLM hätte die TLS-Terminierung vonllm.lanmitgenommen und den Router von außen unerreichbar gemacht, obwohl er läuft. Vorarbeit zur Abschaltung von LiteLLM (42i/intern#1252). - 2026-09-05: Router gebaut und in Betrieb,
llm.lanumgestellt; Stufen 25/45/49/53; Namensräumeopenrouter/,alt/,role/,deizo/; Claude-Code-Pfad. - 2026-09-06: Sprechtext-Normalisierung (Postleitzahl); TTS-Turbo auf Grok
Voice
ara, STT-Turbo auf Voxtral Small 24B nach Hör- und Telefontest; Auftrag an Uwe für Fish S2 Pro und Voxtral Small im Prod-RZ (live/live#2247).