Это личный инженерный эксперимент. Не релиз, не продуктовая статья и не попытка кого-то убедить. Мне просто захотелось проверить руками, насколько российские модели на примере GigaChat готовы к агентной работе в современной среде разработки.
Мне давно интересна разработка при помощи агентов. Обычный чат с моделью — это уже понятный сценарий: спросил, получил ответ, пошёл дальше. Агентный режим — следующий шаг. Модель там не просто пишет текст: она получает историю, вызывает инструменты, читает результаты, продолжает диалог, стримит ответ, иногда работает с картинками и живёт внутри реального проекта.
Поэтому первоначальная цель была простая: взять GigaChat/GigaCode, подключить к реальной агентной среде и посмотреть, насколько эти модели готовы к таким условиям. Не к красивому диалогу в вакууме, а к работе с кодом, инструментами, историей, streaming и всеми странностями, которые появляются в настоящем developer workflow.
Для проверки я взял OpenCode и начал писать небольшой TypeScript-плагин, который подключает GigaChat/GigaCode как провайдера. Это была исследовательская проверка концепции: без команды, коммерческих планов и долгой поддержки репозитория. В начале я вообще не знал, насколько глубоким получится этот слой. Хотелось просто подключить модели и посмотреть, как они покажут себя в реальной работе.
Спойлер такой: обычный текстовый запрос завёлся быстро. Там всё почти ожидаемо: получил токен, отправил messages, получил ответ. Но чем ближе я подходил к настоящему agent workflow — tools, function calling, streaming, OAuth, сертификаты, картинки, история диалога, — тем яснее становилось, что простого подключения мало. Начинается перевод с одного похожего протокола на другой.
Репозиторий: Overman775/opencode-gigachat-plugin
Если совсем коротко:
простой chat completion у GigaChat действительно похож на OpenAI API;
в agent-сценариях сходство быстро заканчивается;
основная сложность не в ответе модели, а в tools, истории, streaming и состоянии function calling;
прямое подключение быстро превращается в небольшой слой совместимости.
Почему OpenCode
OpenCode я взял не случайно. Во-первых, это open source. Для такого эксперимента это важно: можно не гадать, как всё устроено внутри, а открыть исходники, посмотреть, где собирается запрос, как подключаются провайдеры и где вообще можно аккуратно вклиниться без форка.
Во-вторых, это не просто чат в терминале, а coding agent с нормальной агентной механикой. Он работает с файлами, запускает инструменты, хранит историю, может жить внутри проекта. Проект достаточно популярный, вокруг него уже есть экосистема разных провайдеров и инструментов, поэтому проверка получается ближе к реальной разработке, а не к синтетическому примеру.
И ещё у меня уже был положительный опыт работы с OpenCode. Это тоже важно: когда проверяешь новую модель, не хочется одновременно разбираться с незнакомой средой с нуля. OpenCode был понятной точкой входа, где можно быстро перейти от идеи “а что если подключить GigaChat?” к рабочему прототипу.
Изначальная идея была простой: подключить GigaChat к OpenCode напрямую, без форка, и посмотреть на качество российских моделей в реальной работе. Не в абстрактном чате, а в среде, где модель помогает с кодом, видит контекст проекта и может пользоваться инструментами.
Я не начинал с плана исследовать все несовместимости протоколов. Они появились по дороге. Сначала хотелось просто проверить: получится ли завести GigaChat как провайдера и на что будут способны разные модели в таком сценарии.
Сам плагин был не целью, а инструментом исследования. Я не хотел написать “ещё один SDK”. Мне хотелось пройти путь от подключения модели до живой работы в OpenCode и уже по ходу понять, где появятся настоящие ограничения: в качестве ответов, в API или в агентном протоколе вокруг модели.
OpenAI-compatible не означает plug-and-play
В начале у меня была простая надежда: если API похож на OpenAI, значит можно поменять baseURL, подставить ключ и всё заработает.
Для простого чата иногда так и выглядит. Но coding agent требует больше. Он ожидает, что провайдер умеет не только отвечать текстом, но и корректно проводить весь цикл:

