# 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](referenz.md). ## Wo läuft was | Was | Wo | |---|---| | Router (`llmrouter`) | LXC **185020** (`llmlite`, `10.18.5.20`) auf `s18p1`, **OCI-Container** `llmrouter_llmrouter_1` (podman-compose, systemd-Unit `llmrouter`), Bild `git.42i.org/spine/llmrouter:latest`, 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/`: `compose.yml`, `.env` (0600); Laufzeit unter `data/`: `config.json`, `key-acls.json`, `keys.json` (0600), `model-blocklist.json`, `log/` (requests.jsonl, changes.log, state.json, aa-cache.json). Sidecar-Binaries daneben, werden nicht mehr gebaut (Ersatz: `podman run` aus dem Image, siehe Deploy) | | Docker-Image (Prod) | `git.42i.org/spine/llmrouter:latest`, gebaut von CI — seit 2026-09-09 läuft der Prod-Einsatz darauf | | 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":""}`; 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](referenz.md#namensräume). Kurzfassung: | Name | Bedeutung | |---|---| | `role/` | Rolle (`xo`, `architect`, `teamleader`, `worker`, `qa` …) — **Agenten kennen nur Rollen** | | `auto/[:stufe]` | Profile auf drei Achsen: `mass`, `mass:fast`, `allround`, `coding`, `leader` je `:junior`/`:senior` | | `42i/marvin-2609:low|medium|high|max` | konfigurierte Stufen (Index ≥ 25/45/49/53) | | `alt/[:effort]` | günstigste Alternative derselben Klasse; aufgelöst bei der ersten Anfrage, dann alle 12 h | | `openrouter/[: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/data/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, Output ≥ 32768 (je Profil senkbar, sonst 65536) | 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` → `auto/mass:fast`; `coder`, `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. `role/coder` existiert seit 2026-09-09 neben `engineer`, damit Code-Arbeit nicht über `role/worker` (Masse) läuft. **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/data/keys.json`, in der Docker-Demo `./data/keys.json`). Die Werte der Personas liegen in OpenBao `agents//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 [--token ]`. - 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. ```bash # 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 " | python3 -m json.tool # einzelne Anfragen / Modellwechsel / Journal ssh s18p1.lan 'pct exec 185020 -- tail -20 /srv/llmrouter/data/log/requests.jsonl' ssh s18p1.lan 'pct exec 185020 -- cat /srv/llmrouter/data/log/changes.log' ssh s18p1.lan 'pct exec 185020 -- podman logs -l -n 50' ``` Die Antwort trägt `x-llmrouter-model`, damit ein Client sieht, was wirklich lief. Weitere Log-Wege (SQLite, Stats-Binary): [referenz.md](referenz.md). ## Wartung und Deploy Quelle dieses Projekts ist dieses Repo (`spine/llmrouter`). Der Prod-Einsatz läuft seit dem **2026-09-09 als OCI-Container** im LXC (podman-compose hinter der Unit `llmrouter`, Bild `git.42i.org/spine/llmrouter:latest`, Laufzeitdaten unter `/srv/llmrouter/data/`). **Der alte Binary-Weg ist eingestellt** — Server-Updates nur noch über das CI-Image; die Sidecar-Binaries (`resolve`, `stats`, `keys`) werden nicht mehr gebaut, dafür läuft das Werkzeug aus dem Image (siehe Schritt 3). 1. Änderung in diesem Repo (Config, Code), committen, pushen. CI baut und published danach `git.42i.org/spine/llmrouter:latest`. 2. Im LXC Bild ziehen und Unit neu starten: ```sh ssh s18p1.lan 'pct exec 185020 -- podman pull git.42i.org/spine/llmrouter:latest' ssh s18p1.lan 'pct exec 185020 -- systemctl restart llmrouter' ``` Nach Config-Änderungen an Stufen oder `alt/`: `rm /srv/llmrouter/data/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). 3. Sidecar-Werkzeuge ohne Binary — Beispiel Tagesstatistik: ```sh ssh s18p1.lan "pct exec 185020 -- podman run --rm --env-file /srv/llmrouter/.env \ -e LLMROUTER_CONFIG=/data/config.json -e KEYS_FILE=/data/keys.json \ -v /srv/llmrouter/data:/data git.42i.org/spine/llmrouter:latest bun src/stats.ts 7" ``` (analog `src/keys.ts`, `src/resolver.ts`) 4. Rollback auf die Binary (Notfall): `podman-compose down` im `/srv/llmrouter`, dann die gesicherte Unit zurück (`cp /etc/systemd/system/llmrouter.service.binary-bak /etc/systemd/system/llmrouter.service`, `systemctl daemon-reload`, `systemctl enable --now llmrouter`) — die flachen Dateien und `log/` liegen unverändert daneben. 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](caddy/Caddyfile). ### Umstieg aufs Image (geprüft am 2026-09-08, vollzogen am 2026-09-09) Voraussetzungen alle erfüllt: podman 5.4.2 im LXC (kein Docker nötig — Muster wie `caddy.service`: podman-compose + systemd-oneshot), das Image ist öffentlich pullbar (anonym, wie `42i/caddy`), Multi-Arch enthält amd64. Layout wandert vorab ins Hausmuster: alles Laufzeitige unter `./data` (siehe [data/README.md](../data/README.md)), der Compose-Dienst mountet `./data:/data`. Einmalig migrieren — **Kopie, nicht Verschieben**, damit die Binary-Unit bis zum Umschalten lauffähig bleibt: ```sh cd /srv/llmrouter mkdir data cp config.json key-acls.json keys.json model-blocklist.json data/ cp -a log data/log # state.json + aa-cache.json wandern mit ``` `cp -a log` ist der entscheidende Schritt: `state.json` (Zuordnung) und `aa-cache.json` (AA-Index-Cache) liegen im logDir. Mit ihnen startet der Container sofort mit frischer Zuordnung; ohne sie gibt es eine Startlücke (~40 s bei `LLMROUTER_LASTTEST=0`, sonst ≈210k Token), in der Stufen mit 503 antworten („noch nicht aufgelöst"). ```yaml services: llmrouter: image: git.42i.org/spine/llmrouter:latest env_file: [.env] environment: # .env trägt die Host-Pfade — im Container auf /data übersteuern: LLMROUTER_CONFIG: /data/config.json LLMROUTER_ACLS: /data/key-acls.json KEYS_FILE: /data/keys.json MODEL_BLOCKLIST: /data/model-blocklist.json ports: ["127.0.0.1:4010:4010"] # Caddy zeigt weiter auf localhost:4010 volumes: ["./data:/data"] restart: unless-stopped ``` - `logDir` steht in `config.json` auf `./log` — relativ zur Config aufgelöst, im Container also `/data/log`, ohne Config-Änderung. - Sidecar-Binaries (`llmrouter-resolve`, `-stats`, `-keys`) bleiben auf dem LXC; nach dem Umstieg mit den data/-Pfaden aufrufen (`KEYS_FILE=/srv/llmrouter/data/keys.json` …). Die alten flachen Dateien (`config.json`, `keys.json`, die `.bak`-Kopien) können erst aufgeräumt werden, wenn der Umstieg hält. - Ablauf: compose-Datei + Unit (Muster caddy.service) anlegen → `podman-compose up -d` → `/health` prüfen (Zeitstempel aktuell) → `systemctl stop/disable llmrouter`. Rollback: Binary-Unit wieder aktivieren, Container stoppen. - Updates danach: `podman pull` + Container neu starten; CI published `:latest` (und `:sha-`) bei jedem Push auf main (paths-Filter — Doku-Commits bauen nicht). Prüfstand 2026-09-08 (Probelauf auf dem LXC, Produktion unberührt, Port 4011, Log-Dir nach /tmp ausgelagert): Image startet mit der Produktions-Config (25 Aliasse, 21 Rollen, 12 Token), `/models` **byte-identisch** zur Binary, Chat via `/chat/completions` 200 über beide Wege (`42i/marvin-2609:medium` → `z-ai/glm-5.3-flash`), komplette Zuordnungs-Auffrischung im Container funktioniert. Der Mount ist hostseitig — `./data:/data` ergibt dem Container gegenüber denselben `/data`-Inhalt wie der getestete Mount. Container und Image danach rückstandsfrei entfernt. ## Störung | Symptom | Prüfen | Tun | |---|---|---| | Agenten bekommen 502/Timeout | `curl http://llm.lan:4010/health`; `podman ps` im LXC | `systemctl restart llmrouter`; `podman logs -l` | | `… ist noch nicht aufgelöst (503)` | `/zuordnung` leer oder alt | `POST /auffrischen` mit Admin-Key, oder `rm /srv/llmrouter/data/log/state.json` + Neustart; braucht OpenRouter- und AA-Zugang | | 401 für einen Agenten | `podman logs -l` zeigt `401 … (Key n Zeichen)` | Token fehlt in `data/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](../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-09: **Output-Gate je Profil senkbar, neue Rolle `coder`.** Das 64K-Output-Gate (MIN_OUTPUT) trieb `auto/mass:fast` auf `solar-pro4` (0,42 $/1M), weil ling mit 32768 Output-Token durchfiel — gemessen 09.09.: 1.415 Masse-Requests, 1.146 davon unter 100 Completion-Token; das Gate verfehlte dort seinen Zweck und kostete ~35 % Aufschlag. Jetzt ist `minOutput` pro Profil in `config.json` setzbar (Default bleibt 65536), `auto/mass:fast` läuft mit 32768. Dazu `role/coder` → `auto/coding:junior` neben `engineer`, ACL für `persona-spark`, der engineer-Agent in der Mac-opencode.json von `role/worker` auf `role/coder` umgezogen. Deploy über CI-Image (Run #8) + `podman pull` + Neustart, Configs nach `data/` gesynct, `state.json` gelöscht und frisch aufgelöst: `auto/mass:fast` → ling-3.0-flash wiederhergestellt (0,27 $/1M, Referenz trägt `Output>=32768`), `role/coder` in `/v1/models`, Chat je Rolle 200. Seit dem Image-Betrieb antwortet Port 4010 nur auf `127.0.0.1` des LXC — von außen nur noch über Caddy (443/4000). - 2026-09-09: **Umstieg aufs Image vollzogen.** Prod läuft als OCI-Container (`llmrouter_llmrouter_1`, podman-compose hinter der Unit `llmrouter`, Bild `:latest` von Run #7/e144034). Laufzeitdaten wanderten nach `/srv/llmrouter/data/` (Hausmuster `./data:/data`), `state.json` und `aa-cache.json` mitkopiert — Zuordnung unverändert, keine Auffrischung. Verifiziert: `/health` lokal und über `https://llm.lan` 200, Chat 200. **Der Binary-Deploy-Weg (bun build, scp, pct push) ist eingestellt**; Updates nur noch über CI-Image + `podman pull` + Neustart. Unit-Rollback liegt als `llmrouter.service.binary-bak`. - 2026-09-08: **Umstieg aufs Image geprüft.** Probelauf des CI-Images auf dem LXC neben dem Binary (Port 4011): Konfig-/ACL-/Keys-Parität (`/models` byte-identisch), Chat 200 über beide Wege, Zuordnungs-Refresh läuft im Container. Rezept und Fallstricke stehen jetzt in [Wartung und Deploy](#wartung-und-deploy); Prod läuft weiter auf der Binary. Dazu Registry-Login-Fix in der CI (vertauschte Secret-Werte, 401er am Vormittag) und Login-Kommentar korrigiert. - 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:20–23: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).