En kaskade af agenter: terminalen vælger den, der faktisk virker
Da Claude delvist gik ned, og nogle modeller holdt op med at svare, forsøgte jeg at skifte til Gemini — og opdagede, at min reserve havde været i stykker i månedsvis. Sådan bygger du en, der faktisk holder.
TL;DR
En bash-starter kaldet agent, der vælger den bedste fungerende AI-kodnings-CLI, før den starter. Den degraderer langs to akser: først modellen (Opus → Sonnet, stadig inden for Anthropic), så transporten og først derefter udbyderen. Hver probe laver et rigtigt API-kald, fordi modellister lyver. En vagthund tjekker de nederste trin hver anden time og alarmerer 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 er utilgængelig, vil jeg have, at min terminal bliver ved med at virke. Den naive version er »hvis Claude er nede, kør Gemini« — og det troede jeg, jeg havde. Men de interessante fejl er ikke nedbrud. Den mest almindelige: Opus løber tør, mens Sonnet svarer fint. At springe til en anden udbyder dér er absurd.
En rigtig reserve skal altså degradere per model, før den degraderer per udbyder. Og den skal vide, hvilket trin der faktisk lever — det viste sig at være den svære del.
Fem måneders løgn
Før jeg skrev noget, tjekkede jeg det, jeg allerede havde. Proxyens auth-mappe fortalte hele historien i to tidsstempler:
claude-newiqa@gmail.com.json — fornyet i dag, 06:41 · gemini-newiqa@gmail.com-….json — sidst rørt 27. marts
Claude-tokenet blev fornyet dagligt. Gemini-tokenet havde ikke rørt sig i fem måneder. Mit helbredstjek kiggede kun på claude-*.json, så ingen bemærkede noget. Reserven havde været dekorativ siden foråret — et grønt lys over et tomt rum. En reserve, som ingen faktisk kalder, er ingen reserve.
Hvorfor en gateway ikke løser dette
Det oplagte træk er en lokal LLM-gateway — Bifrost, LiteLLM — med udbyderskift i konfigurationen. Jeg kører allerede en (CLIProxyAPI), så en mere ville kun have givet endnu en daemon at passe.
Vigtigere: en gateway fejler på selve opgaven. Skift på API-niveau betyder at udskifte modellen under samme klient. Giv Claude Code et Gemini-svar gennem et oversættelseslag, og den falder fra hinanden på tool-use og streaming. Reserven skal skifte CLI, ikke modellen bagved.
Seks trin, to akser
Udbyder og model fejler uafhængigt, så stigen fletter dem. Hvert trin giver noget bestemt, og prisen for at gå ned er udtalt:
| 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 er et trin en tripel: backend, model at probe, model at køre med. Den tomme køremodel på øverste trin betyder mere, end den ser ud: ikke at sende --model bevarer profilens opus[1m] og dens 1M-kontekst.
# 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 øverste trin er utilgængeligt, annonceres nedstigningen i stedet for at ske i stilhed — en degraderet session må aldrig forveksles med 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.Modellister lyver
Første instinkt er at probe billigt: spørge gatewayen om /v1/models. Det er værdiløst. Min proxy annoncerede muntert gemini-* hele tiden, mens hvert kald gav auth_unavailable. Antigravity-CLI'en gør det samme — agy models svarer fra et katalog.
Det samme gælder modelhelbred. Intet liste-endpoint fortæller, at Opus er ratebegrænset lige nu. Kun en rigtig forespørgsel viser 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
}Modeltilgængeligheden for de officielle trin lånes fra proxyen, som fronter samme Anthropic-konto. Hele forhåndstjekket blev målt til 7 tokens og cirka 1,3 sekunder.
To sikkerhedsnet, ikke ét
En forhåndsprobe fanger ikke alt. Hvis kvoten løber tør i mellemrummet mellem probe og start, havde proben ret og var alligevel ubrugelig. Derfor et net nummer to: exitkode plus forløbet 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"Tærsklen på 25 sekunder koder en vurdering. Et trin, der dør næsten øjeblikkeligt, startede aldrig rigtigt. Et trin, der kørte i minutter og så fejlede, arbejdede faktisk, og stiltiende at køre det arbejde igen hos en anden udbyder ville være værre end at vise fejlen.
Vagthunden og fælden i den
Starteren kører kun, når jeg kører den, så et trin kan rådne mellem sessioner. Et tjek indbygget i min helbredsdroide (launchd, hver anden time, Telegram-alarmer) kalder samme probekode. Min første version havde præcis den fejl, denne artikel handler om: den returnerede succes ved første grønne trin.
# 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 er forkert, fordi de to ikke-Anthropic-trin holder separate tokens — Antigravity-CLI'en sit eget, Gemini-trinnet proxyens. Ét kan rådne, mens det andet ser perfekt ud. Derfor prober tjekket alle trin og behandler DEGRADED som en fejl.
Tre fejl værd at skrive ned
Hver af dem gav grønt signal over ødelagt maskineri — samme fejltype, tre gange på én eftermiddag:
- Proben testede en model, jeg aldrig bruger. Jeg probede
gemini-3.1-pro-preview, mens CLI'en var konfigureret tilgemini-3.1-pro-low. De opløses til forskellige udbydere, så jeg diagnosticerede et »udløbet token«, der aldrig fandtes. En probe skal gå præcis den vej, den rigtige klient tager. - En stille tom variabel. At parse proxyens YAML-nøgle med
awkog\x27gav tom streng under BSD awk — uden fejl eller advarsel. Tjekket gik alligevel igennem. Løst medawk -v q="'". - Proben gik igennem, mens det rigtige fejlede. En
curl-probe gav 200, men det rigtigegemini -pnægtede at starte: headless-tilstand blokerer på ikke-betroede mapper. Trinnet var ødelagt netop i den situation, det findes for.
Brug
At skrive agent giver det bedste tilgængelige trin med den vante Claude Code-oplevelse. Eksplicit valg springer probing helt over — beder 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 CLIKonklusioner
Ingeniørarbejdet her er ubemærkelsesværdigt: et bash-script, nogle curl-kald, et ordnet array. Det, der krævede indsats, var mistilliden til mine egne tjek. Tre gange byggede jeg noget, der meldte succes, mens det underliggende var i stykker — og hver gang var den billige, hurtige probe skyldig.
Robusthed er ikke listen over backends, du konfigurerede. Det er, om noget verificerer, at listen stadig passer. Min havde seks trin på papiret og ét rigtigt i fem måneder.
Kilder
- 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
Fandt du en fejl?
Et forkert faktum, en skæv oversættelse, noget der virker usandt i denne artikel? Skriv til mig — på dit eget sprog.