И здесь начинается разница между похожим API и совместимым агентным протоколом.
OpenCode отправляет запросы в OpenAI-совместимом формате. GigaChat, особенно в контракте legacy v1 chat, говорит похожим, но другим языком:
toolsнужно превращать вfunctions;tool_calls— вfunction_call;аргументы функции в OpenAI-формате часто строка, а GigaChat ждёт объект;
результат функции должен быть валидной JSON-строкой;
системное сообщение должно быть одно и строго первым;
картинки сначала нужно загрузить в
/files;functions_state_idнельзя потерять, иначе ломается продолжение tool loop.
По ходу дела задача стала шире, чем “добавить провайдера”. Постепенно плагин превратился в маленький протокольный переводчик.
Сначала я пошёл через cURL
Я люблю начинать такие эксперименты с cURL. Он быстро убирает лишнюю неопределённость. Если запрос работает руками, значит API живой. Если не работает, нечего винить OpenCode.
Сначала OAuth:
curl -L -X POST 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -H "RqUID: $(uuidgen)" \ -H 'Authorization: Basic <BASE64_CLIENT_ID_AND_SECRET>' \ --data-urlencode 'scope=GIGACHAT_API_PERS'
Потом обычный chat completion:
curl -L -X POST 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H "Authorization: Bearer $TOKEN" \ --data-raw '{ "model": "GigaChat-2", "messages": [ { "role": "user", "content": "Привет! Назови своё имя." } ], "max_tokens": 100 }'
На этом этапе всё спокойно: токен пришёл, модель ответила. Можно подумать, что дальше останется только завернуть это в плагин.
Но cURL оказался полезен дальше. Когда OpenCode начинал падать с 400 или 422, я мог отделить одно от другого: это я не так сформировал payload или GigaChat действительно так валидирует схему?
Постепенно ручные запросы переехали в код: проверил гипотезу через cURL, перенёс правило в TypeScript, добавил тест. Так в плагине появились многие странные на вид преобразования.
OAuth и сертификаты
OAuth сам по себе несложный, но его нужно встроить в жизненный цикл агента. GigaChat выдаёт временный access_token, который потом нужно передавать как Authorization: Bearer .... Токен живёт 30 минут, а сессия coding agent’а может жить дольше.
В плагине появился небольшой auth manager:
читает credentials из
opencode.jsonили переменных окружения;получает токен через
/api/v2/oauth;кэширует его;
обновляет заранее, за 5 минут до истечения;
если несколько запросов одновременно хотят обновить токен, делает только один OAuth-запрос.
Последний пункт важен. В agent workflow несколько запросов могут прийти рядом. Не хочется, чтобы истечение токена превращалось в пачку одинаковых запросов на авторизацию.
С сертификатами тоже был отдельный нюанс. В cURL для первого теста можно поставить -k, но жить на таком решении я не хотел. Поэтому в плагине появился локальный https.Agent, который используется только для запросов к GigaChat, OAuth и /files. Подробности здесь не так важны; главное, что я не стал отключать проверку TLS глобально для всего процесса.
Почему не внешний прокси
Можно было поднять отдельный прокси-сервер. OpenCode ходит в него как в OpenAI-compatible API, а прокси уже ходит в GigaChat.
Для большого решения это разумный путь. Но у меня был частный эксперимент, и держать ещё один сервис не хотелось. Поэтому слой совместимости остался прямо внутри OpenCode-плагина.
Сразу я, честно говоря, не знал, куда именно встраиваться. В OpenCode я до этого глубоко не разбирался, поэтому пришлось идти по наитию: смотреть конфиг провайдеров, читать исходники, запускать, смотреть, куда реально уходит запрос, и постепенно понимать, где можно подменить поведение без форка.
Рабочей точкой оказался сетевой слой. OpenCode всё равно собирает OpenAI-like запрос и отправляет его наружу, поэтому плагин на старте ставит перехватчик globalThis.fetch. В конфиге указывается виртуальный baseURL вроде https://api.gigachat.local/v1, а дальше плагин перехватывает такие запросы, получает OAuth-токен, приводит payload к формату GigaChat, отправляет его в настоящий endpoint и приводит ответ обратно.

