Skip to content

fix(apicompat): repair codex Responses<->Anthropic/ChatCompletions conversion#2916

Open
hanyunsushi wants to merge 2 commits into
Wei-Shaw:mainfrom
hanyunsushi:fix/codex-responses-conversion
Open

fix(apicompat): repair codex Responses<->Anthropic/ChatCompletions conversion#2916
hanyunsushi wants to merge 2 commits into
Wei-Shaw:mainfrom
hanyunsushi:fix/codex-responses-conversion

Conversation

@hanyunsushi
Copy link
Copy Markdown

Problem

When a Responses API client (e.g. codex) is routed through an Anthropic upstream or a chat/completions-only upstream, requests fail or render blank due to several bugs in the apicompat conversion layer. Account "test connection" passes because it uses a self-constructed simple request that never exercises the Responses input parsing, masking these issues.

Related: #2913

Root causes & fixes

Request direction (ResponsesInputItem)

  1. Arguments/Output typed as string — codex sends function_call.arguments as a JSON object and function_call_output.output as an array. The strict string type caused json.Unmarshal to fail (502 on the Anthropic path) or rawString to silently drop the content (chat/completions path). Both are now json.RawMessage with normalization helpers (normalizeResponsesArguments, extractResponsesOutputText, responsesArgumentsToChatString); reverse-direction builders wrap strings with jsonRawString to preserve the OpenAI wire format.

  2. Tools without parameters produced a null input_schema — codex namespace tools (MCP/agent) and bare web_search carry no parameters; Anthropic 422s on a null schema. Now backfilled with an empty object schema.

  3. web_search emitted as the Anthropic server tool web_search_20250305 — some Anthropic-compatible upstreams don't implement server tools (422). Now emitted as a plain function tool.

  4. Top-level instructions and developer-role items were dropped — codex puts its primary system prompt in instructions; the old code only read system-role items, leaving the model with no instructions and (worse) leaking developer input_text blocks into a user message. Both now map to the Anthropic system field.

  5. Empty/whitespace system + string-form system — an empty system 422s; and a string-form system combined with tools 422s on some upstreams. System is now omitted when empty and emitted in array form ([{"type":"text","text":...}]), which is spec-valid and what the official Claude Code client uses.

  6. reasoning_effort: "xhigh" — codex sends xhigh for top-tier models; chat/completions upstreams only accept low/medium/high and 400 on xhigh. Now normalized to high.

Response direction (streaming → Responses events)

  1. Missing response.content_part.added/.done — strict clients (codex) need a content part before output_text.delta; without it text deltas have nothing to attach to and the client renders nothing. Now emitted around the text block on both conversion paths.

  2. output_item.done lacked content/tool-call fields — codex collects the final output and tool calls from OutputItemDone items, not from delta events. Message items now carry their full content, and function_call items carry call_id/name/arguments.

  3. Ghost empty deltas — some upstreams send a leading {"content":""} chunk; emitting it produced an empty output_text.delta and a premature message item, leaving codex stuck on "thinking" with no output. Empty content/reasoning deltas are now skipped.

Tests

Adds coverage for all of the above: polymorphic arguments/output (string/object/array), tool schema backfill, web_search as function tool, instructions/developer → system, empty-system omission, xhigh normalization, content_part event ordering, output_item content/tool-call fields, and empty-delta skipping. All existing apicompat tests pass; gofmt/go vet clean.

…nversion

Several bugs in the OpenAI Responses API conversion layer caused requests
from codex (and other Responses clients) to fail or render blank when routed
through Anthropic upstreams or chat/completions-only upstreams.

Request direction (ResponsesInputItem):
- Arguments/Output were typed string but clients send object/array, causing
  502 (Anthropic path) or silent data loss (chat/completions path). Both are
  now json.RawMessage with normalization helpers.
- Tools with no parameters (e.g. namespace/web_search) produced a null
  input_schema -> Anthropic 422. Now backfilled with an empty object schema.
- web_search was emitted as the Anthropic server tool web_search_20250305,
  which some Anthropic-compatible upstreams do not implement (422). It is now
  a plain function tool.
- Top-level instructions and developer-role items were dropped instead of
  mapped to the Anthropic system field; empty/whitespace system is omitted.
- system is now emitted in array form, which some upstreams require when
  tools are present.
- reasoning_effort 'xhigh' is normalized to 'high' for chat/completions
  upstreams that only accept low/medium/high.

Response direction (streaming -> Responses events):
- Emit response.content_part.added/done around output_text so strict clients
  (codex) have a content part to attach text to.
- output_item.done for message items now carries the full content, and for
  function_call items carries call_id/name/arguments, since codex collects
  final output and tool calls from OutputItemDone items.
- Skip empty-string content/reasoning deltas that produced ghost
  output_text.delta events and a blank render.

Adds polymorphic/tools/system/streaming test coverage.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

All contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

…ponses path

The ChatCompletions->Responses streaming bridge emitted output_item.added and
function_call_arguments.delta per tool call but never the terminal
function_call_arguments.done / output_item.done. codex collects tool calls
from OutputItemDone items, so an unterminated tool call left it stalled with a
blank render (observed when asking mimo-v2.5-pro to e.g. open a browser).
FinalizeChatCompletionsResponsesStream now emits both terminal events with
call_id/name/arguments.

Also fixes argument duplication: a tool call whose first chunk carried both
name and arguments had its arguments counted twice (copyCall already held them
and the accumulator appended them again), producing invalid JSON like
{...}{...}. New tool-call state now starts with empty arguments.
@hanyunsushi
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant