Claude Code без Anthropic: заводим агента на GLM-5.3 и Qwen через свой шлюз и собираем грабли

Из Беларуси Claude Code официально не работает: Anthropic не обслуживает регион, а белорусской картой не оплатить ни подписку, ни API. Мы делаем ZuBrIQ — шлюз к моделям с оплатой в BYN — и за последние месяцы заставили Claude Code, Codex и OpenCode работать на открытых моделях — GLM-5.3 у провайдера и Qwen3.8-27B на собственных V100. Ниже — схема и пять граблей, на каждую из которых мы наступили сами.
Зачем вообще подменять модель под Claude Code
Claude Code — хороший агент, и он умеет ходить не только к Anthropic. Достаточно ANTHROPIC_BASE_URL и токена — и он шлёт запросы по протоколу Anthropic Messages куда угодно. Про это недавно был хороший обзор на Хабре — «Чем заменить Claude или Gemini после блокировки». Там подменяли модель на DeepSeek и MiniMax.
У нас задача шире. Нужны не один клиент и не одна модель, а десяток клиентов и несколько моделей за одним адресом и одним ключом. Плюс учёт расходов, резервные маршруты и модель, данные которой не покидают страну. Поэтому между агентами и моделями стоит шлюз на LiteLLM.
Схема
Claude Code ── /v1/messages ─────────┐
Codex ──────── /v1/responses ────────┤
OpenCode, Zed, Cursor, SDK ──────────┤
/v1/chat/completions │
▼
┌───────────────────────┐
│ LiteLLM (2 пода) │
│ ключи, бюджеты, логи │
│ имя модели → маршруты │
└───────────┬───────────┘
┌──────────────────────┼───────────────────────┐
▼ ▼ ▼
vLLM, 4×V100 16 ГБ GLM-5.3 у провайдера DeepSeek V4 Pro
Qwen3.8-27B AWQ INT4 (основной маршрут) (резерв)
имя: zubr имя: zubr-coder под тем же именем
Главное правило — клиенты знают только стабильные имена: zubr, zubr-flash, zubr-coder. За каждым именем стоит список маршрутов с порядком и резервом. Сменились веса или упал провайдер — интеграции этого не замечают.
Грабля 1. Тихий уход в резерв на /v1/messages
Сначала мы завели GLM-5.3 как обычный OpenAI-совместимый бэкенд — провайдером openai/ с нужным api_base. Чат и SDK работали. А Claude Code и Codex получали ответы… от резервной модели.
Оказалось, LiteLLM для openai/ переводит /v1/messages (Claude Code) и /v1/responses (Codex) в OpenAI Responses API. Провайдер отвечает на /responses 404, и роутер молча уходит на следующий маршрут. В логах ошибки нет, ответ есть — просто не от той модели.
Выдаёт это только заголовок ответа:
x-litellm-attempted-fallbacks: 1
Лечится нативным провайдером LiteLLM для этого вендора: он переводит Messages и Responses в chat/completions, который у провайдера есть. Переменная LITELLM_USE_CHAT_COMPLETIONS_URL_FOR_ANTHROPIC_MESSAGES чинит только /v1/messages, Codex с ней всё равно уходит в резерв.
Вывод: порядок маршрутов LiteLLM соблюдает всегда. Если отвечает «не та» модель — смотрите на fallback, а не на order.
Грабля 2. Claude Code сам просит Haiku, Sonnet и Opus
Подменить ANTHROPIC_MODEL мало. Claude Code отдаёт служебные подзадачи — заголовки, сжатие контекста, субагентов — моделям классов Haiku, Sonnet и Opus по их именам. У ключа без доступа к Anthropic такие запросы падают с 403, причём посреди работы.
Рабочий ~/.claude/settings.json выглядит так:
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.zubriq.ai",
"ANTHROPIC_AUTH_TOKEN": "sk-…",
"ANTHROPIC_MODEL": "zubr-coder[1m]",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "zubr-coder[1m]",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "zubr-flash[1m]",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "zubr",
"CLAUDE_CODE_MAX_CONTEXT_TOKENS": "262144",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
Ключ выпускается в личном кабинете zubriq.ai, а zubrcode connect claude записывает этот файл сам. Три детали, каждая стоила нам вечера:
ANTHROPIC_BASE_URL— без/v1. Claude Code сам дописывает/v1/messages, и с/v1получается/v1/v1/messages.Суффикс
[1m]говорит Claude Code, что у модели окно 1M, и отрезается до отправки: на шлюз уходит чистоеzubr-coder. Без него агент считает окно по своим правилам и сжимает контекст слишком рано.Для модели без
[1m]окно задаётCLAUDE_CODE_MAX_CONTEXT_TOKENS. На модели с[1m]эта переменная не влияет.
Грабля 3. Нулевые расходы
С нативным провайдером запросы пошли куда надо, но расход по ключам стал равен нулю. Для этого провайдера LiteLLM не берёт цену из model_info. Spend ноль — бюджеты ключей не срабатывают, отчёт по марже врёт.
Решение — продублировать цены прямо в litellm_params записи модели:
litellm_params:
model: <provider>/glm-5.3
input_cost_per_token: 0.0000012
output_cost_per_token: 0.000004
cache_read_input_token_cost: 0.00000022
Цифры условные — важно место, где они лежат. И ещё: правка модели через PATCH /model/{id}/update доезжает до второго пода не сразу, а через минуту-две. Не спешите считать, что не сработало.
Грабля 4. Модель, которая не видит картинок
GLM-5.3 силён в коде, но изображения не принимает. Причём ведёт себя по-разному в зависимости от входа:
Вход | Что происходит с картинкой |
|---|---|
| 400: |
Anthropic-совместимый эндпоинт провайдера | молча выкидывается, модель «угадывает» по тексту |
резерв DeepSeek V4 Pro | отвечает, что изображения нет |
Самое опасное — второй вариант: ошибки нет, ответ уверенный и неверный. Пользователь кидает скриншот с ошибкой, а модель рассуждает о том, чего не видела.
В нашем агенте ZubrCode мы сделали подмену. Если у модели в каталоге нет зрения, картинка заменяется текстовым описанием от быстрой модели со зрением — GLM-5.3 Flash. Тонкость: агент переигрывает всю историю на каждом ходе. Без кеша одно и то же фото описывалось бы заново каждый раз. Поэтому описание кешируется по sha256 картинки на диске.
Отдельная мелочь в каталоге: supports_vision: null из /model/info у нас трактовался как явное false ((bool) null). Из-за этого зрение пропало и у модели, которая видит. Правильно — «не знаю, спроси выше по цепочке». И учтите: если под одним именем несколько маршрутов, флаги берутся с первого по порядку.
Грабля 5. Своя модель на V100
Для данных, которые не должны уходить за границу, у нас Qwen3.8-27B на собственных 4×V100 по 16 ГБ. Карты 2017 года, без bf16 и без FP8. Поэтому квант AWQ INT4 с вычислениями в FP16, vLLM с тензорным параллелизмом на 4 карты и форк vLLM с поддержкой SM70.
Что всплыло:
KV-кеш нужно задавать явно. Форк считает бюджет KV как
total × gpu_memory_utilization − активациии не вычитает веса модели: загрузчик для SM70 отдаёт их размер почти нулём. Итог — KV съедает больше, чем можно, и соседу на тех же картах памяти не остаётся. Мы задаём--kv-cache-memory-bytesявно, а--gpu-memory-utilizationоставили только как проверку свободной памяти при старте.Два инстанса нельзя стартовать одновременно. Летом рядом с основной моделью жила отдельная VL-модель. Пик инициализации двух процессов был больше 16 ГБ — один воркер ловил OOM. Дальше хуже: основной инстанс зависал на NCCL watchdog. systemd показывал
active, а порт был закрыт. Лечили так: второй юнит ждал health первого вExecStartPre.Dense против MoE. До сентября под этим именем была MoE 35B-A3B: около 3 млрд активных параметров, первый токен за ~0,6 с. Плотная 27B умнее и видит картинки сама, без отдельной VL-модели. Но первый токен теперь ~1,3 с. Для агента, который делает десятки вызовов, это заметно. Мы оставили её как «Haiku-слот»: фоновые подзадачи и всё, что нельзя отдавать наружу.
Текущие параметры: контекст 262 144 токена, max-num-seqs 24, KV ~4,6 ГБ на карту, tool calling через парсер qwen3_xml.
Что в итоге
Claude Code, Codex и OpenCode работают через один шлюз на GLM-5.3 и Qwen, без патчей клиентов — только конфиг.
Модель в ключе стабильна по имени, а маршруты и веса под ней меняются без участия пользователей.
Самые неприятные ошибки здесь молчаливые: уход в резерв, нулевой spend, выброшенная картинка. Мониторьте заголовки fallback и сверяйте расход, а не только коды ответов.
Шлюз из статьи — часть нашей платформы ZuBrIQ: там же готовые настройки для Claude Code и других клиентов. В следующих статьях цикла — подробно про vLLM на V100 и маскирование персональных данных до отправки в модель. Делитесь в комментариях, какие клиенты и модели вы связываете и на какие грабли наступали.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.