Skip to main content
Tilbage til bloggen
Claude CodeAICLIBashAutomationResilience

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.

Udgivet 29. august 20269 min læsning

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-low

Problemet

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:

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 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.

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 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:

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
}

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.

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"

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

Det 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 til gemini-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 awk og \x27 gav tom streng under BSD awk — uden fejl eller advarsel. Tjekket gik alligevel igennem. Løst med awk -v q="'".
  • Proben gik igennem, mens det rigtige fejlede. En curl-probe gav 200, men det rigtige gemini -p nægtede at starte: headless-tilstand blokerer på ikke-betroede mapper. Trinnet var ødelagt netop i den situation, det findes for.
Mønstret bag alle tre: et tjek, der ikke gør den rigtige ting, bliver før eller siden et tjek, der lyver. Billige prober frister, fordi de er hurtige og som regel stemmer med virkeligheden — lige indtil det øjeblik, hvor man har brug for, at de afviger.

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 CLI

Konklusioner

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

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.