Как научить Kimi говорить на языке Claude Code: полевой гид по переводу форматов инструментов
Как научить Kimi говорить на языке Claude Code: полевой гид по переводу форматов инструментов
Вы хотите запускать харнесс Claude Code — субагентов, скиллы, хуки, всю обвязку, которую вы месяцами настраивали, — но управлять им через Kimi K2.5, или GLM, или DeepSeek, потому что счет за флагман бьет по карману. Есть два пути к этому, и оба упираются в одну и ту же стену:
- Скормить инструменты Claude Code другой модели. Ее определения инструментов уходят в первый запрос, и каждый вызов инструмента должен вернуться в том виде, который ожидает Claude Code. Значит, кто-то должен переводить формат на уровне протокола.
- Использовать сторонний харнесс (OpenCode, Cline, Goose, Crush), который уже говорит с любым провайдером. Тогда вы наследуете их харнесс, а не свой.
Эта статья — про слой, который делает вариант 1 возможным, про переводчики форматов инструментов, и про то, почему, разложив весь ландшафт по полочкам, я думаю, что правильный ответ — ни то ни другое.
1. Почему перевод — это не переименование поля
Наивная ментальная модель такая: Anthropic называет это input_schema, OpenAI называет это parameters, пишем маппер, готово. Эта часть действительно тривиальна. Вот реальная разница.
Определения инструментов:
| Anthropic Messages | OpenAI Chat Completions | OpenAI Responses | |
|---|---|---|---|
| Форма | плоская: {name, description, input_schema} | вложенная: {type:"function", function:{name, description, parameters}} | плоская: {type:"function", name, parameters} |
| Только у Anthropic | cache_control, input_examples, defer_loading | — | — |
Обратите внимание на ловушку уже здесь: собственный Responses API от OpenAI плоский, ближе к Anthropic, чем к OpenAI Chat Completions. Переводчик, который зашивает жестко “OpenAI значит вложенный function{}”, ошибается на половине OpenAI.
Структура хода — вот это главное:
- Anthropic: вызовы инструментов — это блоки контента
tool_useвнутри сообщения ассистента, перемежающиеся с блокамиtextиthinking. Результаты возвращаются как блокиtool_resultв сообщении сrole: "user". - OpenAI CC: вызовы инструментов — это массив
tool_calls[]на сообщении ассистента. Результаты — отдельные сообщения сrole: "tool". - OpenAI Responses: ни то ни другое — самостоятельные элементы
function_call, скоррелированные поcall_id, который является отдельным полем от собственногоidэлемента.
Последствия, которые прокси обязан обрабатывать: Anthropic разрешает текст и вызов инструмента в одном и том же ходе ассистента (наивные конвертеры разбивают их на два сообщения и порождают невалидную историю); каждый tool_use обязан иметь парный tool_result, иначе API отдаст 400 — “осиротевшие вызовы инструментов” это самая частая задача по санитизации, и у LiteLLM есть отдельная фича специально ради нее.
Кодирование аргументов: tool_use.input у Anthropic — это распарсенный объект. function.arguments у OpenAI — это строка, закодированная в JSON. Вызовы без аргументов выдают "" там, где потребитель ждет {}, и JSON.parse падает — живой баг в Vercel AI SDK (#10295).
tool_choice: any у Anthropic — это required у OpenAI; disable_parallel_tool_use живет внутри tool_choice у Anthropic, но это верхнеуровневый parallel_tool_calls у OpenAI. Смена tool_choice к тому же инвалидирует ваши закэшированные блоки сообщений.
Переносимость JSON Schema: ни один крупный провайдер не принимает верхнеуровневый $ref — нужно инлайнить. Gemini отвергает items: {}. Строгий режим OpenAI принимает pattern/minimum/format, но не форсирует их. А в Responses пропуск strict все равно пытается включить строгий режим и молча деградирует — так что прокси, который просто пробрасывает определения инструментов, изменил семантику, не сказав вам об этом.
Все вышеперечисленное — механика. Раздражает, но решаемо. Следующий раздел — нет.
2. Три вещи, которые никто не решил
Каждый шлюз, на который я смотрел, ломается в одних и тех же трех местах.
2.1 Пересборка потока
Непотоковый перевод — решенная задача. Claude Code всегда стримит.
- Anthropic SSE:
content_block_start(типtool_use, несетid+name) → N×content_block_deltaс{"type":"input_json_delta","partial_json":"…"}→content_block_stop→message_deltaсоstop_reason:"tool_use". - OpenAI SSE: фрагменты
delta.tool_calls[], ключуемые по полюindex;id/nameпоявляются один раз, аргументы приходят строковыми фрагментами; заканчивается наfinish_reason:"tool_calls".
Превращение одного в другое требует конечных автоматов на каждый index, удерживаемых через чанки, и вот тут все и разваливается. LiteLLM выдавал content_block_start + content_block_stop с нулем input_json_delta между ними — каждый вызов инструмента приходит с input: {}, и Claude Code сообщает об отсутствии обязательных параметров (#25561, #25321, #25390). Bifrost шлет stop_reason: "end_turn" вместо "tool_use" на смешанных ходах текст+инструмент, так что клиент думает, что ассистент закончил (#3638). Portkey теряет роль assistant из потоковых дельт (#1000). Roo Code держал две статические карты состояния через чанки чисто чтобы прикрыть несогласованность потока между провайдерами.
2.2 Состояние рассуждения непрозрачно и обязательно
Это самая глубокая структурная причина, по которой связка Claude Code плюс чужая модель теряет данные.
Документация Anthropic по расширенному мышлению явно об этом говорит: когда вы постите tool_result, блоки thinking на последнем сообщении ассистента должны быть возвращены целиком и без изменений, иначе вы получите 400: "thinking or redacted_thinking blocks in the latest assistant message cannot be modified". Названная в собственной документации Anthropic коренная причина — “код приложения, который фильтрует блоки контента по типу” — а это ровно то, чем является переводчик формата.
А signature на блоке мышления — это зашифрованное представление рассуждения, которое сервер расшифровывает, чтобы восстановить состояние. В протоколе OpenAI нет поля, которое могло бы его перенести. Круговой рейс через Chat Completions уничтожает его. Вот почему LiteLLM #15601, vercel/ai #11602 и claude-code-router #1400/#1410 существуют как отдельные открытые баги против отдельных кодовых баз: это один и тот же баг.
У каждого провайдера своя версия, и ни одна из них не совместима с другими — signature у Anthropic, reasoning.encrypted_content у OpenAI (который в потоке отсутствует в output_item.added и появляется только в output_item.done — конвертеры, читающие только первое событие, молча его теряют), thought_signature у Gemini на частях functionCall. Также стоит знать: tool_choice: any и tool_choice: tool это жесткие ошибки при расширенном мышлении — только auto/none — что полностью ломает структурированный вывод с принудительным инструментом.
2.3 Кэширование промптов испаряется
cache_control есть только у Anthropic. Кэширование у OpenAI неявное, так что маппить его не на что. Каждый перевод Anthropic→OpenAI молча выбрасывает ваше кэширование промптов — самый большой рычаг из гайда по экономии токенов. Вы экономите на цене за токен и отдаете это обратно на промахах кэша, и ничто в логах вам этого не скажет.
DeepSeek — единственный вендор, который говорит об этом вслух: их документация по совместимости с Anthropic перечисляет cache_control в разделе не поддерживается, прямо рядом с картинками, документами и MCP-интеграциями. Уважение за это.
3. Переводчики
LiteLLM — Python, ~53k ★, прокси + библиотека
LiteLLM — это ответ по умолчанию, и у него есть две Anthropic-поверхности, которые люди постоянно путают. POST /anthropic/* — это сквозной проброс — никакого перевода, он просто форвардит в Anthropic и добавляет учет затрат. POST /v1/messages (anthropic_messages) — это настоящий перевод: на входе формат Anthropic, на выходе любой провайдер. Claude Code документирован первоклассно — направьте ANTHROPIC_BASE_URL на него и задайте CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1.
Он самый полный и самый обкатанный в боях, и он же там, где заведено большинство багов стриминга из §2.1 — не потому что он хуже, а потому что его запускают все. Это Python, он тяжелый, и его логирование болтливо настолько, что это отдельная проблема.
Bifrost — Go, ~6.5k ★, Apache-2.0, шлюз
Bifrost — это то, за чем я бы потянулся, если бы хотел один бинарник вместо Python-процесса. Drop-in префиксы (/openai, /anthropic, /genai), и Claude Code — первоклассная документированная цель с оверрайдами ANTHROPIC_DEFAULT_SONNET_MODEL / ..._HAIKU_MODEL. Он честно документирует свои шаги конвертации — извлечение системного сообщения, группировку сообщений инструментов, трансформацию блока мышления, reasoning→thinking с минимальным бюджетом 1024. Он к тому же MCP-шлюз.
Его заявление “в 50 раз быстрее LiteLLM, накладные расходы 11µs” опубликовано самим вендором без независимого воспроизведения. Относитесь соответственно.
Portkey AI Gateway — TypeScript, ~12k ★, шлюз
У Portkey самая чистая концепция: три универсальных входных формата — /v1/chat/completions, /v1/responses и /v1/messages — каждый из которых работает со всеми провайдерами. Двунаправленный по построению. Но нет штатного туториала по Claude Code, а открытые issue — это ровно классы поломок из §2 (спаривание ID tool_use↔tool_result сломано на параллельных вызовах, tool_choice: none отвергается для Anthropic, отсутствующие роли в потоковых дельтах).
claude-code-router — TypeScript, ~36k ★
Здесь стоит поправить устаревшую ментальную модель: CCR больше не тот маленький прокси-трансформер, который люди помнят. Теперь это монорепозиторий на v3.0.11, поставляющий Electron-панель управления плюс локальный шлюз моделей, и он рулит еще Codex и ZCode, с пресетами для OpenRouter, DeepSeek, Moonshot/Kimi, Z.AI, MiniMax, SiliconFlow. Слой трансформера живет дальше как отдельная, гораздо меньшая библиотека, musistudio/llms, с интерфейсом из четырех хуков (transformRequestIn/Out, transformResponseIn/Out).
Прочитайте его трекер issue, и паттерн из §2.2 выпрыгивает: открытые баги кучкуются на рассуждение × вызовы инструментов, а не на простых вызовах инструментов. Потоковое рассуждение портит дельты аргументов инструмента, reasoning_content от Kimi не сохраняется через историю вызовов инструментов, отсутствующий thought_signature у Gemini, 400 на мышление+инструменты у DeepSeek. Простой вызов инструментов работает. Мышление плюс вызов инструментов — вот где кровоточит.
Vercel AI SDK — TypeScript, ~25k ★, библиотека, не шлюз
AI SDK нормализует внутри процесса через адаптеры на каждый провайдер. tool({description, inputSchema, execute}), Zod или сырая JSON Schema. Он прекрасно ложится в стек Bun/TS, но это библиотека — вы не можете поставить ее перед Claude Code, не построив сначала прокси вокруг нее.
Самая показательная деталь во всем SDK — это experimental_refineToolInput, который существует — согласно документации — потому что “разные LLM-провайдеры генерируют слегка разные входы инструментов” (null против ""). Аварийный люк, выпущенный как официальное признание того, что нормализация теряет данные.
4. Категория прокси умирает, и это хорошая новость
Вот находка, которой я не ожидал. Из четырех самых известных сообществом Anthropic-совместимых прокси два заархивировались за последние полгода: y-router (заархивирован в январе 2026, README теперь указывает на официальную интеграцию OpenRouter) и anthropic-proxy (заархивирован в апреле 2026). Убило их то, что провайдеры сами выкатили эндпоинт:
| Провайдер | Anthropic-совместимый base URL |
|---|---|
| Moonshot / Kimi | https://api.moonshot.ai/anthropic |
| Z.ai / Zhipu GLM | https://api.z.ai/api/anthropic |
| DeepSeek | https://api.deepseek.com/anthropic |
| MiniMax | https://api.minimax.io/anthropic |
| Qwen / DashScope | https://dashscope-intl.aliyuncs.com/apps/anthropic |
| OpenRouter | https://openrouter.ai/api (их “Anthropic-шкурка”) |
А это ровно тот список, между которым Clother и OpenClaude уже переключаются одной переменной окружения. Так что для типичного случая — “я хочу, чтобы Kimi рулил Claude Code” — переводчик формата вам не нужен вообще. Вендор запускает его за вас, на своей стороне, бесплатно. Задайте ANTHROPIC_BASE_URL и вперед.
Две оговорки. Эндпоинт /anthropic у Kimi широко используется, но не документирован вендором (есть открытый запрос на документацию). И Cloudflare AI Gateway здесь неправильный инструмент — его маршрут /anthropic только сквозной; он кэширует и наблюдает трафик к Anthropic, он не переводит от нее.
За настоящим переводчиком вы тянетесь, когда вам нужно что-то, чего вендорский эндпоинт не даст: роутинг (дешевая модель для субагентов класса Haiku, флагман для ведущего), failover между провайдерами при rate-limit, единое место для учета затрат на весь флот — те вещи, о которых §2 гайда по токенам.
5. Другой путь: харнессы, которые уже нормализуют
Если вы не хотите переводить для Claude Code, используйте харнесс, которому никогда не был нужен формат Claude:
| Харнесс | Язык | ★ | Как нормализует |
|---|---|---|---|
| OpenCode | TS/Bun | 185k | Vercel AI SDK + реестр models.dev |
| Cline | TS | 65k | собственные обработчики на провайдер |
| Goose | Rust | 51k | собственный трейт Provider |
| Crush | Go | 27k | fantasy + реестр catwalk |
| Aider | Python | 47k | LiteLLM — но ⚠️ заглох с мая 2026 |
| Roo Code | TS | 24k | 🔴 заархивирован в мае 2026 |
Три вещи, которым научила меня эта таблица:
- Эра вызова инструментов через XML закончилась. Cline v3.35 мигрировал с XML-в-системном-промпте на нативные JSON-вызовы инструментов; Roo убрал XML полностью и теперь жестко отвергает вызовы инструментов без
id. Если вы планировали обойти перевод формата, попросив модель выдавать XML — этот поезд ушел, и индустрия угнала его намеренно. - “toolshim” у Goose — доказательство существования того, что все это шиммируемый слой. Для моделей без нативного вызова инструментов Goose заставляет основную модель выдать вольный JSON, а затем запускает вторую, дешевую модель-интерпретатор (по умолчанию
mistral-nemoна Ollama), чтобы привести его к валидному вызову инструмента. Это экспериментально и оно виснет, но концептуально это чистейшая формулировка идеи: формат инструмента — задача перевода, а перевод — работа, которую можно отдать маленькой модели. - Aider — не референс ни для чего из этого — он намеренно избегает вызова инструментов вообще и правит через текстовые блоки SEARCH/REPLACE. Его режим отказа — “блок не совпал”, а не дрейф схемы. Другая вселенная.
И референсная реализация слоя нормализации, по языкам: TS → Vercel AI SDK, Go → charmbracelet/fantasy, Python → LiteLLM, Rust → пиши свое.
6. Стандарты вас не спасут
MCP это не решает. Это самое частое заблуждение, на которое я натыкаюсь. MCP — это слой обнаружения и транспорта: tools/list выдает вам JSON Schema, а дальше харнесс все равно должен конвертировать каждый MCP-инструмент в нативное определение инструмента провайдера, и модель все равно выдает нативные для провайдера вызовы инструментов. Каждая заковырка переносимости из §1 применяется без изменений. MCP едет поверх проблемы; он ее не касается.
У MCP к тому же своя чехарда: следующая ревизия спецификации приземляется 2026-07-28 и она крупнейшая с момента запуска — рукопожатие initialize убрано полностью, Mcp-Session-Id больше нет, транспорт HTTP+SSE признан устаревшим. Все, что написано под 2025-11-25, потребует переделки.
Что до универсальной спецификации вызова инструментов: никто не выигрывает. UTCP реален, активен и нишев (~300★ на спецификации) — и он поставляет плагин совместимости с MCP, что говорит вам, кто выигрывает. agents.json мертв (последний пуш в августе 2025). ACP от IBM был поглощен в A2A. Сам A2A здоров, но ортогонален: это интероп агент↔агент поверх непрозрачных агентов, а не нормализация схем инструментов. Де-факто стандартизация происходит скучным путем — каждый вендор клонирует форму /chat/completions, а теперь и форму /v1/messages.
7. Вывод, к которому я реально пришел: строить над Claude, а не под ним
Какое-то время я пытался оптимизировать Claude Code снизу — перехватывать часть его работы, подменять модель под ним, подрезать его контекст, переводить его инструменты. И я думаю, что это тупик. Не потому что это нельзя заставить работать, а из-за того, что говорит §2: пересборка потока, непрозрачное состояние рассуждения и испарившееся кэширование промптов — три класса поломок, которые ни один слой перевода не решил, только смягчил. Вы строите не фичу, вы подписываетесь на вечное обслуживание против трех движущихся API. Оптимизация под харнессом — это исследовательский проект: бесконечные эксперименты, никакой инструкции — и она порождает вещь, которая ломается каждый раз, когда провайдер выпускает минорную версию.
Лучший ход — в противоположном направлении: построить собственный харнесс, который сидит над Claude Code, и относиться к Claude Code как к одному из нескольких агентов-черных-ящиков.
Единица, которую вы оркестрируете, перестает быть моделью и становится харнесс-процессом: Claude Code здесь, сессия OpenCode там, прогон Codex вон там, каждый уже свободно владеет диалектом инструментов своего провайдера, каждый управляет своим контекстом, своим кэшированием, своим состоянием рассуждения. Вы говорите с ними через интерфейс, который они уже выставляют наружу — CLI, --format json, идентификатор сессии, который можно возобновить — а не через их внутренний протокол.
Что это вам дает:
- Проблема формата исчезает. Вы никогда не переводите вызов инструмента, потому что никогда не касаетесь слоя вызова инструментов. Claude Code говорит на Anthropic нативно. OpenCode говорит на чем хочет. Каждый держит свой кэш, свои блоки мышления, свой стриминг — три вещи, которые, как говорит §2, нельзя перенести по проводу. Баги из §2 — это баги про пересечение границы. Не пересекайте ее.
- Вы перестаете переадаптировать контекст под каждую модель. Адаптировать контекст под причуды Kimi, потом под GLM, потом под DeepSeek — это работа × N, и она гниет. Над харнессом адаптация контекста — собственная работа каждого харнесса — то, в чем он уже хорош.
- Выбор модели становится решением о роутинге, а не решением о сантехнике. Дешевый харнесс для механической задачи, флагманский харнесс для сложной. Это планировщик, а не прокси.
- Это тезис харнесса, примененный на уровень выше. Если именно харнесс — а не модель — превращает 6.7% в 68%, то рычаг не в подмене движков под харнессом. Он в слое, который решает, какой харнесс что запускает, и отказывается платить за одну и ту же ошибку дважды.
Конкретно, для меня это означает вложить cmdop-claude в cmdop как первый шаг — а затем перенести всю логику оркестрации харнессов туда, вместо того чтобы продолжать прикручивать дешевые модели под харнесс, который их никогда не просил.
Вывод
Если все, чего вы хотите, — это чтобы Kimi рулил Claude Code: используйте вендорский эндпоинт /anthropic и полностью пропустите переводчики. Если вам нужен роутинг, failover или учет затрат на весь флот поверх этого: LiteLLM, если вы уже на Python, Bifrost, если хотите один Go-бинарник. Ждите багов потоковых вызовов инструментов, ждите потери кэширования промптов, ждите, что мышление-плюс-инструменты будет острым краем — и знайте, что это свойства границы, а не выбранного вами инструмента.
А стратегический ответ — перестать давить на эту границу. Не тратьте жизнь на то, чтобы научить каждую модель говорить на диалекте Claude Code снизу. Напишите харнесс, который говорит с харнессами сверху — и тогда уже неважно, на каком диалекте говорит любой из них.