Compare commits
4
Commits
ticket/1283-gate
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789d8c187e | ||
|
|
7c6e747145 | ||
|
|
d509d975e4 | ||
|
|
e14403420d |
+3
-1
@@ -167,8 +167,9 @@
|
||||
"art": "schwelle",
|
||||
"index": 25,
|
||||
"minContext": 256000,
|
||||
"minOutput": 32768,
|
||||
"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": {
|
||||
"art": "schwelle",
|
||||
@@ -221,6 +222,7 @@
|
||||
"lead": "auto/leader:junior",
|
||||
"leader": "auto/leader:junior",
|
||||
"worker": "auto/mass:fast",
|
||||
"coder": "auto/coding:junior",
|
||||
"engineer": "auto/coding:junior",
|
||||
"po": "auto/allround:junior",
|
||||
"speaker": "auto/allround:junior",
|
||||
|
||||
+68
-25
@@ -18,12 +18,12 @@ keine Format-Übersetzung — alle Upstreams sprechen OpenAI-Format.
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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 |
|
||||
|
||||
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
|
||||
|
||||
@@ -59,7 +59,7 @@ 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/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 |
|
||||
@@ -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.
|
||||
|
||||
**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`.
|
||||
**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
|
||||
`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
|
||||
|
||||
- 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
|
||||
sind **dieselben Token wie bei LiteLLM**. Noch ohne Token im Router:
|
||||
`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
|
||||
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'
|
||||
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
|
||||
@@ -158,25 +161,40 @@ 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 weiter als statisches Binary im LXC (Stand 2026-09-08); der Umstieg aufs
|
||||
Image ist **geprüft und machbar**, das Rezept steht unten — vollzogen ist er
|
||||
noch nicht.
|
||||
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` — 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`
|
||||
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, 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 —
|
||||
Muster wie `caddy.service`: podman-compose + systemd-oneshot), das Image ist
|
||||
@@ -243,9 +261,9 @@ Image danach rückstandsfrei entfernt.
|
||||
|
||||
| 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 |
|
||||
| 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` |
|
||||
@@ -268,6 +286,31 @@ starten — LiteLLM bedient die `2608`-Namen weiter, die `2609`-, `alt/`- und
|
||||
|
||||
## 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
|
||||
|
||||
+8
-3
@@ -20,7 +20,7 @@ reicht Durchreichen. Entscheidung Andreas, 2026-09-05.
|
||||
| `deizo/kira-reasoning`, `deizo/kira-stt` | Modelle im Prod-RZ deizo, 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 |
|
||||
| `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 |
|
||||
| `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) |
|
||||
@@ -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
|
||||
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`
|
||||
ü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`,
|
||||
welches Modell dahintersteht, stellt man in `config.json` um, ohne Container.
|
||||
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
|
||||
Senior-Stufe (Andreas: hochstufen einzeln und mit Anlass).
|
||||
|
||||
|
||||
+10
-1
@@ -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": {
|
||||
"persona-spark": [
|
||||
"42i/marvin-2609-core:none",
|
||||
"42i/marvin-2609:medium",
|
||||
"42i/marvin-stt",
|
||||
"42i/marvin-tts",
|
||||
"42i/marvin-stt-turbo",
|
||||
"42i/marvin-tts-turbo",
|
||||
"auto/coding:junior",
|
||||
"auto/mass:fast",
|
||||
"deizo/kira-reasoning",
|
||||
"deizo/kira-stt",
|
||||
"deizo/kira-tts",
|
||||
"role/engineer",
|
||||
"role/coder",
|
||||
"role/worker"
|
||||
],
|
||||
"persona-buzz": [
|
||||
@@ -19,6 +22,8 @@
|
||||
"42i/marvin-2609:medium",
|
||||
"42i/marvin-stt",
|
||||
"42i/marvin-tts",
|
||||
"42i/marvin-stt-turbo",
|
||||
"42i/marvin-tts-turbo",
|
||||
"auto/coding:junior",
|
||||
"auto/mass:fast",
|
||||
"deizo/kira-reasoning",
|
||||
@@ -33,6 +38,8 @@
|
||||
"42i/marvin-2609:medium",
|
||||
"42i/marvin-stt",
|
||||
"42i/marvin-tts",
|
||||
"42i/marvin-stt-turbo",
|
||||
"42i/marvin-tts-turbo",
|
||||
"auto/mass:fast",
|
||||
"auto/coding:junior",
|
||||
"deizo/kira-reasoning",
|
||||
@@ -46,6 +53,8 @@
|
||||
"42i/marvin-2609:medium",
|
||||
"42i/marvin-stt",
|
||||
"42i/marvin-tts",
|
||||
"42i/marvin-stt-turbo",
|
||||
"42i/marvin-tts-turbo",
|
||||
"auto/mass:fast",
|
||||
"deizo/kira-reasoning",
|
||||
"deizo/kira-stt",
|
||||
|
||||
+5
-3
@@ -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 Alias =
|
||||
| { art: "alternative"; referenz: string }
|
||||
// Schwelle auf bis zu drei Achsen (42i/intern#1252): index = Intelligence,
|
||||
// coding und agentic je eigene Untergrenze. Fehlt eine, gilt sie nicht.
|
||||
| { art: "schwelle"; index?: number; coding?: number; agentic?: number; minContext: number; ausweich?: string }
|
||||
// Schwelle auf bis zu vier Achsen (42i/intern#1252): index = Intelligence,
|
||||
// coding und agentic je eigene Untergrenze, minOutput als Overlay auf das
|
||||
// 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"
|
||||
// und "schwelle" -- ein Ziel ohne Rueckfall haengt sonst jede Arbeit, die
|
||||
// darauf zeigt: festes Ziel = lokales Qwen, schwelle = Billigmodell mit
|
||||
|
||||
@@ -32,7 +32,14 @@ import { spawnSync } from "child_process";
|
||||
|
||||
export const API = process.env.OPENROUTER_API_URL ?? "https://openrouter.ai/api/v1";
|
||||
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 INPUT_WEIGHT = 10; // Agenten-Lastprofil: Input dominiert
|
||||
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
|
||||
// der Achsen fordert, uebergibt fuer die anderen keinen Wert; minIndex 0
|
||||
// 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[] }> {
|
||||
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);
|
||||
return { auswahl, infrage };
|
||||
}
|
||||
@@ -575,19 +582,20 @@ export async function billigsterUeberSchwelle(
|
||||
*/
|
||||
function klasseFiltern(
|
||||
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[],
|
||||
gesperrt: Map<string, string>,
|
||||
ablehnen?: (id: string, grund: string) => void,
|
||||
): Candidate[] {
|
||||
const minOutput = regel.minOutput ?? MIN_OUTPUT;
|
||||
const infrage: Candidate[] = [];
|
||||
for (const k of alle.values()) {
|
||||
if (k.id === regel.ausser || !k.tauglich || gesperrt.has(k.id)) continue;
|
||||
if (k.context < regel.minContext) continue;
|
||||
if (regel.maxWeighted !== undefined && k.weighted >= regel.maxWeighted) continue;
|
||||
if (regel.minAgentic !== undefined && !(k.agentic >= regel.minAgentic)) continue;
|
||||
if (k.maxOutput > 0 && k.maxOutput < MIN_OUTPUT) {
|
||||
ablehnen?.(k.id, `max_output < ${MIN_OUTPUT} (${k.maxOutput})`);
|
||||
if (k.maxOutput > 0 && k.maxOutput < minOutput) {
|
||||
ablehnen?.(k.id, `max_output < ${minOutput} (${k.maxOutput})`);
|
||||
continue;
|
||||
}
|
||||
if (k.maxOutput === 0) {
|
||||
|
||||
+2
-1
@@ -154,9 +154,10 @@ export async function aufloesen(c: Config, bisher: State, opts?: { dryRun?: bool
|
||||
def.coding !== undefined ? `Coding>=${def.coding}` : null,
|
||||
def.agentic !== undefined ? `Agentic>=${def.agentic}` : null,
|
||||
`Kontext>=${def.minContext}`,
|
||||
def.minOutput !== undefined ? `Output>=${def.minOutput}` : null,
|
||||
].filter((t): t is string => t !== null);
|
||||
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}`);
|
||||
ziel = zuordnungAus(auswahl.candidate, beschreibung, alt?.seit ?? neu.aktualisiert, false);
|
||||
}
|
||||
|
||||
@@ -124,3 +124,30 @@ describe("Grenze maxOutput", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user