Una cascada de agentes: la terminal elige el que está vivo
Cuando Claude falló en parte y algunos modelos dejaron de responder, intenté pasarme a Gemini — y descubrí que mi respaldo llevaba meses roto. Así se construye uno que de verdad aguante.
TL;DR
Un lanzador en bash llamado agent que elige la mejor CLI de IA disponible antes de arrancar. Degrada en dos ejes: primero el modelo (Opus → Sonnet, sin salir de Anthropic), luego el transporte y solo al final el proveedor. Cada sonda hace una llamada real, porque las listas de modelos mienten. Un vigilante revisa los peldaños inferiores cada dos horas y avisa por 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-lowEl problema
Cuando Claude no está disponible quiero que mi terminal siga funcionando. La versión ingenua es «si Claude cae, ejecuta Gemini», y eso creía tener. Pero los fallos interesantes no son caídas. El más común: Opus se agota mientras Sonnet responde bien. Saltar a otro proveedor ahí es absurdo: el modelo de al lado está sano.
Así que un respaldo real debe degradar por modelo antes que por proveedor. Y debe saber qué peldaño está vivo — que resultó ser la parte difícil, y donde me equivoqué dos veces.
La mentira de cinco meses
Antes de escribir nada revisé lo que ya tenía. El directorio de autenticación del proxy contó la historia entera en dos fechas:
claude-newiqa@gmail.com.json — renovado hoy, 06:41 · gemini-newiqa@gmail.com-….json — última vez el 27 de marzo
El token de Claude se renovaba a diario. El de Gemini llevaba cinco meses inmóvil. Mi comprobación de salud solo miraba claude-*.json, así que nadie se enteró. El respaldo era decorativo desde la primavera: una luz verde sobre una habitación vacía. Esa sola observación definió todo lo demás: un respaldo que nadie llama de verdad no es un respaldo.
Por qué un gateway no lo resuelve
El movimiento obvio es un gateway LLM local — Bifrost, LiteLLM — con failover en la configuración. Ya ejecuto uno (CLIProxyAPI), así que añadir otro solo habría traído un segundo demonio que cuidar.
Más importante: un gateway falla en la tarea real. El failover en la capa de API significa cambiar el modelo bajo un mismo cliente. Dale a Claude Code una respuesta de Gemini mediante traducción y se rompe en tool-use y streaming. El respaldo debe cambiar la CLI, no el modelo detrás.
Seis peldaños, dos ejes
Proveedor y modelo fallan de forma independiente, así que la escalera los intercala. Cada peldaño aporta algo concreto y el coste de bajar está explícito:
| 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 |
En código un peldaño es una tripleta: backend, modelo a sondear, modelo con el que ejecutar. El modelo vacío del peldaño superior importa más de lo que parece: no pasar --model preserva el opus[1m] del perfil y su contexto de 1M. Nombrarlo explícitamente lo recortaría en silencio a Opus normal.
# 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||"
)Cuando el peldaño superior no está disponible, la bajada se anuncia en vez de ocurrir en silencio: una sesión degradada nunca debe confundirse con 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.Las listas de modelos mienten
El primer instinto es sondear barato: pedir /v1/models al gateway y comprobar que el modelo está. No sirve de nada. Mi proxy anunciaba gemini-* todo el tiempo que cada llamada devolvía auth_unavailable. La CLI de Antigravity hace lo mismo: agy models responde desde un catálogo, no desde una sesión funcional.
Lo mismo con la salud del modelo. Ningún endpoint de listado dirá que Opus está limitado ahora mismo. Solo lo muestra una petición real — un 429 o un 529. Por eso cada sonda hace una:
# 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
}La disponibilidad por modelo de los peldaños oficiales se toma prestada del proxy, que da a la misma cuenta de Anthropic. Todo el preflight se midió en 7 tokens y unos 1,3 segundos, con el resultado de Opus cacheado.
Dos redes, no una
Una sonda previa no lo atrapa todo. Si la cuota se acaba entre la sonda y el arranque, la sonda tenía razón y aun así fue inútil. Por eso hay una segunda red: código de salida más tiempo transcurrido.
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"El umbral de 25 segundos codifica un juicio. Un peldaño que muere casi al instante nunca arrancó de verdad, así que seguir es seguro. Uno que funcionó minutos y luego falló estaba trabajando de verdad, y repetir ese trabajo en otro proveedor sería peor que mostrar el error.
El vigilante y la trampa que llevaba dentro
El lanzador solo corre cuando yo lo lanzo, así que un peldaño puede pudrirse entre sesiones. Una comprobación integrada en mi droide de salud (launchd, cada dos horas, alertas de Telegram) llama al mismo código. Mi primera versión tenía justo el bug del que trata este artículo: devolvía éxito en el primer peldaño 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=1Es incorrecto, porque los dos peldaños no-Anthropic tienen tokens separados — la CLI de Antigravity el suyo, y el peldaño Gemini el del proxy. Uno puede pudrirse mientras el otro luce perfecto. Por eso la comprobación sondea todos y trata DEGRADED también como fallo.
Tres errores que conviene anotar
Cada uno produjo una señal verde sobre maquinaria rota: el mismo modo de fallo, tres veces en una tarde:
- La sonda probaba un modelo que nunca uso. Sondeaba
gemini-3.1-pro-previewmientras la CLI estaba configurada congemini-3.1-pro-low. Se resuelven a proveedores distintos, así que diagnostiqué un «token caducado» inexistente. Una sonda debe recorrer exactamente el camino del cliente real. - Una variable vacía en silencio. Parsear la clave YAML del proxy con
awky\x27daba cadena vacía en BSD awk — sin error ni aviso. La comprobación pasaba igual, porque el peldaño probado no necesitaba clave. Se arregla conawk -v q="'". - La sonda pasaba mientras lo real fallaba. Una sonda
curldevolvía 200, pero elgemini -preal se negaba a arrancar: el modo headless bloquea en directorios no confiables. El peldaño estaba roto justo en la situación para la que existe.
Cómo se usa
Escribir agent da el mejor peldaño disponible con la experiencia habitual de Claude Code. La selección explícita se salta el sondeo: si pides Gemini, obtienes 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 CLIConclusiones
La ingeniería aquí es corriente: un script bash, unas llamadas curl, un array ordenado. Lo costoso fue desconfiar de mis propias comprobaciones. Tres veces construí algo que informaba de éxito mientras lo de debajo estaba roto.
La resiliencia no es la lista de backends que configuraste. Es si algo verifica que esa lista sigue siendo cierta. La mía tenía seis peldaños sobre el papel y uno real durante cinco meses.
Fuentes
- 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
¿Ves un error?
¿Un dato incorrecto, una traducción torpe, algo que suena falso en este artículo? Escríbeme — en tu propio idioma.