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
Port aus 42i/agents llmlite/ (Merge 78c3c57, Ursprung 5730cfd) — der
Router lebt seit dem 2026-09-08 in diesem Repo. Arbeitsmodelle (nicht
TTS) brauchen mindestens 65536 Output-Token; inclusionai/ling-3.0-flash
(hart 32768) fällt damit aus der Auswahl. Geprüfte Kette: Rot-Beweis
(Produktion wählte ling für low + haiku, state.json 06:44), Nachher
(Ablehnung "max_output < 65536 (32768)"), Gegenprobe (beschädigtes Gate
quittiert die Testsuite rot mit benanntem Schaden).
Refs 42i/intern#1283
Andreas setzt Jessie fuer die Show-Unterstuetzung (17.9., Kassel) bis auf
Weiteres auf role/leader. auto/coding:junior ergaenzt, damit die
auto/mass-Ausweichkette (mass -> mass:fast -> coding:junior) durch die
ACL-Pruefung kommt, statt im letzten Glied still 403 zu laufen.
Ausgerollt auf llm.lan mit Backup *.bak-show, Router-Neustart, geprueft:
role/leader loest mit persona-jessie auf glm-5.3-flash auf.
Beobachtet am 2026-09-08: Registry-Login ging durch, eine Minute später
401 vom selben Server (Anmelde-Stand unverändert, per Hand mit demselben
Konto verifizierbar). Drei Versuche mit 5s Abstand.
bun test + Build je Push/PR; Push nach main published
git.42i.org/spine/llmrouter:latest (+sha-Tag) und verifiziert den Pull per
/health im Container. Muster: dev-image.yml aus 42i/live (buildx, eigener
Builder, --privileged, DOCKER_API_VERSION 1.43, Verify-Pull).
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.
Alle Laufzeitdateien (config, ACLs, keys, state, aa-cache, log) gehen über
den Mount ./data:/data, weil logDir und ACLs relativ zum CONFIG_PATH
aufgelöst werden. Ohne Keys startet der Router trotzdem, /health bleibt
bedienbar.
Der Router verlaesst 42i/agents: die gemeinsame Modellauswahl wandert als
eigenstaendige Kopie nach src/ (die llmlite-Fassung bleibt dort fuer den
Tier-Updater). Referenz-Stellen zu LiteLLM-Durchreiche und Parallelbetrieb
korrigiert (entfallen mit der Abschaltung 2026-09-07).
auto/mass:fast war als Endstation ohne Rückfall: ling-3.0-flash fiel
am 2026-09-07 nachts aus (5x 502 nach ~270 s hängendem Upstream,
117x 429), und der bisherige Ausweich griff nur bei fetch-Fehlern und
nur für fest-Ziele. Jetzt darf auch ein Schwelle-Alias ausweichen,
und der Ausweich löst bei 429/5xx aus — bevor das erste Byte an den
Client geht, also gefahrlos wiederholbar, weiterhin nur einmal.
auto/mass:fast weicht auf auto/coding:junior aus (ling erfüllt deren
Schwellen strukturell nie, der Rückfall landet also garantiert auf
einem tauglichen Modell). Die Container-Personas bekommen die
Kettenziele in die ACL — ohne sie schlägt der Fallback in
zielBestimmen still fehl und der ursprüngliche Fehler geht durch.
Verifiziert im lokalen End-to-End-Test gegen Schein-Upstreams:
Ausweich bei 429 (200 vom Ersatzmodell, Log-Zeile alias->ausweich),
Durchreichung von 429 ohne ACL-Freigabe, fetch-catch-Pfad unverändert
funktionierend.
Refs 42i/intern#1252
Der Log wusste bisher WER gerufen hat (Key, Rolle), aber nicht WORAN gearbeitet
wurde. Damit liess sich "was kostet ein Vorgang" gar nicht bilden -- die
Voraussetzung fuer die Frage, ob die teuren Tickets die sind, die ein Mensch
wollte.
- server.ts liest den Header x-42i-ticket und legt ihn in den Log-Eintrag
- log.ts: Feld `ticket` in JSONL und SQLite; die Spalte wird per ALTER
nachgezogen (Bestandstabellen haben sie nicht), dazu ein Index darauf.
Das INSERT nennt jetzt Spaltennamen statt Positionen -- sonst haette die
neue Spalte die Reihenfolge zerschossen.
- opencode.json und der Container-Entrypoint schicken den Header aus
{env:AGENT_TICKET}; job.sh exportiert die Variable ohnehin seit 2026-09-02.
Wer keines mitschickt (Chat, Handbetrieb), steht als null im Log -- auch das
ist eine Auskunft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Andreas 2026-09-07: 'stellen wir die worker mal auf ling um und schauen was
passiert.' Bewusst gegen meine Empfehlung aus #1252 (ling Coding 50,6 /
Agentic 21,1 gegen glm 71,5 / 51,5) -- und bewusst als Versuch: der Rueckweg
ist diese eine Zeile.
Nur role/worker. engineer, qa und perf bleiben auf auto/coding:junior, damit
der Vergleich sauber bleibt.
Zu beobachten ist nicht der Preis (der faellt sicher), sondern ob die
Ergebnisse die Abnahme des Teamleiters bestehen -- und wie oft er neu
delegieren muss. Ein Worker, der in Schleifen haengt, braucht am Ende mehr
Zuege als er spart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gegenprobe am Produktivrouter nach dem ersten Fix: Ein abgebrochener Stream
gab den Slot WEITERHIN nicht frei -- cancel() im TransformStream wird von Bun
beim Verbindungsabbruch nicht aufgerufen. Immerhin griff die zweite Haerte:
statt zwei Stunden Stille gab es nach 30 s ein 503 mit Grund.
Jetzt haengt der Slot an req.signal, das beim Abbruch zuverlaessig feuert, plus
einem Sicherheitsnetz von 660 s (laenger als der Upstream-Timeout darf kein
Slot gehalten werden). Die Freigabe ist gegen Doppelaufruf gesichert, ein
spaeteres flush() schadet also nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ein abgebrochener Streaming-Aufruf gegen das lokale Qwen (maxParallel 1) hat
den einzigen Slot dauerhaft gefressen: die Freigabe hing allein im flush() des
TransformStreams, und der laeuft bei cancel() nie. Danach reihte sich jeder
weitere Aufruf UNBEGRENZT in die Warteschlange ein, statt zu scheitern --
auto/mass, role/ops (Buzz) und das small_model aller Container-Personas hingen
zwei Stunden, ohne dass irgendwo etwas rot wurde. Diagnose von Elton im
Selbstheilungslauf, dreimal in Folge belegt.
Drei Haerten:
- Die Freigabe ist gegen Doppelaufruf gesichert (Flag). Erst dadurch darf sie
an mehreren Stellen stehen, ohne Slots zu erfinden.
- cancel() gibt jetzt frei, nicht nur flush(). Das ist der eigentliche Fehler:
bricht der Client ab, laeuft flush() nie.
- Wer laenger als 30 s auf einen Slot wartet, bekommt 503 mit Grund. Ein
Fehler, an dem der Aufrufer etwas aendern kann, ist besser als Stille.
Ausgeloest habe ich den Fall selbst: zwei eigene Testaufrufe gegen role/ops
liefen gegen 18:05 ins Timeout und wurden abgebrochen -- genau die Handlung,
die den Slot frisst. Der Fehler im Code ist aelter, der Ausloeser war meiner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>