Каскад агентів: термінал сам обирає того, хто зараз працює
Коли Claude частково відмовив і частина моделей перестала працювати, я спробував перейти на Gemini — і виявив, що мій фоллбек давно зламаний. Ось як зробити такий, що справді витримає.
TL;DR
Скрипт agent, який перед запуском сам знаходить робочий AI-агент. Спускається він двома сходинками: спершу міняє модель (Opus → Sonnet, залишаючись у Claude), потім спосіб підключення, і аж наостанок — іншого виробника. Перед кожним запуском робить справжній запит, бо списки моделей брешуть. А раз на дві години перевіряє запасні варіанти й пише в 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Проблема
Коли Claude не працює, я хочу, щоб термінал працював далі. Найпростіше рішення — «Claude ліг, запускай Gemini», і я думав, що саме так у мене й зроблено. Але найчастіше ламається не все одразу. Найчастіший випадок такий: Opus вичерпав ліміт, а Sonnet відповідає нормально. Тікати до іншого виробника тут безглуздо — модель поруч жива.
Отже, спершу треба міняти модель і лише потім — виробника. І треба знати, що саме зараз живе. Це виявилось найважчим, і тут я двічі помилився.
П'ять місяців брехні
Перш ніж щось писати, я подивився, що вже є. Дві дати з папки, де лежать ключі доступу, пояснили все:
claude-newiqa@gmail.com.json — оновлено сьогодні, 06:41 · gemini-newiqa@gmail.com-….json — востаннє 27 березня
Ключ до Claude оновлювався щодня. Ключ до Gemini не змінювався п'ять місяців. Моя перевірка дивилась тільки на файл Claude, тому ніхто нічого не помітив. Запасний варіант був іграшковим ще з весни — зелена лампочка над порожньою кімнатою. Саме звідси виросло все інше: якщо запасний варіант ніхто не пробує насправді, це не запасний варіант.
Чому проксі цього не вирішує
Перше, що спадає на думку, — поставити локальний проксі на кшталт Bifrost чи LiteLLM і прописати в ньому перемикання між виробниками. Один такий у мене вже працює (CLIProxyAPI), тож другий додав би тільки ще одну програму, за якою треба стежити.
Але важливіше інше: проксі не робить головного. Він уміє підмінити модель під тією самою програмою. А якщо дати Claude Code відповідь від Gemini через перекладач форматів, він зламається на інструментах і потоковому виводі. Міняти треба саму програму, а не модель під нею. Це робота запускача, а не проксі.
Шість рівнів, два напрямки
Виробник і модель ламаються окремо один від одного, тому в списку вони чергуються. Кожен рівень рятує від чогось свого, і за кожен крок униз є своя плата:
| 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 |
У коді один рівень — це три речі: чим запускати, яку модель перевіряти і з якою моделлю працювати. Порожнє поле моделі на першому рівні важливіше, ніж здається: якщо не вказати --model, залишиться opus[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||"
)Коли перший рівень недоступний, програма про це каже вголос. Урізану сесію не можна сплутати зі звичайною:
$ 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.Списки моделей брешуть
Найперше бажання — перевіряти дешево: спитати в проксі список моделей і подивитись, чи потрібна там є. Так робити не можна. Мій проксі бадьоро показував моделі Gemini весь той час, поки кожне звернення до них поверталося з помилкою доступу. Antigravity CLI поводиться так само: команда agy models читає довідник, а не питає живий сервер.
Те саме з моделями. Жоден список не скаже, що Opus просто зараз упирається в ліміт. Це видно тільки зі справжнього запиту — коли приходить помилка 429 або 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
}Чи жива модель, я питаю в проксі: він працює з тим самим акаунтом Anthropic, тож вичерпана там модель вичерпана і в звичайному Claude Code. Уся перевірка коштує 7 токенів і приблизно 1,3 секунди: безкоштовний запит без ключа, який має повернути помилку 401, плюс один запит на один токен. Відповідь про Opus запам'ятовується, тож два рівні обходяться однією перевіркою.
Дві сітки замість однієї
Перевірка перед запуском ловить не все. Якщо ліміт скінчився саме між перевіркою і запуском, вона була права — і все одно не допомогла. Тому є друга сітка: код помилки плюс час роботи.
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"За порогом у 25 секунд стоїть проста думка. Якщо програма впала майже одразу, вона й не почала працювати — зламаний доступ, вичерпана модель — отже можна спокійно брати наступну. А якщо вона пропрацювала кілька хвилин і аж тоді впала, то робота таки йшла. Мовчки перенести її до іншого виробника було б гірше, ніж просто показати помилку.
Сторож і пастка всередині нього
Запускач працює тільки тоді, коли я його запускаю, тож між сесіями будь-який рівень може тихо зламатись. Тому перевірку вбудовано в мого домашнього сторожа (він оживає кожні дві години й пише в Telegram) — код там той самий. І в першій версії був рівно той баг, про який уся ця стаття: сторож казав «усе добре», щойно бачив перший живий рівень.
# 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Так не можна, бо два запасні рівні тримають різні ключі: в Antigravity CLI свій, а Gemini ходить через проксі. Один може зламатись, поки другий виглядає чудово. Зупинка на першому «все добре» сховала б це так само, як ховалась та п'ятимісячна поломка. Тому тепер перевіряються всі рівні, і навіть слово DEGRADED вважається помилкою: краще дізнатись раніше, поки запас іще є.
Три помилки, які варто запам'ятати
Кожна з них показувала зелене світло над зламаним механізмом. Одна й та сама помилка, тричі за день:
- Перевіряв модель, якою не користуюсь. Я перевіряв
gemini-3.1-pro-preview, хоча програма була налаштована наgemini-3.1-pro-low. Це різні моделі й різні постачальники, тому я вирішив, що «протух ключ», хоча такого не було: просто один із постачальників був не підключений. Перевіряти треба той самий шлях, яким ходить справжня програма. - Порожнє значення без жодної помилки. Я діставав ключ із файлу налаштувань командою
awk, і в macOS вона мовчки повертала порожній рядок — ні помилки, ні попередження. Перевірка все одно проходила, бо тому рівню, який вона перевіряла, ключ був не потрібен. - Перевірка проходила, а запуск падав. Простий запит на адресу Gemini повертав «усе добре», але справжня команда
gemini -pне стартувала: без вікна вона відмовляється працювати в чужій папці. Рівень був зламаний саме в тій ситуації, заради якої існує, і побачити це можна було лише запустивши програму по-справжньому.
Як цим користуватись
Пишеш agent — і отримуєш найкращий доступний варіант зі звичним Claude Code. Якщо назвати програму прямо, перевірка взагалі пропускається: попросив Gemini — отримав Gemini, без суперечок про те, чому Claude кращий:
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Висновки
Нічого складного тут немає: скрипт на bash, кілька запитів, список у потрібному порядку. Найважче було перестати вірити власним перевіркам. Тричі я зробив щось, що бадьоро звітувало про успіх, поки під ним усе було зламане, і щоразу винною була дешева й швидка перевірка.
Надійність — це не список запасних варіантів, які ти колись налаштував. Це те, чи перевіряє хтось, що список іще правдивий. У мене на папері було шість рівнів, а насправді п'ять місяців працював один — і побачити різницю могла тільки перевірка, яка робить справжній запит.
Джерела
- 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
Помітили помилку?
Невірний факт, кривий переклад, щось звучить неправдиво в цій статті? Напишіть мені — своєю мовою.