Files
llmrouter/docs/betrieb.md
T
elton 000b244541 Doku: Einstiegs-README, technische Referenz, Betriebsstand
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.
2026-09-08 09:48:13 +02:00

14 KiB
Raw Blame History

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, leadauto/leader:junior; worker, engineer, qa, perfauto/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/massauto/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 (1719k Token Systemprompt, vier Züge): Luna 0,91,9 s bis zum ersten Wort, glm-5.3-flash 1,95,6 s. Der Cache greift bei beiden; die Zeit geht bei GLM für 70133 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 OpenBao agents/<name>/litellm Feld key; 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ürfen openrouter/* und alt/*, 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) und agents/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.

  1. Ä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.
  2. Binaries bauen: bun run build (dazu bun build --compile --target=bun-linux-x64 src/resolver.ts --outfile dist/llmrouter-resolve).
  3. Ausrollen: scp nach s18p1, dann pct push 185020 <datei> /srv/llmrouter/<datei>.neu, im LXC mv, chmod 755, systemctl restart llmrouter.
  4. Nach Config-Änderungen an Stufen oder alt/: rm /srv/llmrouter/log/state.json vor dem Neustart, sonst bleibt die alte Zuordnung. Der Erstlauf fährt für jede Stufe die Last-Probe (≈210k Token je Stufe).
  5. Caddyfile-Falle: die Datei ist als Bind-Mount im Container; ein mv tauscht den Inode und caddy reload sieht die alte Datei. Nach Änderung oci 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.home andreas/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 den llmlan-Provider in der opencode-Config aus /v1/models des Routers, Effort-Stufen als variants.
  • claude-lan (Funktion in ~/.zshrc): Claude Code im Terminal über den Router; die Desktop-App kann das nicht (sie erzwingt api.anthropic.com).
  • Auswahl-Regel: src/openrouter-auswahl.ts — seit 2026-09-08 eine eigenständige Kopie (vendored aus 42i/agents llmlite/, 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 Repo 42i/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 in info bleibt 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:2023:52, 5× 502 nach ~270 s hängendem Upstream, 117× 429). auto/mass:fast hat jetzt selbst einen Ausweich auf auto/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: 400 scheiterte die Probe willkürlich und kippte marvin-2609:low und alt/claude-haiku-4.5 auf 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. Der fallback an 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, Quelle llmrouter/caddy/, ausgerollt über agents-pull). Vorher hing er in llmlite/compose.yml — ein podman-compose down für LiteLLM hätte die TLS-Terminierung von llm.lan mitgenommen 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.lan umgestellt; Stufen 25/45/49/53; Namensräume openrouter/, 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).