Skip to content

fix: preserve gpt-5.5 streamed text when response.completed.output is empty#3

Open
sungwon1110 wants to merge 2 commits into
insightflo:mainfrom
sungwon1110:fix/gpt-5.5-empty-output-text
Open

fix: preserve gpt-5.5 streamed text when response.completed.output is empty#3
sungwon1110 wants to merge 2 commits into
insightflo:mainfrom
sungwon1110:fix/gpt-5.5-empty-output-text

Conversation

@sungwon1110

Copy link
Copy Markdown

Summary

gpt-5.5 (and possibly other newer Codex backend models) emits response.completed with an empty output: [] array even though text is streamed via response.output_text.delta events. The current parser preferentially returns response.completed.response and discards accumulated deltas, so downstream callers (Claude Code via this proxy, etc.) receive an empty content block.

Older models (gpt-5.4, gpt-5.3-codex) populate output[] in the completion event, so the bug only surfaces on newer models.

Symptoms

  • Claude Code through this proxy with model=gpt-5.5 consistently received empty assistant text.
  • Claude Code looped on retry / context-compact because responses appeared blank.
  • tool_diag log line showed codex_text_blocks=0 anthropic_text_blocks=1 — one block emitted, but the text inside was empty.
  • Reproducible with a one-line curl:
    curl -s -X POST http://127.0.0.1:19080/v1/messages \
      -H 'x-api-key: dummy' -H 'anthropic-version: 2023-06-01' \
      -H 'Content-Type: application/json' \
      -d '{"model":"gpt-5.5","max_tokens":50,"stream":false,
           "messages":[{"role":"user","content":"Reply: alpha"}]}'
    # → {"content":[{"type":"text","text":""}]}   (before fix)
    # → {"content":[{"type":"text","text":"alpha"}]}   (after fix)

Root cause

The response.completed event from the Codex backend looks like this for gpt-5.5:

{"type":"response.completed","response":{
  "model":"gpt-5.5",
  "output":[],
  "usage":{"output_tokens":28,"output_tokens_details":{"reasoning_tokens":21}}
}}

Note output: [] even though response.output_text.delta events arrived earlier with the actual text.

Fix

Apply the same defensive merge in both code paths inside src/codex/client.ts:

  • parseSseResponse (used when client requests stream:true)
  • parseFinalResponse (used when client requests stream:false; the proxy still streams to upstream and collects, so the same shape arrives here)

Both paths now:

  1. Accumulate response.output_text.delta deltas alongside capturing finalResponse.
  2. After the stream ends, check whether finalResponse.output contains any non-empty output_text block.
  3. If not, append a synthetic message block with the joined deltas so downstream extractContentFromCodexOutput sees the text.

Older models still hit the original code path (since their output[] is non-empty); behavior unchanged.

Test plan

  • Manual curl against /v1/messages with model=gpt-5.5 (both stream:true and stream:false) returns the expected text.
  • Same call with model=gpt-5.4 still returns text (regression check on the populated-output path).
  • Claude Code session against the patched proxy stops looping on compact and produces visible responses.
  • Add a unit test (happy path + empty-output-with-deltas) — happy to follow up if maintainers prefer.

🤖 Generated with Claude Code

esungwon added 2 commits May 6, 2026 09:45
… empty

gpt-5.5 (and possibly other newer Codex backend models) ships
`response.completed` events with an empty `output: []` array even though
text was streamed via `response.output_text.delta` events. The previous
parser preferred `response.completed.response` and discarded accumulated
deltas, leaving downstream callers (Claude Code, etc.) with empty
content blocks.

Older models (gpt-5.4, gpt-5.3-codex) populate `output[]` in the
completion event so this only triggers when truly missing.

Symptoms before fix:
- Claude Code via this proxy with model=gpt-5.5 received empty text
- Claude Code looped on retry/compact since responses appeared blank
- `tool_diag` logged `codex_text_blocks=0 anthropic_text_blocks=1`
  (one block, but with empty text)

Fix applied to both code paths:
- `parseSseResponse` (when client requests stream:true)
- `parseFinalResponse` (when client requests stream:false; proxy still
  streams to upstream then collects, so same bug)

Both now accumulate output_text.delta chunks alongside capturing the
final response, and inject the joined text as a message block when the
final response's `output[]` lacks any non-empty `output_text`.
…d.output

Followup to the previous text-only fix. The same empty-output[] pattern
on gpt-5.5 also drops function_call items, since they're streamed via
response.output_item.done events but never re-included in response.completed.

Without this, Claude Code via the proxy receives only the model's narration
("I'll check the logs now") with stop_reason="end_turn" and no tool_use
block, so the agent has nothing to execute and the loop stalls.

Refactored both code paths (parseSseResponse, parseFinalResponse) to share
a single mergeStreamedItems helper that:
- collects all response.output_item.done items (covers function_call,
  message-with-text, reasoning, etc.) alongside the text-delta accumulator
- when finalResponse.output is empty, fills it with the collected items
  (preferring full items over a synthesized text-only message block)
- when finalResponse is missing entirely, falls back to the synthesized
  message as before

Verified with curl against /v1/messages stream:false:
- text-only prompt: returns text content
- bash tool prompt: returns tool_use block with name=Bash, input={command}
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.

2 participants