Skip to main content
Tilbake til bloggen
Claude CodeAICLIBashAutomationResilience

En kaskade av agenter: terminalen velger den som faktisk virker

Da Claude delvis gikk ned og noen modeller sluttet å svare, prøvde jeg å bytte til Gemini — og oppdaget at reserven min hadde vært ødelagt i månedsvis. Slik bygger du en som faktisk holder.

Publisert 29. august 20269 min lesing

TL;DR

En bash-starter kalt agent som velger den beste fungerende AI-koding-CLI-en før den starter. Den degraderer langs to akser: først modellen (Opus → Sonnet, fortsatt innenfor Anthropic), så transporten, og først deretter leverandøren. Hver probe gjør et ekte API-kall, fordi modellister lyver. En vakthund sjekker de nedre trinnene annenhver time og varsler på 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-low

Problemet

Når Claude er utilgjengelig, vil jeg at terminalen skal fortsette å virke. Den naive varianten er «hvis Claude er nede, kjør Gemini» — og det trodde jeg at jeg hadde. Men de interessante feilene er ikke nedetid. Den vanligste: Opus går tom mens Sonnet svarer helt fint. Å hoppe til en annen leverandør der er absurd.

En ekte reserve må altså degradere per modell før den degraderer per leverandør. Og den må vite hvilket trinn som faktisk lever — det viste seg å være den vanskelige delen.

Fem måneders løgn

Før jeg skrev noe, sjekket jeg det jeg allerede hadde. Proxyens auth-katalog fortalte hele historien i to tidsstempler:

claude-newiqa@gmail.com.json — fornyet i dag, 06:41 · gemini-newiqa@gmail.com-….json — sist berørt 27. mars

Claude-tokenet ble fornyet daglig. Gemini-tokenet hadde ikke rørt seg på fem måneder. Helsesjekken min så bare på claude-*.json, så ingen merket noe. Reserven hadde vært dekorativ siden våren — et grønt lys over et tomt rom. En reserve ingen faktisk kaller, er ingen reserve.

Hvorfor en gateway ikke løser dette

Det åpenbare trekket er en lokal LLM-gateway — Bifrost, LiteLLM — med leverandørbytte i konfigurasjonen. Jeg kjører allerede én (CLIProxyAPI), så en til hadde bare gitt enda en daemon å passe på.

Viktigere: en gateway feiler på selve oppgaven. Bytte på API-nivå betyr å bytte modell under samme klient. Gi Claude Code et Gemini-svar via et oversettelseslag, og den faller fra hverandre på tool-use og streaming. Reserven må bytte CLI, ikke modellen bak.

Seks trinn, to akser

Leverandør og modell svikter uavhengig, så stigen fletter dem. Hvert trinn gir noe bestemt, og prisen for å gå ned er uttalt:

RungWhat it survivesCost of getting there
claude + opusnothing — this is the happy path
claude + sonnetOpus capped or overloadedweaker model, same session shape
claude-proxy + opusstale local login, broken CLI authseparate OAuth token
claude-proxy + sonnetboth of the above at onceweaker model via proxy
agyAnthropic outage / exhausted plandifferent vendor, different CLI
geminiagy itself brokendifferent CLI again, shares agy's quota

I koden er et trinn en trippel: backend, modell å probe, modell å kjøre med. Den tomme kjøremodellen på øverste trinn betyr mer enn den ser ut: å ikke sende --model bevarer profilens opus[1m] og dens 1M-kontekst.

agent
# 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 trinn er utilgjengelig, annonseres nedstigningen i stedet for å skje stille — en degradert økt må aldri 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 å probe billig: spørre gatewayen om /v1/models. Det er verdiløst. Proxyen min annonserte muntert gemini-* hele tiden mens hvert kall ga auth_unavailable. Antigravity-CLI-en gjør det samme — agy models svarer fra en katalog.

Det samme gjelder modellhelse. Ingen liste-endepunkt forteller at Opus er ratebegrenset akkurat nå. Bare en ekte forespørsel viser det — en 429 eller 529:

agent
# 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
}

Modelltilgjengeligheten for de offisielle trinnene lånes fra proxyen, som fronter samme Anthropic-konto. Hele forhåndssjekken ble målt til 7 tokens og rundt 1,3 sekunder.

To sikkerhetsnett, ikke ett

En forhåndsprobe fanger ikke alt. Hvis kvoten tar slutt i mellomrommet mellom probe og start, hadde proben rett og var likevel ubrukelig. Derfor et nett nummer to: sluttkode pluss medgått tid.

agent
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"

Terskelen på 25 sekunder koder en vurdering. Et trinn som dør nesten umiddelbart, startet aldri egentlig. Et trinn som kjørte i minutter og så feilet, arbeidet faktisk, og å stille kjøre det arbeidet på nytt hos en annen leverandør ville vært verre enn å vise feilen.

Vakthunden og fellen i den

Starteren kjører bare når jeg kjører den, så et trinn kan råtne mellom økter. En sjekk innebygd i helsedroiden min (launchd, annenhver time, Telegram-varsler) kaller samme probekode. Første versjon hadde nøyaktig den buggen denne artikkelen handler om: den returnerte suksess på første grønne trinn.

# 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=1

Det er feil, fordi de to ikke-Anthropic-trinnene holder separate tokens — Antigravity-CLI-en sitt eget, Gemini-trinnet proxyens. Ett kan råtne mens det andre ser perfekt ut. Derfor prober sjekken alle trinn og behandler DEGRADED som en feil.

Tre feil verdt å skrive ned

Hver av dem ga grønt signal over ødelagt maskineri — samme feilmodus, tre ganger på én ettermiddag:

  • Proben testet en modell jeg aldri bruker. Jeg probet gemini-3.1-pro-preview mens CLI-en var konfigurert for gemini-3.1-pro-low. De løses til ulike leverandører, så jeg diagnostiserte et «utløpt token» som aldri fantes. En probe må gå nøyaktig den veien den ekte klienten tar.
  • En stille tom variabel. Å parse proxyens YAML-nøkkel med awk og \x27 ga tom streng under BSD awk — uten feil eller advarsel. Sjekken gikk likevel gjennom. Løst med awk -v q="'".
  • Proben gikk gjennom mens det ekte feilet. En curl-probe ga 200, men det ekte gemini -p nektet å starte: headless-modus blokkerer på ikke-betrodde kataloger. Trinnet var ødelagt nettopp i situasjonen det finnes for.
Mønsteret bak alle tre: en sjekk som ikke gjør den ekte tingen, blir til slutt en sjekk som lyver. Billige prober frister fordi de er raske og som regel stemmer med virkeligheten — helt frem til øyeblikket du trenger at de avviker.

Bruk

Å skrive agent gir beste tilgjengelige trinn med den vanlige Claude Code-opplevelsen. Eksplisitt valg hopper over probing 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 CLI

Konklusjoner

Ingeniørarbeidet her er umerkelig: et bash-skript, noen curl-kall, et ordnet array. Det som krevde innsats, var å mistro mine egne sjekker. Tre ganger bygde jeg noe som meldte suksess mens det under var ødelagt — og hver gang var den billige, raske proben synderen.

Robusthet er ikke listen over backends du konfigurerte. Det er om noe verifiserer at listen fortsatt stemmer. Min hadde seks trinn på papiret og ett ekte i fem måneder.

Kilder

Fant du en feil?

Et feil faktum, en skjev oversettelse, noe som virker usant i denne artikkelen? Skriv til meg — på ditt eget språk.