Codex CLI через свой endpoint: config.toml по строкам и девять способов его сломать
В первой статье я разбирал, куда ходит Claude Code и какие у него бывают ключи. Codex CLI устроен иначе: у него нет переменной окружения «подставь сюда свой URL», зато есть config.toml с десятком полей, половина из которых ломает всё молча.
Ниже — минимальный рабочий конфиг, потом девять способов его сломать. Каждую ошибку я воспроизвёл сегодня в изолированном CODEX_HOME на версии 0.154, тексты ошибок приведены как есть, только домен заменён на api.example.com.
Минимальный рабочий конфиг
Файл ~/.codex/config.toml:
model = "gpt-5.4-mini"
model_provider = "myproxy"
model_reasoning_effort = "low"
[model_providers.myproxy]
name = "My proxy"
base_url = "https://api.example.com/v1"
env_key = "MYPROXY_API_KEY"
wire_api = "responses"
И в той же оболочке, откуда запускаете codex:
export MYPROXY_API_KEY=sk-...
Что означает каждая строка:
model— id модели ровно так, как его понимает ваш endpoint.model_provider— имя секции ниже. Без этой строки Codex пойдёт в api.openai.com, даже если секция описана.model_reasoning_effort— сколько модель думает перед ответом. Про цену этого — ниже.base_url— с/v1на конце. Codex сам допишет/responses.env_key— имя переменной окружения, из которой взять ключ. Не сам ключ.wire_api— протокол. В 0.154 допустимо только одно значение, но лучше писать явно.
Проверка: codex exec "Ответь одним словом: работает". Если в ответе одно слово и строка tokens used — всё подключено.
Девять способов сломать
1. Переменная OPENAI_BASE_URL ничего не делает
Первое, что пробуют все, кто пришёл из мира OpenAI SDK:
OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.example.com/v1 codex exec "..."
Результат:
warning: Falling back from WebSockets to HTTPS transport. unexpected status 401 Unauthorized: Missing bearer or basic authentication in header, url: wss://api.openai.com/v1/responses
ERROR: unexpected status 401 Unauthorized: Missing bearer or basic authentication in header, url: https://api.openai.com/v1/responses
Обратите внимание на url в ошибке: api.openai.com. Переменная проигнорирована полностью, Codex пошёл в дефолтный endpoint. Единственный способ переопределить адрес — секция model_providers.
2. wire_api = “chat” больше не поддерживается
Если ваш прокси умеет только /chat/completions, плохие новости:
Error loading config.toml: `wire_api = "chat"` is no longer supported.
How to fix: set `wire_api = "responses"` in your provider config.
in `model_providers.myproxy.wire_api`
Codex не запустится вообще. Endpoint обязан реализовывать /responses. Это отсекает заметную часть старых прокси и самописных шлюзов — проверьте, что ваш отдаёт /v1/responses, до того как править конфиг.
3. Опечатка в wire_api
Error loading config.toml: unknown variant `response`, expected `responses`
Здесь хотя бы понятно. Хуже, что то же самое сообщение вы получите за любую опечатку в любом enum-поле, и не всегда так же внятно.
4. base_url без /v1
base_url = "https://api.example.com"
Codex дописывает /responses к тому, что дали, и стучится в https://api.example.com/responses. Большинство серверов на этот путь отдают HTML главной страницы:
ERROR: Reconnecting... 1/5
...
ERROR: Reconnecting... 5/5
ERROR: unexpected status 404 Not Found: <!DOCTYPE html><html lang="en">..., url: https://api.example.com/responses
Пять попыток переподключения, потом HTML в терминале. Диагностика — смотреть на url в конце ошибки. Слеш на конце (/v1/) при этом не мешает, проверил.
5. Переменная с ключом не экспортирована
ERROR: Missing environment variable: `MYPROXY_API_KEY`.
Типичный сценарий: ключ лежит в .env проекта, а Codex запущен из другой оболочки. env_key читает окружение процесса, а не файлы.
6. Модель, о которой Codex не знает
warning: Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues.
У Codex есть встроенная таблица моделей: размер контекста, поддержка reasoning, лимиты. Неизвестный id получает «запасные» параметры. Запрос всё равно уйдёт на endpoint, и дальше вы получите его собственную ошибку, если такой модели там нет. Два разных сообщения на одну опечатку.
7. supports_websockets = true на прокси без WebSocket
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 426 Upgrade Required, url: wss://api.example.com/v1/responses
После этого Codex молча падает на HTTPS, и всё работает. Но каждый ход начинается с неудачного рукопожатия. Если endpoint не заявляет WebSocket явно — не включайте.
8. requires_openai_auth = true
В сети советуют ставить его для «совместимости». В 0.154 с заданным env_key эта строка ничего не меняет: запрос ушёл с ключом, ответ пришёл. Строка безвредна, но вводит в заблуждение — уберите.
9. experimental_bearer_token
Ключ можно вписать прямо в конфиг:
experimental_bearer_token = "sk-..."
Работает. Но теперь ключ лежит открытым текстом в ~/.codex/config.toml, который любят копировать целиком «на новую машину» и случайно коммитить вместе с dotfiles. env_key — тот же результат без этого риска.
Бонус: неправильный или обрезанный ключ вернёт 401 с текстом вашего прокси и url .../v1/responses в конце. Этот url — самый быстрый способ убедиться, что запрос вообще дошёл куда надо.
Сколько стоит слово «работает»
Запустил codex exec --json и посмотрел событие turn.completed:
{"usage":{"input_tokens":14809,"cached_input_tokens":0,"cache_write_input_tokens":0,"output_tokens":66,"reasoning_output_tokens":57}}
Один вопрос, один ответ из одного слова — 14 809 токенов на входе. Это системный промпт, описания инструментов, skills и окружение, которые Codex шлёт каждым ходом. Из 66 токенов ответа 57 — рассуждения, на low.
Отсюда практический вывод: на реальной сессии из десятков ходов цена определяется не моделью, а тем, читаются ли эти 15 тысяч из кэша.
И здесь я упёрся в то, чего пока не понял. Тот же endpoint через curl кэширует нормально: два одинаковых запроса подряд — cached_tokens: 0, затем cached_tokens: 7424 из 7618. А второй ход в codex exec resume --last показал cached_input_tokens: 0 при 15 306 на входе. Либо между ходами меняется что-то в начале запроса, либо resume собирает контекст иначе. Если у вас на второй реплике кэш читается — напишите, какая версия и какой endpoint.
Как проверить у себя: codex exec --json "...", потом codex exec --json resume --last "...", и сравнить cached_input_tokens в двух turn.completed.
Чек-лист
model_providerуказан и совпадает с именем секции.base_urlзаканчивается на/v1.wire_api = "responses", и endpoint реально отдаёт/v1/responses.Ключ — через
env_key, переменная экспортирована в той же оболочке.supports_websocketsиrequires_openai_authне трогать.id модели проверен на endpoint, а не скопирован из чужого конфига.
После настройки — один
codex exec --jsonи взгляд на usage.
Чего я не проверял
Профили (-p), локальные провайдеры через --oss и поведение на Windows. Если у вас есть грабли оттуда — в комментарии, добавлю.
Короткие заметки и замеры между статьями пишу в канале: https://t.me/agent_field_notes
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.