En kaskad av agenter: terminalen väljer den som faktiskt lever
När Claude delvis låg nere och vissa modeller slutade svara försökte jag byta till Gemini — och upptäckte att min reserv varit trasig i månader. Så här bygger man en som faktiskt håller.
TL;DR
En bash-startare kallad agent som väljer den bästa fungerande AI-kodnings-CLI:n innan den startar. Den degraderar längs två axlar: först modellen (Opus → Sonnet, kvar inom Anthropic), sedan transporten och först därefter leverantören. Varje sond gör ett riktigt API-anrop, eftersom modellistor ljuger. En vakthund kontrollerar de nedre stegen varannan timme och larmar via Telegram.
$ 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-lowProblemet
När Claude är otillgänglig vill jag att min terminal fortsätter fungera. Den naiva varianten är ”om Claude ligger nere, kör Gemini” — och det trodde jag att jag hade. Men de intressanta felen är inte avbrott. Det vanligaste: Opus tar slut medan Sonnet svarar utmärkt. Att hoppa till en annan leverantör där är absurt.
Ett riktigt reservläge måste alltså degradera per modell innan det degraderar per leverantör. Och det måste veta vilket steg som faktiskt lever — vilket visade sig vara den svåra delen.
Fem månaders lögn
Innan jag skrev något kontrollerade jag vad jag redan hade. Proxyns auth-katalog berättade hela historien i två tidsstämplar:
claude-newiqa@gmail.com.json — förnyad idag, 06:41 · gemini-newiqa@gmail.com-….json — senast 27 mars
Claude-token förnyades dagligen. Gemini-token hade inte rört sig på fem månader. Min hälsokontroll tittade bara på claude-*.json, så ingen märkte något. Reserven hade varit dekorativ sedan våren — en grön lampa över ett tomt rum. Den enda observationen formade allt nedan: en reserv som ingen faktiskt anropar är ingen reserv.
Varför en gateway inte löser detta
Det uppenbara draget är en lokal LLM-gateway — Bifrost, LiteLLM — med leverantörsväxling i konfigurationen. Jag kör redan en (CLIProxyAPI), så en till hade bara gett ännu en daemon att sköta.
Viktigare: en gateway misslyckas med själva uppgiften. Växling på API-nivå betyder att byta modell under samma klient. Ge Claude Code ett Gemini-svar via ett översättningslager och den faller isär på tool-use och streaming. Reserven måste byta CLI, inte modellen bakom.
Sex steg, två axlar
Leverantör och modell fallerar oberoende, så stegen varvas. Varje steg ger något specifikt, och priset för att kliva ner är uttalat:
| 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 |
I koden är ett steg en trippel: backend, modell att sondera, modell att köra med. Den tomma körmodellen på översta steget betyder mer än den ser ut: att inte skicka --model bevarar profilens opus[1m] och dess 1M-kontext.
# 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||"
)När det översta steget saknas annonseras nedstigningen i stället för att ske tyst — en degraderad session får aldrig misstas för en normal:
$ 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.Modellistor ljuger
Första instinkten är att sondera billigt: fråga gatewayen efter /v1/models. Det är värdelöst. Min proxy annonserade glatt gemini-* hela tiden medan varje anrop gav auth_unavailable. Antigravity-CLI:n gör detsamma — agy models svarar från en katalog.
Detsamma gäller modellhälsa. Ingen listningsendpoint berättar att Opus är hastighetsbegränsad just nu. Bara en riktig förfrågan visar det — en 429 eller 529:
# 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
}Modelltillgängligheten för de officiella stegen lånas från proxyn, som fronter samma Anthropic-konto. Hela förkontrollen mättes till 7 tokens och cirka 1,3 sekunder, med Opus-resultatet cachat.
Två skyddsnät, inte ett
En förkontroll fångar inte allt. Om kvoten tar slut i glappet mellan sond och start hade sonden rätt och var ändå värdelös. Därför finns ett andra nät: slutkod plus förfluten tid.
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"Tröskeln på 25 sekunder kodar ett omdöme. Ett steg som dör nästan omedelbart startade aldrig på riktigt. Ett steg som körde i minuter och sedan misslyckades arbetade faktiskt, och att tyst köra om det arbetet hos en annan leverantör vore sämre än att visa felet.
Vakthunden och fällan i den
Startaren körs bara när jag kör den, så ett steg kan ruttna mellan sessioner. En kontroll inbyggd i min hälsodroid (launchd, varannan timme, Telegram-larm) anropar samma sondkod. Min första version hade precis den bugg artikeln handlar om: den returnerade framgång vid första gröna steget.
# 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=1Det är fel, eftersom de två icke-Anthropic-stegen håller separata tokens — Antigravity-CLI:n sin egen, Gemini-steget proxyns. Ett kan ruttna medan det andra ser perfekt ut. Därför sonderar kontrollen alla steg och behandlar DEGRADED som ett fel.
Tre misstag värda att skriva ner
Vart och ett gav grön signal över trasigt maskineri — samma feltyp, tre gånger på en eftermiddag:
- Sonden testade en modell jag aldrig använder. Jag sonderade
gemini-3.1-pro-previewmedan CLI:n var konfigurerad förgemini-3.1-pro-low. De löses till olika leverantörer, så jag diagnostiserade en ”utgången token” som aldrig fanns. En sond måste gå exakt den väg den riktiga klienten tar. - En tyst tom variabel. Att tolka proxyns YAML-nyckel med
awkoch\x27gav tom sträng under BSD awk — utan fel eller varning. Kontrollen gick ändå igenom. Löstes medawk -v q="'". - Sonden gick igenom medan det riktiga föll. En
curl-sond gav 200, men det riktigagemini -pvägrade starta: headless-läget blockerar på icke betrodda kataloger. Steget var trasigt precis i den situation det finns för.
Användning
Att skriva agent ger bästa tillgängliga steg med den vanliga Claude Code-upplevelsen. Explicit val hoppar över sondering helt — ber du om Gemini får du 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 CLISlutsatser
Tekniken här är oanmärkningsvärd: ett bash-skript, några curl-anrop, en ordnad array. Det som krävde arbete var misstron mot mina egna kontroller. Tre gånger byggde jag något som rapporterade framgång medan det undertill var trasigt — och varje gång var den billiga, snabba sonden boven.
Motståndskraft är inte listan av backends du konfigurerat. Det är huruvida något verifierar att listan fortfarande stämmer. Min hade sex steg på pappret och ett riktigt i fem månader.
Källor
- 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
Hittade du ett fel?
Ett felaktigt faktum, en skev översättning, något som känns falskt i den här artikeln? Skriv till mig — på ditt eget språk.