Skip to main content
Înapoi la blog
Claude CodeAICLIBashAutomationResilience

O cascadă de agenți: terminalul îl alege pe cel care funcționează

Când Claude a căzut parțial și unele modele au încetat să răspundă, am încercat să trec pe Gemini — și am descoperit că rezerva mea era stricată de luni de zile. Iată cum construiești una care chiar rezistă.

Publicat 29 august 20269 min de citit

TL;DR

Un lansator bash numit agent care alege cel mai bun CLI de AI funcțional înainte de pornire. Degradează pe două axe: întâi modelul (Opus → Sonnet, rămânând în Anthropic), apoi transportul și abia la final furnizorul. Fiecare sondă face un apel real, pentru că listele de modele mint. Un câine de pază verifică treptele de jos la fiecare două ore și alertează pe 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

Problema

Când Claude nu e disponibil, vreau ca terminalul meu să funcționeze în continuare. Varianta naivă este „dacă pică Claude, rulează Gemini” — și credeam că exact asta am. Dar defecțiunile interesante nu sunt căderile. Cea mai frecventă: Opus se epuizează în timp ce Sonnet răspunde perfect. Să sari la alt furnizor acolo e absurd.

Deci o rezervă reală trebuie să degradeze după model înainte de a degrada după furnizor. Și trebuie să știe care treaptă e cu adevărat vie — partea dificilă, unde am greșit de două ori.

Minciuna de cinci luni

Înainte să scriu ceva, am verificat ce aveam deja. Directorul de autentificare al proxy-ului a spus toată povestea în două marcaje de timp:

claude-newiqa@gmail.com.json — reînnoit azi, 06:41 · gemini-newiqa@gmail.com-….json — ultima atingere 27 martie

Tokenul Claude se reînnoia zilnic. Cel Gemini nu se mișcase de cinci luni. Verificarea mea de sănătate se uita doar la claude-*.json, așa că nimeni nu a observat. Rezerva era decorativă din primăvară — un bec verde deasupra unei camere goale. O rezervă pe care nimeni nu o apelează cu adevărat nu este o rezervă.

De ce un gateway nu rezolvă asta

Mișcarea evidentă e un gateway LLM local — Bifrost, LiteLLM — cu comutare de furnizor în configurație. Rulez deja unul (CLIProxyAPI), deci încă unul ar fi adus doar un al doilea demon de îngrijit.

Mai important, un gateway eșuează la sarcina reală. Comutarea la nivel de API înseamnă schimbarea modelului sub același client. Dă-i lui Claude Code un răspuns Gemini printr-un strat de traducere și se destramă la tool-use și streaming. Rezerva trebuie să schimbe CLI-ul, nu modelul din spate.

Șase trepte, două axe

Furnizorul și modelul cedează independent, așa că scara le intercalează. Fiecare treaptă aduce ceva anume, iar costul coborârii este explicit:

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

În cod, o treaptă e un triplet: backend, modelul de sondat, modelul de rulare. Modelul gol de pe treapta de sus contează mai mult decât pare: a nu trimite --model păstrează opus[1m] din profil și contextul său de 1M.

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

Când treapta de sus nu e disponibilă, coborârea este anunțată în loc să fie tăcută — o sesiune degradată nu trebuie confundată niciodată cu una 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.

Listele de modele mint

Primul instinct e să sondezi ieftin: ceri gateway-ului /v1/models și verifici că modelul e acolo. Nu valorează nimic. Proxy-ul meu a afișat vesel gemini-* tot timpul în care fiecare apel returna auth_unavailable. CLI-ul Antigravity face la fel — agy models răspunde dintr-un catalog.

La fel și pentru starea modelului. Niciun endpoint de listare nu îți spune că Opus e limitat chiar acum. Doar o cerere reală arată asta — un 429 sau un 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
}

Disponibilitatea pe model pentru treptele oficiale e împrumutată de la proxy, care deservește același cont Anthropic. Întregul preflight a fost măsurat la 7 tokeni și circa 1,3 secunde.

Două plase, nu una

O sondă preliminară nu prinde totul. Dacă cota se epuizează între sondă și lansare, sonda avea dreptate și tot a fost inutilă. De aceea există o a doua plasă: codul de ieșire plus timpul scurs.

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"

Pragul de 25 de secunde codifică o judecată. O treaptă care moare aproape instantaneu nu a pornit cu adevărat. Una care a rulat minute și apoi a eșuat chiar lucra, iar reluarea tăcută a acelei munci la alt furnizor ar fi mai rea decât afișarea erorii.

Câinele de pază și capcana din el

Lansatorul rulează doar când îl rulez eu, deci o treaptă poate putrezi între sesiuni. O verificare integrată în droidul meu de sănătate (launchd, la două ore, alerte Telegram) apelează același cod de sondare. Prima versiune avea exact bug-ul despre care e acest articol: returna succes la prima treaptă verde.

# 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

E greșit, pentru că cele două trepte non-Anthropic dețin tokeni separați — CLI-ul Antigravity pe al său, treapta Gemini pe cel al proxy-ului. Una poate putrezi în timp ce cealaltă arată perfect. De aceea verificarea sondează fiecare treaptă și tratează DEGRADED tot ca eșec.

Trei greșeli de notat

Fiecare a produs un semnal verde peste o mecanică stricată — același mod de eșec, de trei ori într-o după-amiază:

  • Sonda testa un model pe care nu îl folosesc niciodată. Sondam gemini-3.1-pro-preview în timp ce CLI-ul era configurat pe gemini-3.1-pro-low. Se rezolvă la furnizori diferiți, așa că am diagnosticat un „token expirat” inexistent. O sondă trebuie să parcurgă exact calea clientului real.
  • O variabilă goală în tăcere. Parsarea cheii YAML a proxy-ului cu awk și \x27 dădea șir gol pe BSD awk — fără eroare, fără avertisment. Verificarea trecea oricum. Rezolvat cu awk -v q="'".
  • Sonda trecea în timp ce lucrul real eșua. O sondă curl returna 200, dar adevăratul gemini -p refuza să pornească: modul headless se blochează pe directoare neîncredere. Treapta era stricată exact în situația pentru care există.
Tiparul din spatele tuturor: o verificare care nu face lucrul real va deveni în cele din urmă o verificare care minte. Sondele ieftine sunt seducătoare pentru că sunt rapide și de obicei coincid cu realitatea — exact până în clipa în care ai nevoie să difere.

Utilizare

Tastând agent obții cea mai bună treaptă disponibilă cu experiența obișnuită Claude Code. Selecția explicită sare complet peste sondare — dacă ceri Gemini, primești 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

Concluzii

Ingineria de aici e banală: un script bash, câteva apeluri curl, un array ordonat. Efortul a stat în neîncrederea față de propriile verificări. De trei ori am construit ceva care raporta succes în timp ce dedesubt totul era stricat — și de fiecare dată vinovată a fost sonda ieftină și rapidă.

Reziliența nu e lista de backend-uri configurate. E dacă ceva verifică că lista mai e adevărată. A mea avea șase trepte pe hârtie și una reală timp de cinci luni.

Surse

Ai găsit o greșeală?

Un fapt greșit, o traducere stângace, ceva ce sună fals în acest articol? Scrie-mi — în limba ta.