Eine Kaskade von Agenten: das Terminal wählt den, der gerade läuft
Als Claude teilweise ausfiel und einige Modelle nicht mehr liefen, wollte ich auf Gemini wechseln — und merkte, dass mein Fallback seit Monaten kaputt war. So baut man einen, der wirklich hält.
TL;DR
Ein Bash-Launcher namens agent, der vor dem Start das beste funktionierende AI-Coding-CLI auswählt. Er degradiert entlang zweier Achsen: erst das Modell (Opus → Sonnet, innerhalb von Anthropic), dann der Transport, und erst zuletzt der Anbieter. Jede Probe macht einen echten API-Aufruf, denn Modelllisten lügen. Ein Watchdog prüft die unteren Stufen alle zwei Stunden und meldet per Telegram, wenn eine ausfällt.
$ agent --status
OK claude / claude-opus-5 api.anthropic.com reachable, claude-opus-5 answered
OK claude / claude-sonnet-5 api.anthropic.com reachable, claude-sonnet-5 answered
OK claude-proxy / claude-opus-5 claude-opus-5 answered
OK claude-proxy / claude-sonnet-5 claude-sonnet-5 answered
OK agy antigravity answered on Gemini 3.6 Flash (Low)
OK gemini localhost:8317 answered for gemini-3.1-pro-lowDas Problem
Wenn Claude nicht verfügbar ist, soll mein Terminal weiterarbeiten. Die naive Variante lautet „wenn Claude ausfällt, starte Gemini“ — und genau das glaubte ich zu haben. Aber die interessanten Fehler sind keine Ausfälle. Der häufigste: Opus ist am Limit, während Sonnet einwandfrei antwortet. Dann zu einem anderen Anbieter zu springen ist absurd — das Modell nebenan ist gesund.
Ein echter Fallback muss also erst das Modell degradieren und dann den Anbieter. Und er muss wissen, welche Stufe tatsächlich lebt — das war der schwierige Teil, und den habe ich zweimal falsch gemacht.
Die fünfmonatige Lüge
Bevor ich irgendetwas schrieb, prüfte ich den Bestand. Das Auth-Verzeichnis des Proxys erzählte die ganze Geschichte in zwei Zeitstempeln:
claude-newiqa@gmail.com.json — heute erneuert, 06:41 · gemini-newiqa@gmail.com-….json — zuletzt am 27. März
Das Claude-Token wurde täglich erneuert. Das Gemini-Token hatte sich fünf Monate nicht bewegt. Mein Health-Check sah nur auf claude-*.json, also fiel es niemandem auf. Der Fallback war seit dem Frühjahr Dekoration — ein grünes Licht über einem leeren Raum. Diese eine Beobachtung prägte alles Weitere: ein Fallback, den niemand wirklich aufruft, ist kein Fallback.
Warum ein Gateway das nicht löst
Der naheliegende Schritt ist ein lokales LLM-Gateway — Bifrost, LiteLLM — mit Anbieter-Failover in der Konfiguration. Eines läuft bei mir bereits (CLIProxyAPI), ein zweites hätte nur einen weiteren Daemon zum Betreuen gebracht.
Wichtiger: Ein Gateway scheitert an der eigentlichen Aufgabe. Failover auf API-Ebene bedeutet, das Modell unter einem Client auszutauschen. Schickt man Claude Code eine Gemini-Antwort durch eine Übersetzungsschicht, zerbricht es an Tool-Use und Streaming. Der Fallback muss das CLI wechseln, nicht das Modell dahinter. Das ist Aufgabe eines Launchers, nicht eines Proxys.
Sechs Stufen, zwei Achsen
Anbieter und Modell fallen unabhängig voneinander aus, deshalb verschränkt die Leiter beides. Jede Stufe bringt etwas Bestimmtes, und der Preis des Abstiegs steht explizit da:
| Rung | What it survives | Cost of getting there |
|---|---|---|
| claude + opus | nothing — this is the happy path | — |
| claude + sonnet | Opus capped or overloaded | weaker model, same session shape |
| claude-proxy + opus | stale local login, broken CLI auth | separate OAuth token |
| claude-proxy + sonnet | both of the above at once | weaker model via proxy |
| agy | Anthropic outage / exhausted plan | different vendor, different CLI |
| gemini | agy itself broken | different CLI again, shares agy's quota |
Im Code ist eine Stufe ein Tripel: Backend, Modell zum Proben, Modell zum Ausführen. Das leere Ausführungsmodell auf der obersten Stufe ist wichtiger als es aussieht: kein --model zu übergeben bewahrt das opus[1m] des Profils samt 1M-Kontext. Das Modell explizit zu benennen würde das bei jedem Start still auf einfaches Opus kürzen.
# Each rung: backend | model to probe | model to run with
# An empty run-model means "let the CLI use its configured default", which keeps
# the profile's opus[1m] (and its 1M context) instead of downgrading it to plain
# opus just to name it explicitly.
RUNGS=(
"claude|$AGENT_OPUS_MODEL|"
"claude|$AGENT_SONNET_MODEL|sonnet"
"claude-proxy|$AGENT_OPUS_MODEL|$AGENT_OPUS_MODEL"
"claude-proxy|$AGENT_SONNET_MODEL|$AGENT_SONNET_MODEL"
"agy||"
"gemini||"
)Ist die oberste Stufe nicht verfügbar, wird der Abstieg angesagt statt verschwiegen — eine degradierte Sitzung darf nie für eine normale gehalten werden:
$ agent -p "explain this bug"
[agent] Claude Code (claude-opus-5) unavailable — claude-opus-5 unavailable: {"error":...}
[agent] using Claude Code (claude-sonnet-5)
# ...the session continues on Sonnet, inside Anthropic, with no vendor switch.Modelllisten lügen
Der erste Reflex ist eine billige Probe: das Gateway nach /v1/models fragen und prüfen, ob das Modell dabei ist. Das ist wertlos. Mein Proxy führte gemini-* die ganze Zeit fröhlich auf, während jeder Aufruf auth_unavailable zurückgab. Das Antigravity-CLI verhält sich genauso — agy models antwortet aus einem Katalog, nicht aus einer funktionierenden Sitzung.
Dasselbe gilt für den Modellzustand. Kein Listen-Endpunkt verrät, dass Opus gerade im Rate-Limit steckt. Das zeigt nur eine echte Anfrage — ein 429 oder 529. Also macht jede Probe genau das:
# Real /v1/messages call for ONE model. This is what distinguishes "Opus is
# capped" from "Anthropic is down" — no model list can tell you that.
probe_model() {
local m="$1" body
body=$(curl -sS --max-time "$PROBE_TIMEOUT" \
"$AGENT_PROXY_URL/v1/messages" \
-H "Authorization: Bearer $AGENT_PROXY_KEY" \
-H 'content-type: application/json' \
-d "{\"model\":\"$m\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}")
case "$body" in
*'"type":"message"'*) REASON="$m answered"; return 0 ;;
esac
REASON="$m unavailable: $(printf '%s' "$body" | tr -d '\n' | cut -c1-140)"
return 1
}Die Modellverfügbarkeit der offiziellen Stufen wird vom Proxy geliehen, der dasselbe Anthropic-Konto bedient — ein dort gedeckeltes Modell ist auch im offiziellen CLI gedeckelt. Der gesamte Preflight wurde bei 7 Tokens und rund 1,3 Sekunden gemessen: ein kostenloser unauthentifizierter 401-Check plus ein Ein-Token-Aufruf, wobei das Opus-Ergebnis gecacht wird.
Zwei Netze, nicht eines
Eine Preflight-Probe fängt nicht alles. Läuft das Kontingent zwischen Probe und Start aus, hatte die Probe recht und war trotzdem nutzlos. Deshalb gibt es ein zweites Netz: Exit-Code plus verstrichene Zeit.
start=$SECONDS
run_rung "$backend" "$rmodel" "$@"
rc=$?
elapsed=$(( SECONDS - start ))
# Clean exit, or the user interrupted it — either way, done.
if [ $rc -eq 0 ] || [ $rc -eq 130 ]; then
exit $rc
fi
# Survived long enough to have been genuinely used: a real error, not a rung
# that never started. Do not silently rerun the work somewhere else.
if [ $elapsed -ge $FASTFAIL_SECONDS ]; then
exit $rc
fi
warn "$(label "$backend" "$pmodel") exited $rc after ${elapsed}s — treating as unavailable"Die 25-Sekunden-Schwelle kodiert ein Urteil. Eine Stufe, die fast sofort stirbt, ist nie wirklich gestartet — schlechte Auth, gedeckeltes Modell — weiterzugehen ist also sicher. Eine Stufe, die minutenlang lief und dann scheiterte, hat echt gearbeitet, und diese Arbeit still bei einem anderen Anbieter zu wiederholen wäre schlimmer, als den Fehler zu zeigen.
Der Watchdog und die Falle darin
Der Launcher läuft nur, wenn ich ihn starte, also kann eine Stufe zwischen Sitzungen verrotten. Ein Check in meinem bestehenden Health-Droid (launchd, alle zwei Stunden, Telegram-Alerts) ruft denselben Probe-Code auf. Meine erste Version hatte genau den Bug, um den es hier geht: Sie meldete Erfolg bei der ersten grünen Stufe.
# Healthy — every non-Anthropic rung answers
$ agent --check-fallback
all provider-independent fallbacks OK: agy — antigravity answered...; gemini — ...
rc=0
# One rung rotted while the other still works. This is the case that used to
# pass silently, and the whole reason the check exists.
$ GEMINI_API_KEY=broken agent --check-fallback
DEGRADED: still covered by agy — antigravity answered on Gemini 3.6 Flash (Low)
but a rung died: gemini — gemini call failed: {"error":"Invalid API key"}
rc=1Das ist falsch, denn die beiden Nicht-Anthropic-Stufen halten getrennte Tokens — das Antigravity-CLI ein eigenes, die Gemini-Stufe das des Proxys. Eine kann verrotten, während die andere perfekt aussieht, und der Abbruch beim ersten Erfolg würde das genauso verbergen wie damals den fünfmonatigen Ausfall. Deshalb prüft der Check jede Stufe und wertet DEGRADED ebenfalls als Fehler: Frühwarnung, solange noch Reserve da ist.
Drei Fehler, die man sich notieren sollte
Jeder davon erzeugte ein grünes Signal über kaputter Mechanik — derselbe Fehlermodus, dreimal an einem Nachmittag:
- Die Probe testete ein Modell, das ich nie nutze. Ich probte
gemini-3.1-pro-preview, während das CLI aufgemini-3.1-pro-lowkonfiguriert war. Beide lösen zu verschiedenen Anbietern auf, also diagnostizierte ich ein „abgelaufenes Token“, das es nie gab — tatsächlich war ein Anbieter schlicht nicht eingeloggt. Eine Probe muss exakt den Pfad des echten Clients gehen. - Eine still leere Variable. Das Parsen des Proxy-Schlüssels mit
awkund\x27für das Anführungszeichen ergab unter BSD awk einen leeren String — ohne Fehler, ohne Warnung. Der Check bestand trotzdem, weil die geprüfte Stufe keinen Schlüssel brauchte. Behoben durchawk -v q="'". - Die Probe bestand, während die Realität scheiterte. Eine
curl-Probe gegen den Gemini-Endpunkt lieferte 200, doch das echtegemini -pstartete nicht: Der Headless-Modus blockiert bei nicht vertrauenswürdigen Verzeichnissen. Die Stufe war genau in der Situation kaputt, für die sie existiert.
Benutzung
agent tippen liefert die beste verfügbare Stufe mit der gewohnten Claude-Code-Erfahrung. Explizite Auswahl überspringt das Proben komplett — wer Gemini verlangt, bekommt Gemini:
agent # auto-pick the best live rung
agent --use gemini # force a backend
agent --use claude:sonnet # force a backend AND a model
agent --use claude-proxy:opus # opus/sonnet/haiku map to full ids per backend
agent --list # backend names
agent --status # probe every rung and report
agent --check-fallback # exit 1 if a non-Anthropic rung is missing
agent -p "..." # flags pass straight through to the chosen CLIFazit
Die Technik hier ist unspektakulär: ein Bash-Skript, ein paar curl-Aufrufe, ein geordnetes Array. Aufwand kostete das Misstrauen gegen die eigenen Prüfungen. Dreimal baute ich etwas, das Erfolg meldete, während darunter alles kaputt war — und jedes Mal war die billige, schnelle Probe schuld.
Resilienz ist nicht die Liste konfigurierter Backends. Sie ist die Frage, ob irgendetwas überprüft, dass die Liste noch stimmt. Meine hatte auf dem Papier sechs Stufen und fünf Monate lang eine echte.
Quellen
- Claude Code Documentation — headless mode,
ANTHROPIC_BASE_URL, model flags - CLIProxyAPI — the local multi-provider gateway behind the proxy rungs
- Gemini CLI — the last rung, and its trusted-folders behaviour
- Apple Developer — launchd jobs, used by the watchdog
Einen Fehler entdeckt?
Ein falscher Fakt, eine schiefe Übersetzung, etwas, das in diesem Artikel falsch klingt? Schreib mir — in deiner eigenen Sprache.