- BrainTools - https://www.braintools.ru -
После знакомства с Codex CLI [1] от OpenAI я решил провести практический тест: можно ли в российском ИИ-ландшафте собрать ChatGPT-подобный login UX для агентного CLI — запускаю клиент, логинюсь, сразу работаю с инференсом.
Сначала разберу рынок и авторизацию: как я сравнил Яндекс и Сбер, и почему для нужного UX Яндекс оказался проще в реализации. А потом покажу самое вкусное: что пришлось чинить в runtime inference, чтобы агент вообще не умирал на первом ходе.
Для agentic CLI необходим воспроизводимый мост IDP login -> API token -> inference.
У Яндекса такой мост в публичной документации есть: OAuth -> IAM -> AI Studio API.
У Сбера в персональном сценарии (GIGACHAT_API_PERS) доступ к GigaChat API идёт через Client ID/Client Secret -> /api/v2/oauth, то есть через app-credentials контур.
Для гипотезы “сделать ChatGPT-like login UX в Codex CLI под российский инференс” путь через Яндекс оказался единственным вариантом, где связка identity → compute собирается без архитектурного разрыва.
Бонус: Яндекс уже работает через Responses API, поэтому не пришлось насильно возвращать Codex в легаси chat/completions.
Самая жесть была не в авторизации, а в рантайме: content_filter, устаревший формат tool-call сообщений и необходимость жёсткой нормализации контекста.
Мне нужен был простой критерий: после пользовательского логина CLI должен сразу получать рабочий compute-доступ от имени этого пользователя.
Без ручной проклейки ключей, без отдельного контура app-credentials, без разрыва между логином и инференсом.
По документации [2] Yandex Cloud:
для AI Studio API нужен IAM token в Authorization: Bearer;
OAuth-токен пользователя можно обменять на IAM token.
Практически это даёт воспроизводимую цепочку:
Пользователь проходит OAuth-логин.
CLI получает OAuth token со scope cloud:auth.
CLI меняет его на IAM token.
CLI работает с AI Studio API в нужном облачном folder context.
Требования тут понятные и явно описаны в документации:
нужно зарегистрировать [3] приложение в Yandex ID;
указать целевой scope cloud:auth;
у пользователя должен быть доступ к облачному аккаунту [4] (включая ролевой доступ и биллинг).
Но ключевое для меня было то, что связка “человек залогинился -> CLI получил рабочий compute-доступ” в принципе есть.
По публичной документации [5] GigaChat API для персонального сценария:
используется scope=GIGACHAT_API_PERS;
access token выдаётся через POST /api/v2/oauth;
до этого нужен Authorization key (из Client ID + Client Secret / ключа проекта);
токен короткоживущий (30 минут).
На практике для CLI это означает раздельные auth-контуры: пользовательский вход по SberID и доступ к compute API не образуют единую цепочку.
В публично описанном контуре отсутствует прямой механизм обмена пользовательского IDP-токена на compute-токен для GigaChat API.
Для персонального agentic UX уровня с непрерывным auth-контуром это и есть ключевое ограничение. Проще говоря: вошёл как пользователь, а дальше всё равно отдельная возня с app-ключами.
Моя цель была узкой: сделать Codex CLI под российский инференс с login UX, максимально похожим на ChatGPT-поток.
Яндекс позволял проверить гипотезу сразу, потому что в документации есть явный и воспроизводимый мост между пользовательским OAuth и рабочим API-токеном.
Чтобы довести UX до рабочего состояния, пришлось пройти через несколько слоёв архитектуры Codex CLI.
Что сделал:
добавил конфигурируемые OAuth-провайдеры;
вынес их в config.toml (oauth_providers);
собрал onboarding-экран в TUI для настройки и старта логина;
зафиксировал scope cloud:auth, чтобы не ловить случайно кривые конфиги.
Ключевые файлы [6]:
codex-rs/tui/src/onboarding/auth.rs [7]
codex-rs/tui/src/onboarding/onboarding_screen.rs [8]
codex-rs/core/src/config/mod.rs [9]
codex-rs/core/config.schema.json
Что сделал:
универсальный старт OAuth-логина;
генерация authorize URL;
извлечение code;
обмен code на токены.
Отдельно поддержал два режима:
verification_code — нужен в текущем сценарии;
loopback — оставил как универсальный путь для провайдеров, где возможен локальный callback.
Ключевые файлы [10]:
Что сделал:
добавил OauthAuthData в auth storage;
встроил OAuth в общий AuthManager;
добавил refresh-поток, чтобы сессия не рассыпалась после перезапуска/протухания access token.
Ключевые файлы [14]:
codex-rs/core/src/auth/storage.rs [15]
codex-rs/core/src/auth.rs [7]
codex-rs/core/tests/suite/auth_refresh.rs [16]
Это самый важный кусок: логин сам по себе ничего не даёт, пока рантайм не получает корректный IAM + folder context.
Что сделал:
обмен OAuth token на IAM token;
резолв активной folder;
интеграция этого контекста в общий auth/runtime путь.
Ключевой момент: OAuth-логин не должен заканчиваться на уровне UI. Его нужно доводить до полноценного compute-контекста внутри рантайма Codex.
Здесь вынес всю специфическую логику [17] в отдельный файл [18]:
codex-rs/core/src/auth/yandex.rs [19]
Чтобы это было похоже на “родной” опыт [20], добавил провайдера как встроенный вариант и связал модельные пресеты с folder-aware slug.
Ключевые файлы [21]:
codex-rs/core/src/model_provider_info.rs [22]
codex-rs/core/src/models_manager/manager.rs [23]
codex-rs/core/src/models_manager/model_presets.rs [24]
На стороне пользователя поток стал таким:
Запускаю codex.
Выбираю нужного провайдера.
Прохожу вход в браузере.
Возвращаюсь в CLI и ввожу verification_code.
CLI получает рабочий контекст и может идти в модель.
Субъективно это уже близко к нужному login first, work immediately, ради которого я и затевал первую часть эксперимента.
После логина всё выглядело красиво ровно до первого живого запроса. Дальше началась суровая реальность: вместо нормального агентного цикла я наблюдал сообщения от цензора Yandex с фразой CONTENT FILTER снова и снова. После этого разваливающийся tool-loop уже не удивлял.
На самом деле content_filter я увидел при первом же запросе: Codex отправляет отдельно developer сообщение со страшным словом shell да еще и в XML тегах. Не проблема, если склеить все developer сообщения в одно, то начинает проходить.
Но цензор только разминался. При втором запросе я снова словил content_filter. Почему? Зачем? Поэкспериментировав с историей, решил отдельно логировать сырые запросы в инференс на провайдере Yandex:
log_yandex_responses_request_payload(...) в codex-rs/core/src/yandex_context_normalizer.rs [25];
вызов этого логирования в build_responses_request(...) (codex-rs/core/src/client.rs [26]).
Что это дало: в логах сразу видно role, text_len, превью каждого ResponseItem. И там быстро всплыл корень боли [27].
По логам сессий в ~/.codex-yandex/log/codex-tui.log:
в проблемных запросах в input уходил огромный блок # AGENTS.md [28] instructions ..., который честно собирал рантайм по всему проекту для контекста агента (text_len порядка 18095);
после этого шли ретраи и финал с reason: content_filter.
То есть фильтр бил не по содержанию промпта, а по форме и объёму префикса контекста.
Чтобы это стабилизировать, появился [29] normalize_responses_input_for_provider(...):
схлопывание стартовых служебных сообщений в компактный префикс;
вынос developer-контента в единый блок (склейка);
временный хак: вырезать AGENTS-полотно (то что вообще делает агента Codex агентом), но сохранить <environment_context>.
Код: merge_initial_context_messages(...) и strip_agents_md_context_hack(...) в codex-rs/core/src/yandex_context_normalizer.rs [25].
В ряде rollout-ов assistant присылал не нормальный function_call, а текст вида:
[TOOL_CALL_START]exec_command ...
или fenced-блоки с update_plan/exec_command.
Без нормализации это ломает агентный цикл, потому что рантайм ждёт поддержку tool API и структурный tool-call item.
Я не в первый раз встречаюсь с таким поведением [30] (этим часто грешат китайские модели), поэтому не удивился и просто добавил преобразование [31] в normalize_responses_output_items_for_provider(...):
парсинг [TOOL_CALL_START]...;
парсинг fenced tool-блоков;
ремонт кривого JSON (например \' в аргументах shell);
генерация валидных ResponseItem::FunctionCall с call_id формата yandex-legacy-*.
В Codex возможность параллельных вызовов инструментов определяется флагом parallel_tool_calls. Он подтягивается из model_info.supports_parallel_tool_calls (см. run_sampling_request(...) и сборку запроса в client.rs [26]).
Проблема в том, что на старте Yandex-модель определялась как unknown slug (Unknown model gpt://.../yandexgpt/rc). А для неизвестных моделей Codex по умолчанию ставит supports_parallel_tool_calls = false.
В итоге цикл автоматически перешёл в последовательный режим. И, что интересно, это неожиданно стабилизировало поведение [32] агента: пропали ситуации, когда модель в одном ответе генерировала несколько tool-команд и рантайм начинал спотыкаться о формат событий. Мы ведь помним, что Yandex нарушает tool API на второй-третий запрос, а поиск серии tool calls в ответе модели не выглядит стабильным решением.
По факту вывод простой: при интеграции стороннего провайдера лучше сначала добиться стабильного serial tool-loop. Параллелизм имеет смысл включать только тогда, когда формат ответов и событий гарантированно чистый и предсказуемый.
Самый приятный бонус интеграции: не пришлось тащить костыли под legacy chat/completions, который команда Codex уже успела отключить в приложении. Откатывать логику ребят было бы странно.
В провайдере сразу зафиксировано:
wire_api = "responses" (create_yandex_provider в codex-rs/core/src/model_provider_info.rs [22]);
supports_websockets = false и обычный HTTP streaming путь.
Это сильно упростило архитектуру: я чинил только нормализацию и совместимость формата, а не весь транспортный слой Codex.
После всех правок пайплайн стал воспроизводимым:
OAuth/login и IAM/folder context собираются автоматически.
Запросы не валятся пачкой в content_filter по тем или иным причинам.
Legacy tool-call текст приводится к структурным функциям, и агентный цикл живёт.
Inference работает на Responses API без отката в старый режим.
Итого: самая сложная часть оказалась не в добавлении логина Yandex, а в адаптации Codex рантайма к реальным возможностям инференса.
Интеграция заработала технически. Login собирается. IAM подтягивается. Responses API работает. Tool-loop после нормализации не разваливается на первом шаге.
Но ощущение “всё заработало” так и не появилось.
Формально интегрировать инференс — не значит получить полноценный агентный рантайм. Вопрос в агентных критериях. Если их нет, никакая обвязка не сделает систему агентной.
Я для себя сформулировал минимальный набор таких критериев.
Логин пользователя должен напрямую давать compute-доступ. Без разрыва между identity пользователя и инференсом.
Если после входа начинается отдельная возня с ключами — это не агентный UX.
Инъекции — реальная угроза. Но когда инференс начинает реагировать [33] на служебные теги или слова вроде shell как на атаку, это ломает инструментальный сценарий.
Защита не должна разрушать базовые инженерные use-case.
Если несколько циклов reasoning приводят к деградации инструкций, потере политики или невозможности нормально использовать AGENTS.md [34] / SKILL.md, [35] это уже архитектурное ограничение.
Агентность невозможна без устойчивого управления контекстом.
Агентный рантайм строится на контракте. Если модель периодически “падает” в legacy-текст вместо структурных tool-calls, контракт нарушается. Тогда рантайм начинает чинить модель, вместо того чтобы выполнять задачу.
Самая тонкая тема — это не фильтр и не tool API, а устойчивость следования инструкциям в многошаговом цикле.
В моих экспериментах после 2–3 итераций reasoning модели начинали терять строгость: забывать [36] ограничения, игнорировать формат, “переизобретать” правила, которые уже были заданы в начале сессии (установка python зависимостей через uv [37] — это еще то приключение).
Я давно гоняю разные инференсы под агентной нагрузкой — от простых CLI-циклов до прогонов под агентные бенчмарки. И там всегда возникает ощущение: эта модель “держит форму” или нет. Появляется субъективное чувство — “твоя” она или нет.
Но когда модель обнуляется уже после пары циклов — это перестаёт быть субъективным ощущением. Это измеримый эффект деградации инструкционного слоя под нагрузкой. И речь не о качестве ответа и не о креативности.
Речь о способности сохранять контракт:
помнить ограничения,
соблюдать формат,
продолжать начатую стратегию,
не разрушать уже построенный контекст.
Если модель не удерживает инструкционную рамку в пределах одной агентной сессии, никакая обвязка её не спасёт. Агентность — это не “умный ответ”, это стабильное поведение в агентном цикле. И это, по сути, один из главных маркеров зрелости агентного слоя.
YandexGPT Pro 5.1 (её я тестировал в Codex; Alice AI LLM позиционируется как чат-модель — tool-контракт и агентный reasoning она держит заметно хуже)
Непрерывный auth-контур собрать можно — плюс.
Responses API есть — плюс.
Tool API формально поддерживается — плюс (с поправкой на нормализацию и легаси-адаптацию).
Устойчивость контекста и поведение фильтра — спорный момент.
Долгоживущий reasoning без деградации — нестабильно.
Инфраструктурно Yandex сейчас ближе к агентному UX. Поведенчески ограничения начинают проявляться уже в многошаговом цикле.
GigaChat-2-Max (тестировался на стандартном Codex CLI через роутер)
Непрерывного auth-контрура нет — первый критерий не выполняется.
Контекст больше (заявленные 128k против ~32k у YandexGPT Pro 5.1) — плюс.
Tool API соблюдается чище, чем у Yandex — плюс.
Но деградация инструкционного слоя под нагрузкой ощущается сильнее.
То есть формально контракт может выглядеть аккуратнее, но после пары циклов reasoning модель начинает “расплываться” быстрее.
И это не раздача оценок, а попытка трезво посмотреть на зрелость агентного слоя через один и тот же рантайм.
Агентность — не про OAuth и не про наличие tool-calls. Это способность модели удерживать инструкционную рамку, не деградировать после нескольких итераций и соблюдать контракт с рантаймом. Если этого нет, мы получаем не агента, а чат с инструментами.
Codex CLI – это универсальный агентный рантайм. На его базе можно строить и бизнес-агентов, и исследовательские пайплайны, и автоматизацию. Но если модель не удерживает контракт в цикле, сфера применения не имеет значения — деградация всё равно проявится.
С самого начала мне хотелось не оценки инференсам раздавать, а провести воспроизводимый эксперимент.
Интеграция показала: разные провайдеры живут в разных эпохах API.
У GigaChat — контракт раннего OpenAI, который уже даже не fully compatible.
У Yandex до Responses API был свой, мягко говоря, нестандартный completions.
У Codex — строгий рантайм, который ждёт нормальный Responses и корректный tool-контракт.
Чтобы не переписывать CLI под каждого вендора, я давно вынес адаптацию в отдельный слой — роутер [38]. Сначала он жил в проде как утилитарная прокладка между разными контрактами. Потом я пересобрал его в чистую Rust-реализацию, чтобы можно было нормально экспериментировать с инференсами в Codex.
Роутер принимает Responses API и chat/completions, нормализует события, сглаживает различия провайдеров и позволяет подключать разные инференсы к Codex без переписывания CLI.
Проект роутера здесь:
https://github.com/olegische/xrouter [38]
Fork Codex с Yandex здесь:
https://github.com/xrouter-ru/codex-yandex-provider [39]
Если хотите проверить агентные критерии сами — подключайте через роутер Yandex, GigaChat или любой другой инференс и прогоняйте свои сценарии в реальном агентном цикле.
Такие эксперименты важны не только для пользователей. Они сталкивают провайдеров с реальными многошаговыми задачами, а не с синтетическими демо-кейсами. Под такой нагрузкой быстрее всего проявляются ограничения и растёт зрелость экосистемы.
Автор: olegische
Источник [40]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/26575
URLs in this post:
[1] Codex CLI: https://github.com/openai/codex
[2] документации: https://yandex.cloud/ru/docs/iam/concepts/authorization/oauth-token
[3] зарегистрировать: https://yandex.ru/dev/id/doc/ru/register-api
[4] облачному аккаунту: https://console.yandex.cloud/link/ai-studio
[5] документации: https://developers.sber.ru/docs/ru/gigachat/quickstart/ind-using-api
[6] Ключевые файлы: https://github.com/xrouter-ru/codex-yandex-provider/tree/yandex/codex-rs
[7] auth.rs: http://auth.rs
[8] screen.rs: http://screen.rs
[9] mod.rs: http://mod.rs
[10] Ключевые файлы: https://github.com/xrouter-ru/codex-yandex-provider/tree/yandex/codex-rs/login/src
[11] oauth.rs: http://oauth.rs
[12] lib.rs: http://lib.rs
[13] server.rs: http://server.rs
[14] Ключевые файлы: https://github.com/xrouter-ru/codex-yandex-provider/tree/yandex/codex-rs/core
[15] storage.rs: http://storage.rs
[16] refresh.rs: http://refresh.rs
[17] логику: http://www.braintools.ru/article/7640
[18] отдельный файл: https://github.com/xrouter-ru/codex-yandex-provider/blob/yandex/codex-rs/core/src/auth/yandex.rs
[19] yandex.rs: http://yandex.rs
[20] опыт: http://www.braintools.ru/article/6952
[21] Ключевые файлы: https://github.com/xrouter-ru/codex-yandex-provider/tree/yandex/codex-rs/core/src/models_manager
[22] info.rs: http://info.rs
[23] manager.rs: http://manager.rs
[24] presets.rs: http://presets.rs
[25] normalizer.rs: http://normalizer.rs
[26] client.rs: http://client.rs
[27] боли: http://www.braintools.ru/article/9901
[28] AGENTS.md: http://AGENTS.md
[29] появился: https://github.com/xrouter-ru/codex-yandex-provider/blob/yandex/codex-rs/core/src/yandex_context_normalizer.rs
[30] поведением: http://www.braintools.ru/article/9372
[31] добавил преобразование: https://github.com/xrouter-ru/codex-yandex-provider/blob/yandex/codex-rs/core/src/yandex_context_normalizer.rs#L41
[32] поведение: http://www.braintools.ru/article/5593
[33] реагировать: http://www.braintools.ru/article/1549
[34] AGENTS.md: https://agents.md/
[35] SKILL.md,: https://agentskills.io/what-are-skills
[36] забывать: http://www.braintools.ru/article/333
[37] uv: https://docs.astral.sh/uv/
[38] роутер: https://github.com/olegische/xrouter
[39] https://github.com/xrouter-ru/codex-yandex-provider: https://github.com/xrouter-ru/codex-yandex-provider
[40] Источник: https://habr.com/ru/articles/1006422/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1006422
Нажмите здесь для печати.