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ă.
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-lowProblema
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:
| 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 |
Î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.
# 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:
# 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.
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=1E 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 pegemini-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\x27dădea șir gol pe BSD awk — fără eroare, fără avertisment. Verificarea trecea oricum. Rezolvat cuawk -v q="'". - Sonda trecea în timp ce lucrul real eșua. O sondă
curlreturna 200, dar adevăratulgemini -prefuza să pornească: modul headless se blochează pe directoare neîncredere. Treapta era stricată exact în situația pentru care există.
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 CLIConcluzii
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
- 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
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.