Перейти к содержимому

Форматы API прокси

opencodex предоставляет один локальный прокси сразу в нескольких клиентских диалектах. Клиент Codex может говорить на Responses API, OpenAI-совместимое приложение — на Chat Completions, а Claude Code — на Anthropic Messages, при этом от каждого upstream-провайдера не требуется реализовывать все эти форматы.

Обычный путь преобразования такой:

client dialect → internal Responses model → provider adapter → provider wire format
provider events → internal adapter events → client dialect

Представление Responses — центр этого моста. Нативно совместимые маршруты могут пропускать части перевода и передавать запрос дальше почти как есть, но аутентификация, routing, admission control и safety ответа всё равно происходят на границе прокси. Listener и admission key настраиваются в Конфигурации; если один публичный id модели должен выбирать между несколькими целями, используйте Combos.

Запросы к моделям, изображениям, видео и поиску, содержащие учётные данные, не следуют HTTP-перенаправлениям автоматически, в том числе в пределах одного origin. Укажите конечный URL API вместо перенаправляющего адреса. Сервер не отправляет учётные данные и тело запроса по адресу перенаправления. Существующая обработка ошибок и передача ответа сохраняются; маршруты native Responses и compact могут вернуть клиенту исходные 3xx и Location. Поведение перенаправлений клиента не определяется этой транспортной политикой сервера.

Клиентская поверхность 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 истории-замены Не применяется

Это нативная форма 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’ы обрабатывают только известные им типы и могут отвергнуть функцию, которую их провайдер не умеет выразить.

При 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(), даже если передаётся по сети.

Ответы, отклонённые на этапе аутентификации или проверки допустимости источника, не доходят до этой обёртки и не содержат идентификатора. Поэтому отсутствие заголовка означает, что запрос был отклонён до записи в журнал.

Когда включён 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.

Этот 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, вместо тихого изменения смысла вернётся ошибка.

Эти 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, обрабатываются как прежде. Обновите обнаружение моделей или повторно примените профиль подключённого хаба перед новой попыткой; один лишь повтор не гарантирует разрешения.

Без 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

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.

Привязка аккаунта ниже относится к штатным клиентам 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-аутентификацией владеет сам прокси.

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.

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’ов см. Поверхность подагентов.