The Daily Newsstand · Free, Always
Tuesday, September 15, 2026

Codex CLI через свой endpoint: config.toml по строкам и девять способов его сломать

Translate

В первой статье я разбирал, куда ходит 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.

Чек-лист

  1. model_provider указан и совпадает с именем секции.

  2. base_url заканчивается на /v1.

  3. wire_api = "responses", и endpoint реально отдаёт /v1/responses.

  4. Ключ — через env_key, переменная экспортирована в той же оболочке.

  5. supports_websockets и requires_openai_auth не трогать.

  6. id модели проверен на endpoint, а не скопирован из чужого конфига.

  7. После настройки — один codex exec --json и взгляд на usage.

Чего я не проверял

Профили (-p), локальные провайдеры через --oss и поведение на Windows. Если у вас есть грабли оттуда — в комментарии, добавлю.

Короткие заметки и замеры между статьями пишу в канале: https://t.me/agent_field_notes

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.