From bf7bc2c0b943ed4fde0da61c7cab1577e1929754 Mon Sep 17 00:00:00 2001 From: Elton Turing Date: Tue, 8 Sep 2026 09:07:34 +0200 Subject: [PATCH] =?UTF-8?q?Ausweich=20auch=20f=C3=BCr=20Schwelle-Aliasse?= =?UTF-8?q?=20und=20bei=20429/5xx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- config.json | 13 +-- key-acls.json | 242 ++++++++++++++++++++++++++------------------------ src/config.ts | 9 +- src/server.ts | 34 ++++++- 4 files changed, 167 insertions(+), 131 deletions(-) diff --git a/config.json b/config.json index 7a8a3cc..20b6393 100644 --- a/config.json +++ b/config.json @@ -163,12 +163,13 @@ "ausweich": "auto/mass:fast", "_doku": "Massenarbeit ohne Latenzanspruch: Mail-Ingest, Triage, Zusammenfassungen, opencodes small_model. Laeuft auf der eigenen Karte und kostet nichts. Steht der Knoten oder ist die Karte belegt, weicht der Router einmal auf auto/mass:fast aus -- ohne das haengt sonst jede Titelzeile jedes Agenten (42i/intern#1252)." }, - "auto/mass:fast": { - "art": "schwelle", - "index": 25, - "minContext": 256000, - "_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'." - }, + "auto/mass:fast": { + "art": "schwelle", + "index": 25, + "minContext": 256000, + "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." + }, "auto/allround:junior": { "art": "schwelle", "index": 40, diff --git a/key-acls.json b/key-acls.json index c264d14..e6f3237 100644 --- a/key-acls.json +++ b/key-acls.json @@ -1,120 +1,126 @@ { - "_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.", - "keys": { - "persona-spark": [ - "42i/marvin-2609-core:none", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "42i/marvin-stt", - "42i/marvin-tts", - "role/engineer", - "role/worker" - ], - "persona-buzz": [ - "42i/marvin-2609-core:none", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "42i/marvin-stt", - "42i/marvin-tts", - "role/ops", - "role/worker" - ], - "persona-jessie": [ - "42i/marvin-2609-core:medium", - "42i/marvin-2609-core:none", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "42i/marvin-stt", - "42i/marvin-tts", - "role/assistant", - "role/marketing" - ], - "persona-klara": [ - "42i/marvin-2609-core:none", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "42i/marvin-stt", - "42i/marvin-tts", - "role/service" - ], - "persona-elton": [ - "42i/marvin-2609:low", - "42i/marvin-2609:medium", - "42i/marvin-2609:high", - "42i/marvin-2609:max", - "42i/marvin-2609-core:none", - "42i/marvin-2609-core:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "42i/marvin-stt", - "42i/marvin-stt-turbo", - "42i/marvin-tts", - "42i/marvin-tts-turbo", - "role/*", - "openrouter/*", - "alt/*", - "auto/*" - ], - "persona-festus": [ - "42i/marvin-2609-core:none", - "42i/marvin-2609-core:medium", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "role/privacy" - ], - "persona-aria": [ - "42i/marvin-2609-core:none", - "42i/marvin-2609-core:medium", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "role/architect" - ], - "persona-inka": [ - "42i/marvin-2609-core:none", - "42i/marvin-2609-core:medium", - "42i/marvin-2609:low", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "42i/marvin-stt", - "42i/marvin-stt-turbo", - "42i/marvin-tts", - "42i/marvin-tts-turbo", - "openrouter/openai/gpt-5.6-luna", - "role/inka" - ], - "persona-andreas": [], - "lab-xo": [ - "42i/marvin-2609:low", - "42i/marvin-2609:high", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts", - "role/xo", - "role/*", - "auto/*" - ], - "team-copilot": [ - "42i/marvin-2609:low", - "42i/marvin-2609:medium", - "deizo/kira-reasoning", - "deizo/kira-stt", - "deizo/kira-tts" - ] - } + "_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.", + "keys": { + "persona-spark": [ + "42i/marvin-2609-core:none", + "42i/marvin-2609:medium", + "42i/marvin-stt", + "42i/marvin-tts", + "auto/coding:junior", + "auto/mass:fast", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/engineer", + "role/worker" + ], + "persona-buzz": [ + "42i/marvin-2609-core:none", + "42i/marvin-2609:medium", + "42i/marvin-stt", + "42i/marvin-tts", + "auto/coding:junior", + "auto/mass:fast", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/ops", + "role/worker" + ], + "persona-jessie": [ + "42i/marvin-2609-core:medium", + "42i/marvin-2609-core:none", + "42i/marvin-2609:medium", + "42i/marvin-stt", + "42i/marvin-tts", + "auto/mass:fast", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/assistant", + "role/marketing" + ], + "persona-klara": [ + "42i/marvin-2609-core:none", + "42i/marvin-2609:medium", + "42i/marvin-stt", + "42i/marvin-tts", + "auto/mass:fast", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/service" + ], + "persona-elton": [ + "42i/marvin-2609:low", + "42i/marvin-2609:medium", + "42i/marvin-2609:high", + "42i/marvin-2609:max", + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "42i/marvin-stt", + "42i/marvin-stt-turbo", + "42i/marvin-tts", + "42i/marvin-tts-turbo", + "role/*", + "openrouter/*", + "alt/*", + "auto/*" + ], + "persona-festus": [ + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:medium", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/privacy" + ], + "persona-aria": [ + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:medium", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/architect" + ], + "persona-inka": [ + "42i/marvin-2609-core:none", + "42i/marvin-2609-core:medium", + "42i/marvin-2609:low", + "42i/marvin-2609:medium", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "42i/marvin-stt", + "42i/marvin-stt-turbo", + "42i/marvin-tts", + "42i/marvin-tts-turbo", + "openrouter/openai/gpt-5.6-luna", + "role/inka" + ], + "persona-andreas": [], + "lab-xo": [ + "42i/marvin-2609:low", + "42i/marvin-2609:high", + "42i/marvin-2609:medium", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts", + "role/xo", + "role/*", + "auto/*" + ], + "team-copilot": [ + "42i/marvin-2609:low", + "42i/marvin-2609:medium", + "deizo/kira-reasoning", + "deizo/kira-stt", + "deizo/kira-tts" + ] + } } diff --git a/src/config.ts b/src/config.ts index 7297b2c..5026667 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,10 +15,11 @@ 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: Alias, auf den bei Upstream-Ausfall gewechselt wird. Nur fuer - // "fest" sinnvoll -- ein festes Ziel hat sonst keinen Rueckfall, und genau - // das trifft das lokale Qwen: steht der Knoten, haengt jede Massenarbeit. + | { art: "schwelle"; index?: number; coding?: number; agentic?: number; minContext: 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 + // Ratengrenzen und haengendem Upstream (429-Burst 2026-09-07). | { art: "fest"; upstream: string; model: string; body?: Record; ausweich?: string }; export type Config = { port: number; diff --git a/src/server.ts b/src/server.ts index 44ef50f..adfc3a2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -118,7 +118,7 @@ class SlotFrist extends Error { } // --- Aufloesung Name -> Ziel --------------------------------------------------- -type Ziel = { alias: string; upstreamName: string; upstream: Upstream; model: string; effort: Effort | null; body?: Record; effortSetzen: boolean; /** Alias, auf den bei Upstream-Ausfall einmal ausgewichen wird (nur "fest", s. config.ts). */ ausweich?: string }; +type Ziel = { alias: string; upstreamName: string; upstream: Upstream; model: string; effort: Effort | null; body?: Record; effortSetzen: boolean; /** Alias, auf den bei Upstream-Ausfall einmal ausgewichen wird (fest oder schwelle, s. config.ts). */ ausweich?: string }; type Fehler = { fehler: string; status: number }; const OR = (): Upstream => config.upstreams.openrouter; @@ -172,7 +172,7 @@ async function zielBestimmen(angefragtRoh: string, keyName: string, clientEffort if (def.art === "fest") return { alias: name, upstreamName: def.upstream, upstream: config.upstreams[def.upstream], model: def.model, effort: null, body: def.body, effortSetzen: false, ausweich: def.ausweich }; const z = state.zuordnung[name]; if (z === undefined) return { fehler: `${name} ist noch nicht aufgeloest (Zuordnung fehlt) -- spaeter erneut`, status: 503 }; - return { alias: name, upstreamName: "openrouter", upstream: OR(), model: z.model, effort: z.effort, effortSetzen: true }; + return { alias: name, upstreamName: "openrouter", upstream: OR(), model: z.model, effort: z.effort, effortSetzen: true, ausweich: def.ausweich }; } if (name.startsWith("openrouter/")) { const { id, effort } = referenzParsen(name.slice(11)); @@ -373,7 +373,9 @@ Bun.serve({ } catch (e) { freigeben(); const grund = (e as Error).message; - // Ausweich (42i/intern#1252): ein "festes" Ziel hat keinen Rueckfall -- + // Ausweich (42i/intern#1252): ein Ziel ohne eigenen Rueckfall (festes + // Ziel oder Schwelle auf einem Ausfall-Modell) wuerde den Aufrufer leer + // ausgehen lassen -- // steht der lokale LLM-Knoten oder haelt eine fremde Arbeit die Karte, // bekaeme der Aufrufer gar keine Antwort statt einer langsameren. Genau // das trifft die Massenarbeit (auto/mass -> lokales Qwen), die ueberall @@ -407,6 +409,32 @@ Bun.serve({ return Response.json({ error: { message: `Upstream ${ziel.upstreamName} nicht erreichbar: ${eintrag.fehler}` } }, { status: 502 }); } } + // Ausweich auch bei Upstream-Antwortfehlern: 429 und 5xx kommen als + // gueltige HTTP-Antwort durch und erreichen den fetch-catch nicht -- genau + // der Modus, in dem Billigmodelle nachts ausfallen (429-Burst am + // 2026-09-07, 42i/intern#1252). Der Wechsel passiert, bevor das erste + // Byte an den Client geht, und nur EINMAL; scheitert auch der Ausweich, + // geht die urspruengliche Antwort durch. + if (!antwort.ok && (antwort.status === 429 || antwort.status >= 500) && ziel.ausweich !== undefined && origJson !== null) { + const z2 = await zielBestimmen(ziel.ausweich, keyName); + if (!("fehler" in z2)) { + sagen(`Ausweich ${ziel.alias} -> ${ziel.ausweich} (Upstream ${ziel.upstreamName} antwortete ${antwort.status})`); + const h2 = new Headers(headers); + const k2 = upstreamKey(z2.upstream); + if (k2 !== undefined) h2.set("authorization", `Bearer ${k2}`); + if (z2.upstreamName === "openrouter") { h2.set("http-referer", "https://llm.lan"); h2.set("x-title", `llmrouter/${keyName}`); } + freigeben(); + freigeben = await belegen(z2.upstreamName, z2.upstream); + try { + antwort = await fetch(`${z2.upstream.base}${pfad}`, { method: "POST", headers: h2, body: JSON.stringify(jsonBody(origJson, z2, pfad)), signal: AbortSignal.timeout(600_000) }); + eintrag.alias = `${ziel.alias}->${ziel.ausweich}`; eintrag.model = z2.model; eintrag.upstream = z2.upstreamName; eintrag.effort = z2.effort; + ziel = z2; + } catch (e2) { + freigeben(); + eintrag.fehler = `Ausweich: ${(e2 as Error).message}`; + } + } + } eintrag.status = antwort.status; const ct = antwort.headers.get("content-type") ?? ""; const weiter = new Headers(antwort.headers);