Ausweich auch für Schwelle-Aliasse und bei 429/5xx

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
This commit is contained in:
2026-09-08 09:07:34 +02:00
parent 16a6b98c51
commit bf7bc2c0b9
4 changed files with 167 additions and 131 deletions
+7 -6
View File
@@ -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,
+124 -118
View File
@@ -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"
]
}
}
+5 -4
View File
@@ -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<string, unknown>; ausweich?: string };
export type Config = {
port: number;
+31 -3
View File
@@ -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<string, unknown>; 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<string, unknown>; 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);