Это решение появилось не сразу как красивая архитектура, а скорее как результат нескольких итераций: “попробовал — посмотрел, где сломалось — пошёл читать исходники — поправил”. Отдельно пришлось следить, чтобы перехват не был слишком широким. Bearer-токен нельзя добавлять просто потому, что где-то в URL встретилась строка api.gigachat.local, поэтому запрос считается GigaChat-запросом только для доверенных хостов или служебного заголовка.
Где началась настоящая несовместимость
Обычный chat completion почти не удивил. Настоящая несовместимость началась с tools.
OpenCode может отправить примерно такое:
{ "tools": [ { "type": "function", "function": { "name": "dart-mcp-server/read_package_uris", "parameters": { "type": "object", "additionalProperties": false } } } ], "tool_choice": "auto" }
GigaChat v1 ожидает формат ближе к такому:
{ "functions": [ { "name": "tool_1", "description": "...", "parameters": { "type": "object", "properties": {} } } ], "function_call": "auto" }
Разница не только в названии поля.
Первый нюанс — системные сообщения. У GigaChat системное сообщение должно быть одним и стоять первым. В agent-среде история может содержать несколько system/developer-сообщений. Плагин достаёт их, склеивает через перенос строки и кладёт итоговое system на индекс 0. Без этого легко получить 422 Unprocessable Entity.
Второй нюанс — имена инструментов. GigaChat строго относится к именам функций: латиница, цифры, underscore, не начинаться с цифры. А MCP-инструменты часто называются так:
dart-mcp-server/read_package_uris
Для API это не имя функции. Поэтому в GigaChat уходит безопасное tool_1, а обратно в OpenCode возвращается оригинальное имя.
Третий нюанс — arguments. В OpenAI-совместимом формате аргументы функции часто передаются как JSON-строка:
"arguments": "{\"location\":\"Moscow\"}"
GigaChat ждёт объект:
"arguments": { "location": "Moscow" }
На бумаге это выглядит как простое JSON.parse в одну сторону и JSON.stringify в другую. На практике до этой простой формулы я дошёл не сразу. OpenCode и GigaChat здесь почти говорят на одном языке, но с разными типами данных. Сначала часть запросов просто падала, и приходилось смотреть не только на документацию, но и на чужие примеры, SDK и то, как там собираются реальные запросы к GigaChat.
Отдельно помог репозиторий ai-forever/gigachat. Я не до конца понял, как правильно называть его статус — официальный, околоофициальный или просто основной SDK из экосистемы GigaChain, — но для практической разработки он оказался хорошим ориентиром. По нему было проще понять, как в живом коде устроены модели сообщений, function calling и streaming.
Дальше начались совсем маленькие на вид несовпадения. Например, assistant-сообщение с tool call может прийти с content: null. Рука сама тянется заменить это на пустую строку: вроде бы так безопаснее, меньше null в истории. Но для GigaChat в этом месте важен именно null, если рядом есть function_call. Иначе можно получить 422 и долго смотреть не туда, думая, что проблема в описании функции или JSON Schema.
С результатами инструментов тоже пришлось привыкать к другому ожиданию API. OpenCode-инструмент может вернуть обычный текст: вывел команду, прочитал файл, вернул строку. А GigaChat в сообщении роли function ждёт валидную JSON-строку. Поэтому обычный текст приходится упаковывать через JSON.stringify, иначе ответ ломается ошибкой invalid function result json string.
После нескольких таких мест стало ясно, что это не набор случайных придирок валидатора. Это разные представления о том, как должна выглядеть история после вызова инструмента. Где-то строка должна стать объектом, где-то null нельзя трогать, где-то текст нужно снова превратить в JSON-строку. По отдельности всё мелочи. Вместе — уже полноценный слой совместимости.
Состояние и один вызов за раз
Следующая штука всплыла уже не на уровне “отправил запрос — получил ответ”, а когда я начал гонять нормальный agent workflow. Модель вызывает инструмент, клиент его выполняет, результат возвращается обратно, и модель должна продолжить мысль.
В этот момент у GigaChat появляется functions_state_id. Сначала его легко принять за внутреннее служебное поле: вернулось и вернулось. Но если выкинуть его как “лишнее”, следующий шаг tool flow начинает ломаться. Поэтому плагин сохраняет этот id и прокидывает его дальше, чтобы GigaChat понимал, к какому вызову функции относится продолжение диалога.
В таком контракте GigaChat лучше чувствует себя в последовательном сценарии: один вызов инструмента, один результат, следующий шаг. Для OpenCode/MCP-мира это не всегда естественно. Агент может захотеть сначала прочитать файл, потом посмотреть конфиг, потом запустить тесты. Но с GigaChat надёжнее думать о tool calling как о каскаде.

