После знакомства с Codex CLI от 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, без разрыва между логином и инференсом.
По документации 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.
Требования тут понятные и явно описаны в документации:
нужно зарегистрировать приложение в Yandex ID;
указать целевой scope cloud:auth;
у пользователя должен быть доступ к облачному аккаунту (включая ролевой доступ и биллинг).
Но ключевое для меня было то, что связка "человек залогинился -> CLI получил рабочий compute-доступ" в принципе есть.
По публичной документации 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, чтобы не ловить случайно кривые конфиги.
Ключевые файлы:
codex-rs/tui/src/onboarding/auth.rs
codex-rs/tui/src/onboarding/onboarding_screen.rs
codex-rs/core/src/config/mod.rs
codex-rs/core/config.schema.json
Что сделал:
универсальный старт OAuth-логина;
генерация authorize URL;
извлечение code;
обмен code на токены.
Отдельно поддержал два режима:
verification_code — нужен в текущем сценарии;
loopback — оставил как универсальный путь для провайдеров, где возможен локальный callback.
Ключевые файлы:
codex-rs/login/src/oauth.rs
codex-rs/login/src/lib.rs
codex-rs/login/src/server.rs
Что сделал:
добавил OauthAuthData в auth storage;
встроил OAuth в общий AuthManager;
добавил refresh-поток, чтобы сессия не рассыпалась после перезапуска/протухания access token.
Ключевые файлы:
codex-rs/core/src/auth/storage.rs
codex-rs/core/src/auth.rs
codex-rs/core/tests/suite/auth_refresh.rs
Это самый важный кусок: логин сам по себе ничего не даёт, пока рантайм не получает корректный IAM + folder context.
Что сделал:
обмен OAuth token на IAM token;
резолв активной folder;
интеграция этого контекста в общий auth/runtime путь.
Ключевой момент: OAuth-логин не должен заканчиваться на уровне UI. Его нужно доводить до полноценного compute-контекста внутри рантайма Codex.
Здесь вынес всю специфическую логику в отдельный файл:
codex-rs/core/src/auth/yandex.rs
Чтобы это было похоже на "родной" опыт, добавил провайдера как встроенный вариант и связал модельные пресеты с folder-aware slug.
Ключевые файлы:
codex-rs/core/src/model_provider_info.rs
codex-rs/core/src/models_manager/manager.rs
codex-rs/core/src/models_manager/model_presets.rs
На стороне пользователя поток стал таким:
Запускаю 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;
вызов этого логирования в build_responses_request(...) (codex-rs/core/src/client.rs).
Что это дало: в логах сразу видно role, text_len, превью каждого ResponseItem. И там быстро всплыл корень боли.
По логам сессий в ~/.codex-yandex/log/codex-tui.log:
в проблемных запросах в input уходил огромный блок # AGENTS.md instructions ..., который честно собирал рантайм по всему проекту для контекста агента (text_len порядка 18095);
после этого шли ретраи и финал с reason: content_filter.
То есть фильтр бил не по содержанию промпта, а по форме и объёму префикса контекста.
Чтобы это стабилизировать, появился 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.
В ряде rollout-ов assistant присылал не нормальный function_call, а текст вида:
[TOOL_CALL_START]exec_command ...
или fenced-блоки с update_plan/exec_command.
Без нормализации это ломает агентный цикл, потому что рантайм ждёт поддержку tool API и структурный tool-call item.
Я не в первый раз встречаюсь с таким поведением (этим часто грешат китайские модели), поэтому не удивился и просто добавил преобразование в 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).
Проблема в том, что на старте Yandex-модель определялась как unknown slug (Unknown model gpt://.../yandexgpt/rc). А для неизвестных моделей Codex по умолчанию ставит supports_parallel_tool_calls = false.
В итоге цикл автоматически перешёл в последовательный режим. И, что интересно, это неожиданно стабилизировало поведение агента: пропали ситуации, когда модель в одном ответе генерировала несколько 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);
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.
Инъекции — реальная угроза. Но когда инференс начинает реагировать на служебные теги или слова вроде shell как на атаку, это ломает инструментальный сценарий.
Защита не должна разрушать базовые инженерные use-case.
Если несколько циклов reasoning приводят к деградации инструкций, потере политики или невозможности нормально использовать AGENTS.md / SKILL.md, это уже архитектурное ограничение.
Агентность невозможна без устойчивого управления контекстом.
Агентный рантайм строится на контракте. Если модель периодически "падает" в legacy-текст вместо структурных tool-calls, контракт нарушается. Тогда рантайм начинает чинить модель, вместо того чтобы выполнять задачу.
Самая тонкая тема — это не фильтр и не tool API, а устойчивость следования инструкциям в многошаговом цикле.
В моих экспериментах после 2–3 итераций reasoning модели начинали терять строгость: забывать ограничения, игнорировать формат, "переизобретать" правила, которые уже были заданы в начале сессии (установка python зависимостей через uv — это еще то приключение).
Я давно гоняю разные инференсы под агентной нагрузкой — от простых 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 под каждого вендора, я давно вынес адаптацию в отдельный слой — роутер. Сначала он жил в проде как утилитарная прокладка между разными контрактами. Потом я пересобрал его в чистую Rust-реализацию, чтобы можно было нормально экспериментировать с инференсами в Codex.
Роутер принимает Responses API и chat/completions, нормализует события, сглаживает различия провайдеров и позволяет подключать разные инференсы к Codex без переписывания CLI.
Проект роутера здесь:
https://github.com/olegische/xrouter
Fork Codex с Yandex здесь:
https://github.com/xrouter-ru/codex-yandex-provider
Если хотите проверить агентные критерии сами — подключайте через роутер Yandex, GigaChat или любой другой инференс и прогоняйте свои сценарии в реальном агентном цикле.
Такие эксперименты важны не только для пользователей. Они сталкивают провайдеров с реальными многошаговыми задачами, а не с синтетическими демо-кейсами. Под такой нагрузкой быстрее всего проявляются ограничения и растёт зрелость экосистемы.
Источник


