Compare commits

Author SHA1 Message Date
elton 789d8c187e betrieb.md: Deploy-Verlauf des Output-Gates ergaenzt 2026-09-09 17:13:14 +02:00
elton 7c6e747145 minOutput je Profil senkbar, neue Rolle coder
llmrouter CI / bun test + build (push) Successful in 4s
llmrouter CI / Build and publish image (push) Successful in 21s
Das 64K-Output-Gate war global und trieb auto/mass:fast auf solar-pro4
(0,42 statt 0,27 $/1M), weil ling-3.0-flash 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. Jetzt kann je
Schwelle-Profil minOutput gesetzt werden (Default 65536);
auto/mass:fast laeuft mit 32768, max_output unbekannt bleibt abgelehnt.

Dazu role/coder -> auto/coding:junior (ACL fuer persona-spark), damit
Code-Arbeit nicht ueber role/worker in die Masse faellt; der
engineer-Agent der Mac-opencode.json wechselt auf role/coder.

Refs #1283
2026-09-09 17:03:51 +02:00
elton d509d975e4 Betrieb: Umstieg aufs Image vollzogen, Binary-Weg eingestellt 2026-09-09 07:41:21 +02:00
elton e14403420d ACL: turbo-STT/TTS fuer die Container-Personas freigeben (Agenten-Sprachkanal auf Voxtral/Grok umgestellt)
llmrouter CI / bun test + build (push) Successful in 4s
llmrouter CI / Build and publish image (push) Successful in 19s
2026-09-09 06:58:15 +02:00
8 changed files with 137 additions and 40 deletions
+3 -1
View File
@@ -167,8 +167,9 @@
"art": "schwelle", "art": "schwelle",
"index": 25, "index": 25,
"minContext": 256000, "minContext": 256000,
"minOutput": 32768,
"ausweich": "auto/coding:junior", "ausweich": "auto/coding:junior",
"_doku": "Wie auto/mass, aber wenn es schnell gehen muss: billigstes Cloud-Modell ueber der Mindestschwelle. Andreas 2026-09-06: 'bei masse ist halt: brauchst du es schnell, nimm ling, ist geschwindigkeit nicht wichtig, nimm core'. Ausweich auf auto/coding:junior seit 2026-09-08 (Andreas): ling hat nachts Kapazitaets- und Ratengrenzprobleme (2026-09-07 23:20-23:52: 5x 502 nach ~270 s haengendem Upstream, 117x 429), und auto/mass:fast war als Endstation ohne Rueckfall. coding:junior erfuellt ling strukturell nie (Coding 50,6 / Agentic 21,1 gegen Coding>=68 / Agentic>=45) -- der Ausweich landet also garantiert auf einem tauglichen Modell, nicht auf ling selbst." "_doku": "Wie auto/mass, aber wenn es schnell gehen muss: billigstes Cloud-Modell ueber der Mindestschwelle. Andreas 2026-09-06: 'bei masse ist halt: brauchst du es schnell, nimm ling, ist geschwindigkeit nicht wichtig, nimm core'. Ausweich auf auto/coding:junior seit 2026-09-08 (Andreas): ling hat nachts Kapazitaets- und Ratengrenzprobleme (2026-09-07 23:20-23:52: 5x 502 nach ~270 s haengendem Upstream, 117x 429), und auto/mass:fast war als Endstation ohne Rueckfall. coding:junior erfuellt ling strukturell nie (Coding 50,6 / Agentic 21,1 gegen Coding>=68 / Agentic>=45) -- der Ausweich landet also garantiert auf einem tauglichen Modell, nicht auf ling selbst. minOutput 32768 seit 2026-09-09 (Andreas): Masse schreibt fast nur kleine Completion-Haeufchen (gemessen 09.09.: 1.146 von 1.415 Requests unter 100 Token), das 64K-Output-Gate trieb sie sonst unnoetig auf solar-pro4; Arbeitsmodelle behalten 65536."
}, },
"auto/allround:junior": { "auto/allround:junior": {
"art": "schwelle", "art": "schwelle",
@@ -221,6 +222,7 @@
"lead": "auto/leader:junior", "lead": "auto/leader:junior",
"leader": "auto/leader:junior", "leader": "auto/leader:junior",
"worker": "auto/mass:fast", "worker": "auto/mass:fast",
"coder": "auto/coding:junior",
"engineer": "auto/coding:junior", "engineer": "auto/coding:junior",
"po": "auto/allround:junior", "po": "auto/allround:junior",
"speaker": "auto/allround:junior", "speaker": "auto/allround:junior",
+68 -25
View File
@@ -18,12 +18,12 @@ keine Format-Übersetzung — alle Upstreams sprechen OpenAI-Format.
| Was | Wo | | Was | Wo |
|---|---| |---|---|
| Router (`llmrouter`) | LXC **185020** (`llmlite`, `10.18.5.20`) auf `s18p1`, systemd-Unit `llmrouter`, Port **4010** | | 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 | | 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` | | 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` | | 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/` | | 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 (Demo/Basis) | `git.42i.org/spine/llmrouter:latest`, gebaut von CI; `docker compose up` mit `./data`-Mount als Demo | | 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` | | 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) | | 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 | | Kira im Prod-RZ (deizo) | `10.6.42.111:8000/8001/8002` über den WireGuard-Peer des LXC |
@@ -47,7 +47,7 @@ Claude-Code-Pfad) steht in [referenz.md](referenz.md#namensräume). Kurzfassung:
| alles andere | **404** — kein stiller Fallback mehr seit der LiteLLM-Abschaltung | | alles andere | **404** — kein stiller Fallback mehr seit der LiteLLM-Abschaltung |
Ein Modellwechsel steht in `log/changes.log` (im LXC: Ein Modellwechsel steht in `log/changes.log` (im LXC:
`/srv/llmrouter/log/changes.log`). `/srv/llmrouter/data/log/changes.log`).
## Rollen und Profile — drei Ebenen ## Rollen und Profile — drei Ebenen
@@ -59,7 +59,7 @@ ohne Deploy, ohne Container anzufassen (42i/intern#1252).
| Profil | Regel | trifft am 2026-09-07 | $/1M gew. | | Profil | Regel | trifft am 2026-09-07 | $/1M gew. |
|---|---|---|---| |---|---|---|---|
| `auto/mass` | fest: lokales Qwen, Ausweich auf `mass:fast` | qwen36:35b | 0 | | `auto/mass` | fest: lokales Qwen, Ausweich auf `mass:fast` | qwen36:35b | 0 |
| `auto/mass:fast` | Index ≥ 25 | ling-3.0-flash | 0,27 | | `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:junior` | Index ≥ 40 | glm-5.3-flash | 0,95 |
| `auto/allround:senior` | Index ≥ 49 | grok-4.6:high | 26,00 | | `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:junior` | Coding ≥ 68, Agentic ≥ 45 | glm-5.3-flash | 0,95 |
@@ -72,10 +72,13 @@ Agentic **41,2** — es schreibt guten Text und trägt den Werkzeug-Rundlauf nic
Ein einzelner Index sieht diesen Unterschied strukturell nicht. Ein einzelner Index sieht diesen Unterschied strukturell nicht.
**Rollenbelegung:** `xo`, `teamleader`, `lead``auto/leader:junior`; **Rollenbelegung:** `xo`, `teamleader`, `lead``auto/leader:junior`;
`worker`, `engineer`, `qa`, `perf``auto/coding:junior`; alle übrigen `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`. `auto/allround:junior`; `inka` fest auf `openrouter/openai/gpt-5.6-luna`.
**Keine Rolle steht auf einer Senior-Stufe** (Andreas, 2026-09-07) — hochgestuft **Keine Rolle steht auf einer Senior-Stufe** (Andreas, 2026-09-07) — hochgestuft
wird einzeln und mit Anlass, nicht vorsorglich. 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 **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` (lokales Qwen) oder Schwelle auf einem Ausfall-Modell wie
@@ -116,7 +119,7 @@ endpoint"`). Nachmessen: `tools/telefon-latenz.py` in diesem Repo.
## Keys und Rechte ## Keys und Rechte
- Client-Token: `keys.json` (Token → Name; im LXC - Client-Token: `keys.json` (Token → Name; im LXC
`/srv/llmrouter/keys.json`, in der Docker-Demo `./data/keys.json`). Die `/srv/llmrouter/data/keys.json`, in der Docker-Demo `./data/keys.json`). Die
Werte der Personas liegen in OpenBao `agents/<name>/litellm` Feld `key`; es Werte der Personas liegen in OpenBao `agents/<name>/litellm` Feld `key`; es
sind **dieselben Token wie bei LiteLLM**. Noch ohne Token im Router: sind **dieselben Token wie bei LiteLLM**. Noch ohne Token im Router:
`inka-mx`, `nightjob`, `lab-xo`, `team-copilot` (LiteLLM kennt nur Hashes). `inka-mx`, `nightjob`, `lab-xo`, `team-copilot` (LiteLLM kennt nur Hashes).
@@ -147,9 +150,9 @@ curl -s "http://llm.lan:4010/stats?days=7" \
# aktuelle Zuordnung aller Stufen und alt/-Referenzen # aktuelle Zuordnung aller Stufen und alt/-Referenzen
curl -s http://llm.lan:4010/zuordnung -H "Authorization: Bearer <ADMIN_KEY>" | python3 -m json.tool curl -s http://llm.lan:4010/zuordnung -H "Authorization: Bearer <ADMIN_KEY>" | python3 -m json.tool
# einzelne Anfragen / Modellwechsel / Journal # einzelne Anfragen / Modellwechsel / Journal
ssh s18p1.lan 'pct exec 185020 -- tail -20 /srv/llmrouter/log/requests.jsonl' ssh s18p1.lan 'pct exec 185020 -- tail -20 /srv/llmrouter/data/log/requests.jsonl'
ssh s18p1.lan 'pct exec 185020 -- cat /srv/llmrouter/log/changes.log' ssh s18p1.lan 'pct exec 185020 -- cat /srv/llmrouter/data/log/changes.log'
ssh s18p1.lan 'pct exec 185020 -- journalctl -u llmrouter -n 50 --no-pager -o cat' ssh s18p1.lan 'pct exec 185020 -- podman logs -l -n 50'
``` ```
Die Antwort trägt `x-llmrouter-model`, damit ein Client sieht, was wirklich Die Antwort trägt `x-llmrouter-model`, damit ein Client sieht, was wirklich
@@ -158,25 +161,40 @@ lief. Weitere Log-Wege (SQLite, Stats-Binary): [referenz.md](referenz.md).
## Wartung und Deploy ## Wartung und Deploy
Quelle dieses Projekts ist dieses Repo (`spine/llmrouter`). Der Prod-Einsatz Quelle dieses Projekts ist dieses Repo (`spine/llmrouter`). Der Prod-Einsatz
läuft weiter als statisches Binary im LXC (Stand 2026-09-08); der Umstieg aufs läuft seit dem **2026-09-09 als OCI-Container** im LXC (podman-compose hinter
Image ist **geprüft und machbar**, das Rezept steht unten — vollzogen ist er der Unit `llmrouter`, Bild `git.42i.org/spine/llmrouter:latest`, Laufzeitdaten
noch nicht. 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 1. Änderung in diesem Repo (Config, Code), committen, pushen. CI baut und
published danach `git.42i.org/spine/llmrouter:latest` — für den published danach `git.42i.org/spine/llmrouter:latest`.
Binary-Weg irrelevant, aber der Stand, den jede Demo zieht. 2. Im LXC Bild ziehen und Unit neu starten:
2. Binaries bauen: `bun run build` (dazu ```sh
`bun build --compile --target=bun-linux-x64 src/resolver.ts --outfile dist/llmrouter-resolve`). ssh s18p1.lan 'pct exec 185020 -- podman pull git.42i.org/spine/llmrouter:latest'
3. Ausrollen: `scp` nach `s18p1`, dann `pct push 185020 <datei> /srv/llmrouter/<datei>.neu`, ssh s18p1.lan 'pct exec 185020 -- systemctl restart llmrouter'
im LXC `mv`, `chmod 755`, `systemctl restart llmrouter`. ```
4. Nach Config-Änderungen an Stufen oder `alt/`: `rm /srv/llmrouter/log/state.json` 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 vor dem Neustart, sonst bleibt die alte Zuordnung. Der Erstlauf fährt für jede
Stufe die Last-Probe (≈210k Token je Stufe). 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` 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 tauscht den Inode und `caddy reload` sieht die alte Datei. Nach Änderung
`oci restart caddy`. Quelle: [caddy/Caddyfile](caddy/Caddyfile). `oci restart caddy`. Quelle: [caddy/Caddyfile](caddy/Caddyfile).
### Umstieg aufs Image (geprüft am 2026-09-08, ausstehend) ### 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 — 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 Muster wie `caddy.service`: podman-compose + systemd-oneshot), das Image ist
@@ -243,9 +261,9 @@ Image danach rückstandsfrei entfernt.
| Symptom | Prüfen | Tun | | Symptom | Prüfen | Tun |
|---|---|---| |---|---|---|
| Agenten bekommen 502/Timeout | `curl http://llm.lan:4010/health`; `systemctl status llmrouter` im LXC | `systemctl restart llmrouter`; Journal lesen | | 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 log/state.json` + Neustart; braucht OpenRouter- und AA-Zugang | | `… 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 | Journal zeigt `401 … (Key n Zeichen)` | Token fehlt in `keys.json` → mit `keys.ts add --token` nachtragen | | 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 | | 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) | | 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` | | `llm.lan` antwortet mit LiteLLM-Fehlern statt Router | Caddy zeigt auf 4000 | Caddyfile prüfen, `oci restart caddy` |
@@ -268,6 +286,31 @@ starten — LiteLLM bedient die `2608`-Namen weiter, die `2609`-, `alt/`- und
## Verlauf ## 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 - 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 dem LXC neben dem Binary (Port 4011): Konfig-/ACL-/Keys-Parität
(`/models` byte-identisch), Chat 200 über beide Wege, Zuordnungs-Refresh (`/models` byte-identisch), Chat 200 über beide Wege, Zuordnungs-Refresh
+8 -3
View File
@@ -20,7 +20,7 @@ reicht Durchreichen. Entscheidung Andreas, 2026-09-05.
| `deizo/kira-reasoning`, `deizo/kira-stt` | Modelle im Prod-RZ deizo, fest | | `deizo/kira-reasoning`, `deizo/kira-stt` | Modelle im Prod-RZ deizo, fest |
| `openrouter/<id>[:effort]` | 1:1 an OpenRouter, Effort als `reasoning.effort` | | `openrouter/<id>[:effort]` | 1:1 an OpenRouter, Effort als `reasoning.effort` |
| `alt/<id>[:effort]` | **günstigste Alternative derselben Klasse** wie `<id>` auf dieser Stufe | | `alt/<id>[:effort]` | **günstigste Alternative derselben Klasse** wie `<id>` auf dieser Stufe |
| `auto/<profil>[:stufe]` | **Profil auf drei Achsen** (`mass`, `mass:fast`, `allround`, `coding`, `leader` je `:junior`/`:senior`) — billigstes Modell über den geforderten Untergrenzen | | `auto/<profil>[:stufe]` | **Profil auf drei Achsen** (`mass`, `mass:fast`, `allround`, `coding`, `leader` je `:junior`/`:senior`) — billigstes Modell über den geforderten Untergrenzen; `minOutput` senkt das 64K-Output-Gate je Profil |
| `role/<name>` | Rolle aus `rollen`, zeigt auf einen der obigen Namen | | `role/<name>` | Rolle aus `rollen`, zeigt auf einen der obigen Namen |
| `claude-*` (nackte Anthropic-ID) | Claude Code/Desktop: wird zu `alt/anthropic/<id>[:effort]` (siehe unten) | | `claude-*` (nackte Anthropic-ID) | Claude Code/Desktop: wird zu `alt/anthropic/<id>[:effort]` (siehe unten) |
| alles andere | **404** — unbekannte Namen sind ein Fehler, keine stille Weiterleitung (die LiteLLM-Durchreiche ist mit deren Abschaltung 2026-09-07 entfallen) | | alles andere | **404** — unbekannte Namen sind ein Fehler, keine stille Weiterleitung (die LiteLLM-Durchreiche ist mit deren Abschaltung 2026-09-07 entfallen) |
@@ -45,12 +45,17 @@ nur auf bis zu drei Achsen statt einer: `index` (Intelligence), `coding` und
keinen Werkzeug-Rundlauf. `auto/mass` ist der Sonderfall: fest auf dem lokalen keinen Werkzeug-Rundlauf. `auto/mass` ist der Sonderfall: fest auf dem lokalen
Qwen (kostet nichts) und mit `ausweich` auf `auto/mass:fast`, falls der Knoten Qwen (kostet nichts) und mit `ausweich` auf `auto/mass:fast`, falls der Knoten
steht — ohne den hinge sonst jede Titelzeile jedes Agenten, weil `auto/mass` steht — ohne den hinge sonst jede Titelzeile jedes Agenten, weil `auto/mass`
überall als `small_model` hängt. überall als `small_model` hängt. Über die Achsen hinaus kann ein Profil das
64K-Output-Gate je Stufe senken: `minOutput` (Default 65536, Andreas
2026-09-09). `auto/mass:fast` läuft mit 32768 — Masse-Arbeit schreibt fast nur
kleine Completion-Häufchen, das Gate trieb sie sonst un nötig auf
`upstage/solar-pro4`; `max_output` unbekannt bleibt abgelehnt.
**Rollen** entkoppeln Agenten von Modellnamen: ein Agent ruft `role/worker`, **Rollen** entkoppeln Agenten von Modellnamen: ein Agent ruft `role/worker`,
welches Modell dahintersteht, stellt man in `config.json` um, ohne Container. welches Modell dahintersteht, stellt man in `config.json` um, ohne Container.
Seit 2026-09-07 zeigen alle Rollen auf `auto/`-Profile: xo/teamleader/lead → Seit 2026-09-07 zeigen alle Rollen auf `auto/`-Profile: xo/teamleader/lead →
`auto/leader:junior`, worker/engineer/qa/perf → `auto/coding:junior`, der Rest `auto/leader:junior`, coder/engineer/qa/perf → `auto/coding:junior`, worker
`auto/mass:fast`, der Rest →
`auto/allround:junior`, `inka` fest auf Luna. Keine Rolle steht auf einer `auto/allround:junior`, `inka` fest auf Luna. Keine Rolle steht auf einer
Senior-Stufe (Andreas: hochstufen einzeln und mit Anlass). Senior-Stufe (Andreas: hochstufen einzeln und mit Anlass).
+10 -1
View File
@@ -1,17 +1,20 @@
{ {
"_note": "Rechte je Client-Key: Name -> erlaubte Aliasse (neue Namen; Altnamen werden vor der Pruefung uebersetzt). [] = alles. Die Token selbst liegen NICHT hier, sondern in KEYS_FILE auf dem Host (token -> name). Abgeleitet aus llmlite/key-acls.json am 2026-09-05: role/* und -auto gestrichen (Andreas), -auto -> :medium. Stand 2026-09-05 abends: Stufen low/medium/high/max; none->low, extra->high, ultra->max, mass->low. Namen ab 2026-09-05: 42i/marvin-2609:*; die Rechte gelten nur fuer Router-Aliasse, unbekannte Namen (2608) reicht der Router an LiteLLM durch, dort gelten dessen ACLs. Muster mit * erlaubt (openrouter/*, alt/*, role/*). Rollen aus der LiteLLM-Zeit je Key wieder eingetragen; Elton und Andreas duerfen openrouter/* und alt/*. 2026-09-07: core:none fuer alle Container-Personas, weil opencodes small_model (Titel, Zusammenfassungen) dorthin zeigt -- nach 42i/intern#1252 wird daraus auto/mass. 2026-09-07 aufgeraeumt: 'nightjob' (kein Token, kein Konsument, keine Nutzung) und 'persona-inka-mx' entfernt -- beide Inka-Tokens (Shim und Telefon-Front-End) laufen im Router unter 'persona-inka', der mx-Name existierte nur noch als LiteLLM-key_alias. 2026-09-08: auto/mass:fast und auto/coding:junior fuer die Container-Personas -- die Ausweich-Kette auto/mass -> auto/mass:fast -> auto/coding:junior laeuft durch die ACL-Pruefung des aufrufenden Keys; ohne die Ziele dort schlaegt der Fallback still fehl (403 in zielBestimmen) und der urspruengliche Fehler geht durch.", "_note": "Rechte je Client-Key: Name -> erlaubte Aliasse (neue Namen; Altnamen werden vor der Pruefung uebersetzt). [] = alles. Die Token selbst liegen NICHT hier, sondern in KEYS_FILE auf dem Host (token -> name). Abgeleitet aus llmlite/key-acls.json am 2026-09-05: role/* und -auto gestrichen (Andreas), -auto -> :medium. Stand 2026-09-05 abends: Stufen low/medium/high/max; none->low, extra->high, ultra->max, mass->low. Namen ab 2026-09-05: 42i/marvin-2609:*; die Rechte gelten nur fuer Router-Aliasse, unbekannte Namen (2608) reicht der Router an LiteLLM durch, dort gelten dessen ACLs. Muster mit * erlaubt (openrouter/*, alt/*, role/*). Rollen aus der LiteLLM-Zeit je Key wieder eingetragen; Elton und Andreas duerfen openrouter/* und alt/*. 2026-09-07: core:none fuer alle Container-Personas, weil opencodes small_model (Titel, Zusammenfassungen) dorthin zeigt -- nach 42i/intern#1252 wird daraus auto/mass. 2026-09-07 aufgeraeumt: 'nightjob' (kein Token, kein Konsument, keine Nutzung) und 'persona-inka-mx' entfernt -- beide Inka-Tokens (Shim und Telefon-Front-End) laufen im Router unter 'persona-inka', der mx-Name existierte nur noch als LiteLLM-key_alias. 2026-09-08: auto/mass:fast und auto/coding:junior fuer die Container-Personas -- die Ausweich-Kette auto/mass -> auto/mass:fast -> auto/coding:junior laeuft durch die ACL-Pruefung des aufrufenden Keys; ohne die Ziele dort schlaegt der Fallback still fehl (403 in zielBestimmen) und der urspruengliche Fehler geht durch. 2026-09-09: 42i/marvin-stt-turbo und 42i/marvin-tts-turbo fuer die vier Container-Personas (Andreas: 'schaltet TTS und STT grundsaetzlich auf die externen Modelle um, die wir auch fuer Inka verwenden') -- die Agenten-Sprachkanaelle laufen damit ueber Voxtral Small bzw. Grok Voice statt lokalem whisper.cpp/Qwen3-TTS; die lokalen marvin-stt/marvin-tts bleiben als Rueckweg erlaubt. 2026-09-09: role/coder fuer persona-spark (neue Rolle, zeigt wie engineer auf auto/coding:junior).",
"keys": { "keys": {
"persona-spark": [ "persona-spark": [
"42i/marvin-2609-core:none", "42i/marvin-2609-core:none",
"42i/marvin-2609:medium", "42i/marvin-2609:medium",
"42i/marvin-stt", "42i/marvin-stt",
"42i/marvin-tts", "42i/marvin-tts",
"42i/marvin-stt-turbo",
"42i/marvin-tts-turbo",
"auto/coding:junior", "auto/coding:junior",
"auto/mass:fast", "auto/mass:fast",
"deizo/kira-reasoning", "deizo/kira-reasoning",
"deizo/kira-stt", "deizo/kira-stt",
"deizo/kira-tts", "deizo/kira-tts",
"role/engineer", "role/engineer",
"role/coder",
"role/worker" "role/worker"
], ],
"persona-buzz": [ "persona-buzz": [
@@ -19,6 +22,8 @@
"42i/marvin-2609:medium", "42i/marvin-2609:medium",
"42i/marvin-stt", "42i/marvin-stt",
"42i/marvin-tts", "42i/marvin-tts",
"42i/marvin-stt-turbo",
"42i/marvin-tts-turbo",
"auto/coding:junior", "auto/coding:junior",
"auto/mass:fast", "auto/mass:fast",
"deizo/kira-reasoning", "deizo/kira-reasoning",
@@ -33,6 +38,8 @@
"42i/marvin-2609:medium", "42i/marvin-2609:medium",
"42i/marvin-stt", "42i/marvin-stt",
"42i/marvin-tts", "42i/marvin-tts",
"42i/marvin-stt-turbo",
"42i/marvin-tts-turbo",
"auto/mass:fast", "auto/mass:fast",
"auto/coding:junior", "auto/coding:junior",
"deizo/kira-reasoning", "deizo/kira-reasoning",
@@ -46,6 +53,8 @@
"42i/marvin-2609:medium", "42i/marvin-2609:medium",
"42i/marvin-stt", "42i/marvin-stt",
"42i/marvin-tts", "42i/marvin-tts",
"42i/marvin-stt-turbo",
"42i/marvin-tts-turbo",
"auto/mass:fast", "auto/mass:fast",
"deizo/kira-reasoning", "deizo/kira-reasoning",
"deizo/kira-stt", "deizo/kira-stt",
+5 -3
View File
@@ -13,9 +13,11 @@ import { dirname, resolve } from "path";
export type Upstream = { base: string; keyEnv?: string; maxParallel?: number; /** Client-Token unveraendert weitergeben (Durchreiche an LiteLLM) */ clientKey?: boolean }; export type Upstream = { base: string; keyEnv?: string; maxParallel?: number; /** Client-Token unveraendert weitergeben (Durchreiche an LiteLLM) */ clientKey?: boolean };
export type Alias = export type Alias =
| { art: "alternative"; referenz: string } | { art: "alternative"; referenz: string }
// Schwelle auf bis zu drei Achsen (42i/intern#1252): index = Intelligence, // Schwelle auf bis zu vier Achsen (42i/intern#1252): index = Intelligence,
// coding und agentic je eigene Untergrenze. Fehlt eine, gilt sie nicht. // coding und agentic je eigene Untergrenze, minOutput als Overlay auf das
| { art: "schwelle"; index?: number; coding?: number; agentic?: number; minContext: number; ausweich?: string } // 64K-Output-Gate (Andreas 2026-09-09: mass-Arbeit darf mit 32768 laufen,
// Default bleibt 65536). Fehlt eine, gilt sie nicht.
| { art: "schwelle"; index?: number; coding?: number; agentic?: number; minContext: number; minOutput?: number; ausweich?: string }
// ausweich: Alias, auf den bei Upstream-Ausfall gewechselt wird. Fuer "fest" // ausweich: Alias, auf den bei Upstream-Ausfall gewechselt wird. Fuer "fest"
// und "schwelle" -- ein Ziel ohne Rueckfall haengt sonst jede Arbeit, die // und "schwelle" -- ein Ziel ohne Rueckfall haengt sonst jede Arbeit, die
// darauf zeigt: festes Ziel = lokales Qwen, schwelle = Billigmodell mit // darauf zeigt: festes Ziel = lokales Qwen, schwelle = Billigmodell mit
+14 -6
View File
@@ -32,7 +32,14 @@ import { spawnSync } from "child_process";
export const API = process.env.OPENROUTER_API_URL ?? "https://openrouter.ai/api/v1"; export const API = process.env.OPENROUTER_API_URL ?? "https://openrouter.ai/api/v1";
export const MIN_CONTEXT = 256_000; export const MIN_CONTEXT = 256_000;
export const MIN_OUTPUT = 65536; // Andreas 2026-09-08, ticket 42i/intern#1283 — work models need at least 64K output tokens; inclusionai/ling-3.0-flash is hard-capped at 32768 /**
* Default des Output-Gates, je Schwelle-Profil ueber `minOutput` uebersteuerbar
* (Andreas 2026-09-09: auto/mass:fast laeuft mit 32768, weil Masse-Arbeit
* kleine Completion-Tokenhaeufchen schreibt; Arbeitsmodelle behalten 65536).
* Anlass 2026-09-08, ticket 42i/intern#1283: inclusionai/ling-3.0-flash ist
* hart bei 32768 gedeckelt.
*/
export const MIN_OUTPUT = 65536;
export const MAX_LATENCY_MS = 3_000; export const MAX_LATENCY_MS = 3_000;
export const INPUT_WEIGHT = 10; // Agenten-Lastprofil: Input dominiert export const INPUT_WEIGHT = 10; // Agenten-Lastprofil: Input dominiert
export const OUTPUT_WEIGHT = 1; export const OUTPUT_WEIGHT = 1;
@@ -558,9 +565,9 @@ export async function billigsterUeberSchwelle(
// und taugt damit fuer Text, nicht fuer den Werkzeug-Rundlauf. Wer nur eine // und taugt damit fuer Text, nicht fuer den Werkzeug-Rundlauf. Wer nur eine
// der Achsen fordert, uebergibt fuer die anderen keinen Wert; minIndex 0 // der Achsen fordert, uebergibt fuer die anderen keinen Wert; minIndex 0
// heisst "auf dieser Achse keine Anforderung". // heisst "auf dieser Achse keine Anforderung".
opts?: { lasttest?: boolean; varianten?: Variante[]; bestand?: string; minCoding?: number; minAgentic?: number }, opts?: { lasttest?: boolean; varianten?: Variante[]; bestand?: string; minCoding?: number; minAgentic?: number; minOutput?: number },
): Promise<{ auswahl: Auswahl | null; infrage: Candidate[] }> { ): Promise<{ auswahl: Auswahl | null; infrage: Candidate[] }> {
const infrage = klasseFiltern(alle, { minIndex: schwelle, minContext, minCoding: opts?.minCoding, minAgentic: opts?.minAgentic }, opts?.varianten ?? [], gesperrt, abgelehnt); const infrage = klasseFiltern(alle, { minIndex: schwelle, minContext, minCoding: opts?.minCoding, minAgentic: opts?.minAgentic, minOutput: opts?.minOutput }, opts?.varianten ?? [], gesperrt, abgelehnt);
const auswahl = await erstesTaugliches(infrage, key, abgelehnt, opts); const auswahl = await erstesTaugliches(infrage, key, abgelehnt, opts);
return { auswahl, infrage }; return { auswahl, infrage };
} }
@@ -575,19 +582,20 @@ export async function billigsterUeberSchwelle(
*/ */
function klasseFiltern( function klasseFiltern(
alle: Map<string, Candidate>, alle: Map<string, Candidate>,
regel: { minIndex: number; minCoding?: number; minAgentic?: number; minContext: number; maxWeighted?: number; ausser?: string }, regel: { minIndex: number; minCoding?: number; minAgentic?: number; minContext: number; minOutput?: number; maxWeighted?: number; ausser?: string },
varianten: Variante[], varianten: Variante[],
gesperrt: Map<string, string>, gesperrt: Map<string, string>,
ablehnen?: (id: string, grund: string) => void, ablehnen?: (id: string, grund: string) => void,
): Candidate[] { ): Candidate[] {
const minOutput = regel.minOutput ?? MIN_OUTPUT;
const infrage: Candidate[] = []; const infrage: Candidate[] = [];
for (const k of alle.values()) { for (const k of alle.values()) {
if (k.id === regel.ausser || !k.tauglich || gesperrt.has(k.id)) continue; if (k.id === regel.ausser || !k.tauglich || gesperrt.has(k.id)) continue;
if (k.context < regel.minContext) continue; if (k.context < regel.minContext) continue;
if (regel.maxWeighted !== undefined && k.weighted >= regel.maxWeighted) continue; if (regel.maxWeighted !== undefined && k.weighted >= regel.maxWeighted) continue;
if (regel.minAgentic !== undefined && !(k.agentic >= regel.minAgentic)) continue; if (regel.minAgentic !== undefined && !(k.agentic >= regel.minAgentic)) continue;
if (k.maxOutput > 0 && k.maxOutput < MIN_OUTPUT) { if (k.maxOutput > 0 && k.maxOutput < minOutput) {
ablehnen?.(k.id, `max_output < ${MIN_OUTPUT} (${k.maxOutput})`); ablehnen?.(k.id, `max_output < ${minOutput} (${k.maxOutput})`);
continue; continue;
} }
if (k.maxOutput === 0) { if (k.maxOutput === 0) {
+2 -1
View File
@@ -154,9 +154,10 @@ export async function aufloesen(c: Config, bisher: State, opts?: { dryRun?: bool
def.coding !== undefined ? `Coding>=${def.coding}` : null, def.coding !== undefined ? `Coding>=${def.coding}` : null,
def.agentic !== undefined ? `Agentic>=${def.agentic}` : null, def.agentic !== undefined ? `Agentic>=${def.agentic}` : null,
`Kontext>=${def.minContext}`, `Kontext>=${def.minContext}`,
def.minOutput !== undefined ? `Output>=${def.minOutput}` : null,
].filter((t): t is string => t !== null); ].filter((t): t is string => t !== null);
const beschreibung = teile.join(", "); const beschreibung = teile.join(", ");
const { auswahl } = await billigsterUeberSchwelle(def.index ?? 0, def.minContext, d.alle, d.key, melden, d.gesperrt, { ...o, minCoding: def.coding, minAgentic: def.agentic }); const { auswahl } = await billigsterUeberSchwelle(def.index ?? 0, def.minContext, d.alle, d.key, melden, d.gesperrt, { ...o, minCoding: def.coding, minAgentic: def.agentic, minOutput: def.minOutput });
if (auswahl === null) throw new Error(`kein Modell erfuellt ${beschreibung}`); if (auswahl === null) throw new Error(`kein Modell erfuellt ${beschreibung}`);
ziel = zuordnungAus(auswahl.candidate, beschreibung, alt?.seit ?? neu.aktualisiert, false); ziel = zuordnungAus(auswahl.candidate, beschreibung, alt?.seit ?? neu.aktualisiert, false);
} }
+27
View File
@@ -124,3 +124,30 @@ describe("Grenze maxOutput", () => {
expect(abgelehnt).toHaveLength(0); expect(abgelehnt).toHaveLength(0);
}); });
}); });
describe("minOutput je Profil", () => {
it("32768 laesst ling als billigsten Kandidaten zu, Default tut es nicht", async () => {
const { alle } = kandidatenBauen({ voll: modelle, konto: null });
const mitGate = await billigsterUeberSchwelle(40, 0, alle, undefined, undefined, new Map(), { varianten: [] });
expect(mitGate.auswahl?.candidate.id).toBe("z-ai/glm-5.3-flash");
const ohneGate = await billigsterUeberSchwelle(40, 0, alle, undefined, undefined, new Map(), { varianten: [], minOutput: 32768 });
expect(ohneGate.auswahl?.candidate.id).toBe("inclusionai/ling-3.0-flash");
});
it("bekannte Ablehnung traegt den wirksamen Grenzwert", async () => {
const { alle } = kandidatenBauen({ voll: modelle, konto: null });
const abgelehnt: Array<{ id: string; grund: string }> = [];
await billigsterUeberSchwelle(40, 0, alle, undefined, (id, grund) => abgelehnt.push({ id, grund }), new Map(), { varianten: [], minOutput: 65536 });
const ling = abgelehnt.find((e) => e.id === "inclusionai/ling-3.0-flash");
expect(ling?.grund).toContain("65536");
});
it("max_output unbekannt bleibt abgelehnt, auch mit herabgesetztem minOutput", async () => {
const { alle } = kandidatenBauen({ voll: modelle, konto: null });
const abgelehnt: Array<{ id: string; grund: string }> = [];
await billigsterUeberSchwelle(40, 0, alle, undefined, (id, grund) => abgelehnt.push({ id, grund }), new Map(), { varianten: [], minOutput: 1 });
const foo = abgelehnt.find((e) => e.id === "foo/ohne-angabe");
expect(foo).toBeDefined();
expect(foo!.grund).toBe("max_output unbekannt");
});
});