Это чуть медленнее и менее свободно, зато так проще не потерять состояние. В исследовательском проекте для меня это был нормальный компромисс: важнее было понять, заведётся ли вся цепочка, чем сразу пытаться выжать из неё максимальную параллельность.
Где ещё пришлось переводить
Когда messages и functions наконец начали проходить, появилось ощущение: основную часть несовместимости я уже поймал. На практике нет. Как только OpenCode начинает работать не в режиме “один вопрос — один ответ”, наружу вылезают соседние куски протокола.
Первым таким местом стала JSON Schema для инструментов. MCP и OpenAI-совместимые клиенты могут добавлять в неё additionalProperties, $schema, nullable и другие поля, которые в их мире выглядят нормально. Но GigaChat legacy function schema принимает не всё. Поэтому схему пришлось не просто переслать, а привести к более строгому виду: убрать лишнее, оставить понятную структуру и не пытаться протащить через API всё, что пришло от клиента.
Потом всплыли reasoning-настройки. В OpenAI-совместимых клиентах могут встречаться reasoning_effort или thinking, но в GigaChat legacy v1 я не стал отправлять их как отдельные JSON-поля. Вместо этого перевёл их в системную инструкцию: условно низкий, средний или высокий уровень “подумай подробнее”. Это не идеальная модель управления рассуждением, но для проверки идеи такого компромисса хватило.
Со streaming та же ловушка: слово одно, форма разная. GigaChat отдаёт SSE, а OpenCode ждёт OpenAI-compatible chunks. Значит, поток тоже приходится читать как протокол, а не как “просто текст по кусочкам”: построчно парсить data: ..., переводить delta.function_call в delta.tool_calls и держать стабильный tool_call.id для одного вызова.
Картинки оказались ещё одним примером этой же истории. OpenAI-совместимый запрос может положить base64-картинку прямо в content. GigaChat ждёт другой маршрут: сначала загрузить файл в /api/v2/files, потом передать file_id в attachments. Для OpenCode это всё равно должно выглядеть как обычный запрос с картинкой, поэтому эту механику тоже пришлось спрятать внутрь плагина.
Практические выводы
Я не делал этот репозиторий как продукт, который нужно довести до идеала. Это было исследование, которое начиналось с простого вопроса: насколько современные российские модели готовы к работе внутри coding agent’а, если дать им не демо-чат, а настоящий OpenCode-сценарий. Уже по дороге выяснилось, что для такой проверки важна не только модель, но и весь API-контракт вокруг неё.
Выводы такие.
Простая генерация работает. Если нужен сценарий “спросил текст -> получил текст”, всё выглядит нормально.
Agent-сценарии требуют адаптера. Если у вас tools, MCP, stream, картинки и история, без слоя совместимости будет больно. Не потому что GigaChat плохой, а потому что похожий API и совместимый агентный протокол — разные уровни требований.
Многое выяснялось не из первого чтения документации, а через обычную отладку: отправил payload, получил 400 или 422, сузил проблему, сравнил с примерами и поправил переводчик. Так постепенно проявлялись реальные границы контракта: где API терпит похожий формат, а где требует строго своё.
Личные ощущения от моделей
Отдельно скажу про качество ответов, потому что это была одна из главных причин эксперимента. Я не только писал адаптер, но и запускал его: пробовал разные модели и смотрел, как они ведут себя в работе.
Это не бенчмарк. Я не гонял строгий набор задач, не строил таблицы и не мерил проценты. Это именно личное ощущение от работы в OpenCode.
В зависимости от модели GigaChat результат плавал где-то в диапазоне от “примерно уровень старого GPT-3.5” до “местами похоже на GPT-4o”. На простых задачах, объяснениях, небольших правках кода и русскоязычном контексте всё выглядело живым. В более сложных агентных сценариях качество уже упиралось не только в саму модель, но и в протокол: насколько корректно прошёл tool call, не потерялось ли состояние, не сломалась ли схема.
Мой вывод такой: сами модели уже заслуживают внимания. Но в coding-agent среде качество ощущается не только по “умности модели”. Оно складывается из модели, API-контракта, function calling, streaming и того, насколько предсказуемо всё это работает вместе.
Отдельно держу в голове GigaChat-3.1. Я работал через Sber Studio, личный кабинет технологических продуктов Сбера. В нём для моего сценария были доступны модели первого и второго поколения, а GigaChat-3.1 в списке выбора просто не было. Локально запускать такие модели в рамках этого эксперимента тоже не получалось.
Поэтому 3.1 я не проверил, хотя именно её теперь особенно хочется потрогать руками. Причина простая: в посте Сбера про 3.1 отдельно говорится про улучшения в reasoning и вызове функций. А для coding-agent workflow это как раз больное место. Если эти улучшения проявятся в API-сценарии с инструментами, результат может быть заметно лучше, чем у тех моделей, которые я успел проверить.
Что в итоге получилось
Плагин получился не “ещё одним клиентом для GigaChat”, а прослойкой между двумя мирами. Снаружи для OpenCode всё выглядит так, будто он ходит в обычный OpenAI-like endpoint. Внутри плагин забирает этот запрос, получает OAuth-токен, приводит payload к формату GigaChat и потом собирает ответ обратно.
Самый простой сценарий — обычный чат — завёлся быстро. Но как только появились streaming, tools, MCP-инструменты и картинки, одной замены baseURL уже не хватило. Плагин начал обрастать переводчиками для каждого отдельного случая: сохранить functions_state_id, переименовать инструмент, почистить JSON Schema, не трогать content: null, загрузить файл и передать file_id, перепаковать stream.
Для меня это и был главный результат исследования. Я начинал с вопроса “на что способны российские модели в реальной агентной среде разработки”, а по дороге получил ещё и карту мест, где похожий API перестаёт быть совместимым протоколом.
Ответ получился такой: жить может. С GigaChat API уже можно экспериментировать в coding-agent средах, и это не сводится к обычному чату. Но прямое подключение быстро упирается в детали контракта, поэтому между моделью и агентом нужен слой совместимости. Без него будет много познавательных вечеров с логами.
Небольшое пожелание к платформе
После этого эксперимента у меня осталось одно простое пожелание к платформе GigaChat: очень нужен понятный официальный контракт для таких интеграций. Модельная часть уже интересна, но developer tools смотрят не только на модель — им нужен предсказуемый способ встраивания.
Не в смысле “скопируйте OpenAI”. Скорее в практическом смысле. Современные dev tools, coding agents, MCP-клиенты, IDE-плагины и прокси уже часто ожидают OpenAI-like поведение. Если GigaChat хочет легко встраиваться в такие сценарии, разработчику нужен либо официальный совместимый режим, либо подробная OpenAPI-спецификация, где явно описаны messages, functions/tools, tool results, streaming, attachments и состояние между вызовами.
Тогда не придётся выяснять через 400/422, что arguments здесь объект, content при function call должен остаться null, functions_state_id нельзя потерять, а результат функции обязан быть валидной JSON-строкой. И больше людей смогут не писать свои переводчики, а просто брать GigaChat и пробовать его в реальных инструментах разработки.
