Форматы API прокси
opencodex предоставляет один локальный прокси сразу в нескольких клиентских диалектах. Клиент Codex может говорить на Responses API, OpenAI-совместимое приложение — на Chat Completions, а Claude Code — на Anthropic Messages, при этом от каждого upstream-провайдера не требуется реализовывать все эти форматы.
Обычный путь преобразования такой:
client dialect → internal Responses model → provider adapter → provider wire formatprovider events → internal adapter events → client dialectПредставление Responses — центр этого моста. Нативно совместимые маршруты могут пропускать части перевода и передавать запрос дальше почти как есть, но аутентификация, routing, admission control и safety ответа всё равно происходят на границе прокси. Listener и admission key настраиваются в Конфигурации; если один публичный id модели должен выбирать между несколькими целями, используйте Combos.
Перенаправления upstream
Заголовок раздела «Перенаправления upstream»Запросы к моделям, изображениям, видео и поиску, содержащие учётные данные, не следуют HTTP-перенаправлениям автоматически, в том числе в пределах одного origin. Укажите конечный URL API вместо перенаправляющего адреса. Сервер не отправляет учётные данные и тело запроса по адресу перенаправления. Существующая обработка ошибок и передача ответа сохраняются; маршруты native Responses и compact могут вернуть клиенту исходные 3xx и Location. Поведение перенаправлений клиента не определяется этой транспортной политикой сервера.
Обзор endpoint’ов
Заголовок раздела «Обзор endpoint’ов»| Клиентская поверхность | Endpoint | Успешный non-stream результат | Успешный результат потока или сокета |
|---|---|---|---|
| OpenAI Responses | POST /v1/responses |
Responses JSON | Responses SSE или текстовые Responses JSON frame’ы по WebSocket |
| OpenAI Chat Completions | POST /v1/chat/completions |
chat.completion JSON |
chat.completion.chunk SSE, заканчивающийся [DONE] |
| Anthropic Messages | POST /v1/messages |
Anthropic message JSON |
Anthropic Messages SSE |
| Подсчёт токенов Anthropic | POST /v1/messages/count_tokens |
{ "input_tokens": number } |
Не применяется |
| Обнаружение моделей | GET /v1/models |
Каталог или явно запрошенный снимок Desktop | Не применяется |
| Голос и Realtime | POST /v1/live, POST /v1/realtime/calls |
Ответ создания вызова после ретрансляции | Отдельный sideband WebSocket ретранслирует frame’ы в обе стороны |
| Компактизация Responses | POST /v1/responses/compact |
JSON истории-замены | Не применяется |
POST /v1/responses
Заголовок раздела «POST /v1/responses»Это нативная форма data plane для opencodex. Тело запроса должно быть JSON-объектом с непустым
model. Поле input может быть строкой или массивом Responses item’ов.
Разрешённые поля запроса
Заголовок раздела «Разрешённые поля запроса»| Область | Допустимая форма |
|---|---|
| Модель и ввод | Обязательный непустой model; необязательный строковый input или массив item’ов |
| Элементы сообщений | Сообщения user, developer, system и assistant; строковое содержимое или типизированные content-блоки, допустимые для этой роли |
| Блоки содержимого | Текст, входные изображения, входные файлы, выходной текст, отказы и блоки сводки/текста рассуждений там, где их допускает родительский item |
| История инструментов | Item’ы function_call, function_call_output, custom_tool_call и custom_tool_call_output |
| Инструменты | Function tool’ы плюс свободные built-in или hosted tool entry; tool_choice принимает auto, none, required, именованные function/custom choice, hosted choice или allowed_tools |
| Рассуждения | reasoning.effort и reasoning.summary (auto, concise, detailed или none) |
| Продолжение и кэширование | previous_response_id, store и prompt_cache_key |
| Управление генерацией | max_output_tokens, temperature, top_p, stop, presence_penalty и frequency_penalty |
| Сервис и исполнение | stream, service_tier, parallel_tool_calls, instructions, metadata и user |
| Расширенные поля Responses | background, include, prompt, text и truncation принимаются на совместимых маршрутах |
Неизвестные типы item’ов принимаются как свободные typed-item’ы для forward compatibility. Translated-adapter’ы обрабатывают только известные им типы и могут отвергнуть функцию, которую их провайдер не умеет выразить.
JSON и SSE-вывод
Заголовок раздела «JSON и SSE-вывод»При stream: true ответ идёт как text/event-stream. Мост испускает события Responses вроде
response.created, delta-события для output item’ов и текста/tool’ов, а также ровно одно
терминальное событие response.completed, response.failed или response.incomplete. Обычный
поток заканчивается data: [DONE].
При stream: false или при отсутствии stream те же события адаптера собираются в один JSON
Responses. Обе формы сохраняют выбранную модель, output item’ы, terminal status и usage.
Клиентские frame’ы Responses SSE ограничены 4 MiB на frame, считая сырые байты до разделителя SSE-блока. В HTTP незавершённый upstream-frame, превысивший этот предел, завершается fail-closed синтетическим событием response.failed, после которого идёт data: [DONE]. В мосте Responses WebSocket то же условие даёт 502 websocket_protocol_error и отменяет upstream-reader. Если полноценный terminal-frame Responses уже получен, он остаётся авторитетным: слишком большие или некорректные байты после него отбрасываются и не заменяют завершённый ход транспортной ошибкой.
Каждый terminal usage-объект Responses всегда включает оба detail-объекта, даже если провайдер их не сообщил:
{ "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens_details": { "reasoning_tokens": 0 }}Когда данные есть, input_tokens_details может также содержать cache_write_tokens. Эти detail
объекты присутствуют всегда ради совместимости со strict-клиентами Responses; ноль может означать
«не сообщено», а не обязательно «провайдер такой работы не делал».
Сопоставление ответа с записью запроса в журнале
Заголовок раздела «Сопоставление ответа с записью запроса в журнале»Каждый допущенный HTTP-ответ Responses содержит заголовок x-opencodex-request-id с созданным
прокси идентификатором вида ocx-<32 hex>. Это ключ, связывающий ответ с соответствующей строкой
в журнале запросов и отчётах об использовании.
Прокси всегда создаёт это значение сам и перезаписывает любой идентификатор, переданный вызывающей
стороной или возвращённый вышестоящим сервером, поэтому оно уникально для этого прокси и ему можно
доверять как ключу сопоставления. Заголовок указан в Access-Control-Expose-Headers, благодаря чему
браузерный JavaScript может читать его при междоменных запросах: без этого пользовательский
заголовок с префиксом x- невидим для response.headers.get(), даже если передаётся по сети.
Ответы, отклонённые на этапе аутентификации или проверки допустимости источника, не доходят до этой обёртки и не содержат идентификатора. Поэтому отсутствие заголовка означает, что запрос был отклонён до записи в журнал.
WebSocket-upgrade на том же пути
Заголовок раздела «WebSocket-upgrade на том же пути»Когда включён websockets, клиент может выполнить upgrade для /v1/responses, а не открывать
HTTP POST. Аутентификация и origin admission происходят во время WebSocket-handshake и не
повторяются внутри каждого frame’а.
Клиент отправляет текстовые JSON-frame’ы:
{ "type": "response.create", "model": "provider/model", "input": "Hello", "tools": [], "generate": true}Всё, кроме type, становится телом Responses-запроса, а proxy принудительно включает streaming
для этого хода. Новый response.create отменяет и вытесняет предыдущий ход на этом socket’е.
response.processed принимается как no-op acknowledgement. Неразбираемые frame’ы и посторонние
типы игнорируются.
Серверные frame’ы — это текстовые JSON-frame’ы. Успешный streamed-output использует те же JSON
payload’ы, которые появились бы в строках SSE data:, только без SSE-обёртки и без [DONE].
Не-streaming внутренний результат переупаковывается в response.created, затем идёт ноль или
больше frame’ов response.output_item.done, после чего следует terminal frame. Ошибки используют
такую форму:
{ "type": "error", "status": 502, "error": { "type": "upstream_error", "message": "..." }, "headers": {}}Warmup-frame с generate: false upstream не вызывает. Он возвращает синтетические
response.created и response.completed с пустым response id и без output.
POST /v1/chat/completions
Заголовок раздела «POST /v1/chat/completions»Этот endpoint принимает OpenAI-совместимые запросы Chat Completions с обязательным model и
непустым массивом messages. Он переводит system-, user-, assistant- и tool-сообщения во
внутренние Responses item’ы; переводит function tool’ы, tool choice, изображения, reasoning effort
и поддерживаемые response format’ы; запускает обычный pipeline маршрутизации Responses; а затем
переводит результат обратно.
Не-streaming output имеет object: "chat.completion". Streaming-вывод идёт как SSE-объекты с
object: "chat.completion.chunk", delta’ами choice, terminal choice с finish_reason и
data: [DONE]. Информация о tool call’ах и usage тоже переводится обратно там, где её несут
исходные события.
Поскольку внутренний путь исполнения основан на Responses, adapter провайдера может сузить поддерживаемый набор функций. Например, если feature запроса нельзя выразить через выбранный adapter, вместо тихого изменения смысла вернётся ошибка.
POST /v1/messages и count_tokens
Заголовок раздела «POST /v1/messages и count_tokens»Эти endpoint’ы говорят на диалекте Anthropic Messages, который используют Claude Code и совместимые клиенты. Большинство запросов переводится в Responses, маршрутизируется обычным образом, а затем обратно в Anthropic JSON или Anthropic SSE.
Повторная передача reasoning в преобразуемых запросах Messages использует общий бюджет
преобразования запроса, включая копии при кодировании и декодировании. При превышении лимита
возвращается HTTP 413 с translation_buffer_limit; подписи и непрозрачные данные reasoning
не обрезаются для соблюдения лимита. Для нативного Anthropic passthrough действует отдельный
контракт ограничения размера тела.
Нативный Anthropic passthrough допустим только когда одновременно выполняются все условия:
- native passthrough не отключён в конфигурации Claude Code;
- запрошенная модель начинается с
claudeилиanthropic; - запрос несёт нативный bearer Anthropic или
x-api-key; - на non-loopback listener запрос также несёт валидный proxy admission только в
x-opencodex-api-key; и - ни один alias или model map не забирает этот model id в routed-цель.
Если запрос подходит, он пересылается в Anthropic dialect, и нативные beta-header’ы, thinking signature’ы и subscription identity проходят сквозь систему end to end. В противном случае запрос идёт через round-trip Responses.
Dedicated admission-header никогда не пересылается. Proxy admission secret в Authorization или
x-api-key также удаляется, а отдельный настоящий credential Anthropic сохраняется. Неоднозначные
credential-заголовки, объединённые запятыми, завершаются fail closed.
POST /v1/messages/count_tokens использует те же правила разрешения модели и то же решение о
passthrough. Native-eligible-запрос пересылается в count-endpoint Anthropic. Для остальных запросов
используется локальная документированная оценка по system content, messages и tools, и
возвращается:
{ "input_tokens": 123 }Неразрешённый Desktop ID в формате даты может быть реальным нативным модельным ID,
отсутствующим в результатах обнаружения. Если имеющихся данных недостаточно для разрешения ID,
Messages и count-tokens возвращают HTTP 503 с фиксированной ошибкой desktop_model_mapping_unavailable;
это не доказывает недействительность модели. Неизвестные старые хеш-псевдонимы по-прежнему дают
HTTP 400. В обоих случаях дата не удаляется и другая маршрутизация не подставляется. Известные ID,
зарегистрированные сопоставления и точные записи modelMap, включая распознанные реальные
нативные ID, обрабатываются как прежде. Обновите обнаружение моделей или повторно примените
профиль подключённого хаба перед новой попыткой; один лишь повтор не гарантирует разрешения.
GET /v1/models
Заголовок раздела «GET /v1/models»Без format=desktop-config действуют следующие обычные контракты каталога:
| Контракт | Триггер | Форма верхнего уровня | Поведение id модели |
|---|---|---|---|
| Список моделей Anthropic | Заголовок anthropic-version или ?flavor=anthropic, без client_version |
{ "data": [...] } с Anthropic model-info entry |
Claude Code получает читаемые id; Desktop может получать семейство alias’ов, специфичное для профиля |
| Каталог Codex | Query-параметр client_version |
{ "models": [...] } |
Нативные и маршрутизируемые записи несут более богатые поля каталога Codex: visibility, effort, WebSocket и multi-agent metadata |
| Обычный список OpenAI | Ни один триггер не сработал | { "object": "list", "data": [...] } |
Видимые native-id идут без префикса; routed-id — как alias или provider/model |
Снимок конфигурации Desktop
Заголовок раздела «Снимок конфигурации Desktop»GET /v1/models?ids=desktop&format=desktop-config явно выбирает снимок Desktop независимо
от user-agent. Ответ — { "version": 1, "models": [...] } с Cache-Control: no-store.
Клиент отправляет Accept: application/json, anthropic-version: 2023-06-01 и существующие
учётные данные для доступа к данным; администраторский токен и загрузка профиля не нужны.
Элементы — модели конфигурации Desktop, выданные хабом, а не строки каталога Codex.
Этот формат вместе с ids=cli или любым client_version возвращает HTTP 400. Без выбора
формата обычные контракты выше не меняются. При выключенном Claude ответ имеет вид
{ "version": 1, "models": [] }: подключённый Desktop apply считает модели недоступными и
не записывает заменяющий профиль. Старые хабы с обычным каталогом вместо версии 1 не
поддерживаются; перехода к локально созданным ID нет.
Снимок остаётся списком моделей только для чтения, а не API ротации или загрузки профиля.
Миграция ключа Desktop, восстановление и отключение используют существующий цикл подключения.
Ротация сохраняет модели и выбор; CLI-поле rotation различает committed и rolled_back.
Отключение восстанавливает управляемые настройки либо сообщает о стандартном fallback для
распознанного старого профиля, сохраняя пользовательские поля и более поздний действительный
выбор. Конфликты и неполное восстановление не считаются завершением. Перезапустите Desktop для
чтения изменений; отключение не отзывает ключ хаба автоматически.
См. руководство Desktop. Повторная передача thinking и кеш остаются
отдельно в #3719.
POST /v1/live и Realtime sideband
Заголовок раздела «POST /v1/live и Realtime sideband»Привязка аккаунта ниже относится к штатным клиентам Codex. Для диктовки и GPT-Live с внешним API-ключом см. спецификацию аудио API на английском.
В Connections > API keys есть отдельные разделы диктовки и голоса. Ключ данных остаётся только в памяти формы. Диктовка отправляет выбранный файл, а проверка голоса ждёт подтверждения сеанса без микрофона. Наличие конфигурации не означает успешное подключение.
POST /v1/live принимает surface Frameless call-creation из ChatGPT/Codex App.
POST /v1/realtime/calls принимает surface call-creation OpenAI Realtime. opencodex выбирает
подходящий маршрут семейства OpenAI, нормализует запрос call-creation под нужный режим
upstream-аутентификации и ретранслирует ограниченный ответ.
После создания call клиент может подключиться к sideband WebSocket в любой поддерживаемой входной форме:
/v1/live/{callId}/v1/realtime/calls/{callId}/v1/realtime?call_id={callId}
Proxy нормализует upstream-join URL и затем прозрачно ретранслирует text- и binary-frame’ы в обе стороны. Клиентские protocol-header’ы сохраняются, а upstream-аутентификацией владеет сам прокси.
POST /v1/responses/compact
Заголовок раздела «POST /v1/responses/compact»Compaction возвращает replacement history для клиентов, которым нужно сократить длинный Responses conversation.
| Тип маршрута | Поведение |
|---|---|
| Canonical ChatGPT или официальный маршрут OpenAI | Пересылает запрос в нативный endpoint /responses/compact с разрешённым аккаунтом и model-authentication |
| Любая другая routed-модель | Запускает внутренний, не-streaming, без-tool’овый compaction-turn с compaction_trigger; требует ровно один синтетический item compaction, чей encrypted_content — это envelope ocx1:; затем декодирует это summary обратно в replacement history v1 |
Нативные compact-ответы буферизуются с максимумом 32 MiB, включая ответы, у которых один только
заявленный Content-Length уже превышает лимит. Для compaction есть такие специфические ошибки:
| Статус | Тип или код | Значение |
|---|---|---|
| 400 | invalid_request_error |
Некорректная форма JSON/body или отсутствует модель |
| 404 | invalid_request_error |
Запрошенную модель нельзя маршрутизировать |
| 499 | client_cancelled |
Клиент отменил запрос во время forwarding или buffering |
| 502 | compact_response_too_large |
Нативный compact-output превысил 32 MiB |
| 502 | upstream_error |
Сбой соединения, чтения или synthetic compaction-turn |
| 502 | invalid_response_error |
Synthetic-turn не создал ровно один корректный непустой item compaction ocx1: |
Матрица аутентификации
Заголовок раздела «Матрица аутентификации»На bind’е только для loopback data-plane admission не требует настроенного ключа. На удалённой
привязке используйте матрицу ниже. «Dedicated» означает X-OpenCodex-API-Key; остальные столбцы —
это Authorization: Bearer ... и x-api-key.
| Поверхность | Выделенный | Bearer | x-api-key |
|---|---|---|---|
/v1/responses HTTP и WebSocket |
Принимается | Принимается | Отклоняется |
/v1/responses/compact |
Принимается | Принимается | Отклоняется |
/v1/chat/completions |
Принимается | Принимается | Отклоняется |
/v1/messages и /v1/messages/count_tokens |
Принимается | Принимается | Принимается |
/v1/models |
Принимается | Принимается | Принимается |
/v1/live, /v1/realtime/calls и sideband-join’ы |
Принимается | Принимается | Принимается |
Запросы Responses и Chat принимают ключ прокси в выделенном заголовке или поле Bearer. На нативных маршрутах bearer допуска заменяется выбранными сохранёнными учётными данными Codex, а на остальных удаляется. Он никогда не используется для аутентификации upstream. Если передаётся отдельный bearer провайдера, ключ прокси следует указать в выделенном заголовке.
Маршрут Cursor без ключа и без OAuth может использовать отдельный bearer вызывающей стороны, но не секрет прокси и не автоматически добавленную аутентификацию ChatGPT main. Выбор Combo/policy и фактические изменения маршрута shadow/thread-spawn не передают исходные учётные данные вызывающей стороны новым целям. Каноническая маршрутизация OpenAI может восстановить единственный bearer вызывающей стороны, не являющийся ключом прокси, после внутреннего изменения маршрута только если его JWT содержит claim учётной записи ChatGPT и любой явно указанный заголовок учётной записи соответствует этому claim. Для передачи аутентификации вызывающей стороны в необязательные sidecar OpenAI необходимы один JWT и явно указанный соответствующий chatgpt-account-id. Непрозрачные bearer не восстанавливаются после изменения маршрута даже при наличии явно указанного заголовка учётной записи. В остальных случаях конечной цели нужны собственные настроенные, OAuth или сохранённые учётные данные; иначе запрос завершается локальной ошибкой. Один маркер thread-spawn без изменения маршрута не удаляет учётные данные.
Для Chat-запроса к Cursor без настроенного ключа необязательное дополнение сохранённой аутентификацией main откладывается до фактического планирования помощника OpenAI с доступным каноническим кандидатом Direct. Независимый запрос Cursor не удерживает native main через этот механизм и не задерживает смену профиля. Учётные данные помощника подчиняются ограничениям запуска и смены профиля и остаются отдельно от bearer Cursor. Помощники Pool и помощники с явно указанным аккаунтом сохраняют прежний выбор аккаунта.
Claude replay сохраняет аутентификацию main только в снимке в памяти, владение которым обеспечено текущим turn, и восстанавливает её только для конечного канонического маршрута ChatGPT.
Общий словарь ошибок
Заголовок раздела «Общий словарь ошибок»Когда это нужно, ошибки используют envelope клиентского диалекта, но значения status/code остаются стабильными:
| Статус | Тип или код | Значение |
|---|---|---|
| 401 | authentication_error |
Отсутствует обязательный credential для proxy-admission или он неверен |
| 403 | origin_rejected |
Data-plane запрос или WebSocket-upgrade Responses/OpenAI пришёл с запрещённого origin |
| 503 | combo_unavailable |
Все цели выбранной combo недоступны, в cooldown, отключены или иным образом не подходят |
| 400 | unreadable_encrypted_agent_task |
У шифрованной задачи воркера v2 нет ни подходящей канонической цели ChatGPT, ни прямой Responses-цели с аутентификацией по ключу, явно доверенной через allowEncryptedV2AgentTasks: true и способной её обработать |
| 426 | upgrade_required |
Транспорт Responses WebSocket выключен или upgrade не удался; используйте HTTP |
Сбои, пришедшие с Anthropic-side, отрисовываются в error envelope Anthropic, поэтому отклонение
origin превращается в 403 permission_error, а не в OpenAI-style body origin_rejected.
Гигиена encrypted_content
Заголовок раздела «Гигиена encrypted_content»Proxy относится к подлинному ciphertext backend’а как к непрозрачным данным. Структурно валидный ciphertext сохраняется байт в байт: opencodex его не расшифровывает, не переводит содержимое и не перешифровывает для другого провайдера.
Исторически некоторые agent hook’и клали plaintext control text в слот encrypted_content. Ради
совместимости proxy отделяет такой plaintext в текстовые части, сохраняя нетронутыми все
структурно валидные фрагменты Fernet. Если после такой починки у agent_message не остаётся ни
одной шифрованной части, сообщение становится обычным user-message. Если текущая задача v2
остаётся по-настоящему зашифрованной, а выбранная routed-цель не умеет читать ciphertext нативного
ChatGPT, opencodex завершит запрос ошибкой unreadable_encrypted_agent_task, вместо того чтобы
отправить нечитаемые байты этому провайдеру. О поведении клиента вокруг worker-task’ов см.
Поверхность подагентов.

