Skip to main content
Назад к блогу
Claude CodeAICLIBashAutomationResilience

Каскад агентов: терминал сам выбирает того, кто сейчас работает

Когда Claude частично отказал и часть моделей перестала работать, я попробовал перейти на Gemini — и обнаружил, что мой фоллбэк давно сломан. Вот как сделать тот, который действительно выдержит.

Опубликовано 29 августа 2026 г.9 мин чтения

TL;DR

Bash-лаунчер agent, который перед стартом выбирает лучший работающий AI CLI. Он деградирует по двум осям: сначала модель (Opus → Sonnet, оставаясь внутри Anthropic), затем транспорт, и только потом вендор. Каждая проба делает реальный вызов, потому что списки моделей врут. Watchdog проверяет нижние ступени каждые два часа и шлёт алерт в 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-токен не двигался пять месяцев. Мой health-check смотрел только на claude-*.json, поэтому никто ничего не заметил. Фоллбэк был декоративным с весны — зелёная лампочка над пустой комнатой. Именно это наблюдение и сформировало всё дальнейшее: фоллбэк, который никто по-настоящему не вызывает, — не фоллбэк.

Почему gateway это не решает

Очевидный ход — локальный LLM-gateway вроде Bifrost или LiteLLM с failover в конфиге. Один у меня уже работает (CLIProxyAPI), так что второй дал бы лишь ещё одного демона, за которым нужно следить.

Важнее другое: gateway не решает саму задачу. Failover на уровне API означает подмену модели под тем же клиентом. Скорми Claude Code ответ Gemini через слой трансляции — и он рассыплется на tool-use и стриминге. Фоллбэк должен переключать CLI, а не модель под ним.

Шесть ступеней, две оси

Провайдер и модель отказывают независимо, поэтому лестница их чередует. Каждая ступень даёт что-то конкретное, и цена спуска названа явно:

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

В коде ступень — это тройка: бэкенд, модель для пробы, модель для запуска. Пустая модель запуска на верхней ступени важнее, чем кажется: не передавать --model значит сохранить opus[1m] из профиля и его 1M контекста. Явное имя модели тихо срезало бы это до обычного Opus при каждом запуске.

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

Когда верхняя ступень недоступна, спуск объявляется, а не происходит молча — деградированную сессию нельзя спутать с обычной:

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

Списки моделей врут

Первый инстинкт — пробовать дёшево: спросить у gateway /v1/models и проверить, что модель на месте. Это бесполезно. Мой прокси бодро отдавал gemini-* всё то время, пока каждый вызов к ним возвращал auth_unavailable. Antigravity CLI ведёт себя так же — agy models отвечает из каталога, а не из рабочей сессии.

То же со состоянием модели. Ни один эндпоинт со списком не скажет, что Opus сейчас под лимитом. Это показывает только реальный запрос — 429 или 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
}

Доступность моделей для официальных ступеней заимствуется у прокси, который фронтит тот же аккаунт Anthropic — исчерпанная там модель исчерпана и в официальном CLI. Весь preflight замерен на 7 токенов и около 1,3 секунды, причём результат Opus кешируется.

Две сетки, а не одна

Preflight ловит не всё. Если квота кончилась в промежутке между пробой и запуском, проба была права и всё равно бесполезна. Поэтому есть вторая сетка: код выхода плюс время работы.

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"

Порог в 25 секунд кодирует суждение. Ступень, умершая почти сразу, по-настоящему и не стартовала — плохая авторизация, исчерпанная модель — значит идти дальше безопасно. Ступень, проработавшая минуты и затем упавшая, действительно работала, и молча перегонять эту работу другому вендору было бы хуже, чем показать ошибку.

Watchdog и ловушка внутри него

Лаунчер работает только когда я его запускаю, поэтому ступень может сгнить между сессиями. Проверка, встроенная в мой health-дроид (launchd, каждые два часа, алерты в 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

Это неверно, потому что две не-Anthropic ступени держат разные токены — у Antigravity CLI свой, а Gemini-ступень пользуется проксёвым. Одна может сгнить, пока вторая выглядит идеально. Поэтому проверка проходит все ступени и считает DEGRADED тоже ошибкой: раннее предупреждение, пока запас ещё есть.

Три ошибки, которые стоит выписать

Каждая дала зелёный сигнал над сломанным механизмом — один и тот же тип отказа, трижды за день:

  • Проба проверяла модель, которой я не пользуюсь. Я пробовал gemini-3.1-pro-preview, тогда как CLI был настроен на gemini-3.1-pro-low. Они резолвятся в разных провайдеров, и я диагностировал «протухший токен», которого не существовало. Проба должна проходить тот же путь, что и реальный клиент.
  • Тихо пустая переменная. Разбор YAML-ключа прокси через awk и \x27 для кавычки давал пустую строку в BSD awk — без ошибки и предупреждения. Проверка всё равно проходила, потому что тестируемой ступени ключ был не нужен. Чинится передачей кавычки через awk -v q="'".
  • Проба проходила, а реальный запуск падал. curl-проба к Gemini-эндпоинту возвращала 200, но настоящий gemini -p отказывался стартовать: headless-режим блокируется на недоверенных каталогах. Ступень была сломана именно в той ситуации, ради которой существует.
Общий шаблон всех трёх: проверка, которая не делает настоящего дела, рано или поздно начнёт врать. Дешёвые пробы соблазнительны, потому что быстры и обычно совпадают с реальностью — ровно до момента, когда тебе нужно, чтобы они разошлись.

Как этим пользоваться

Набираешь agent — получаешь лучшую доступную ступень с привычным Claude Code. Явный выбор вообще пропускает пробы: попросил Gemini — получил 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

Выводы

Инженерия здесь заурядная: bash-скрипт, несколько curl-вызовов, упорядоченный массив. Усилий потребовало недоверие к собственным проверкам. Трижды я построил нечто, рапортующее об успехе, пока под ним всё было сломано.

Устойчивость — это не список настроенных бэкендов. Это наличие того, кто проверяет, что список всё ещё верен. У меня на бумаге было шесть ступеней и одна настоящая в течение пяти месяцев.

Источники

Заметили ошибку?

Неверный факт, кривой перевод, что-то звучит неправдой в этой статье? Напишите мне — на своём языке.