fix: preserve gpt-5.5 streamed text when response.completed.output is empty#3
Open
sungwon1110 wants to merge 2 commits into
Open
fix: preserve gpt-5.5 streamed text when response.completed.output is empty#3sungwon1110 wants to merge 2 commits into
sungwon1110 wants to merge 2 commits into
Conversation
… 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}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
gpt-5.5(and possibly other newer Codex backend models) emitsresponse.completedwith an emptyoutput: []array even though text is streamed viaresponse.output_text.deltaevents. The current parser preferentially returnsresponse.completed.responseand 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) populateoutput[]in the completion event, so the bug only surfaces on newer models.Symptoms
model=gpt-5.5consistently received empty assistant text.tool_diaglog line showedcodex_text_blocks=0 anthropic_text_blocks=1— one block emitted, but the text inside was empty.curl:Root cause
The
response.completedevent from the Codex backend looks like this forgpt-5.5:{"type":"response.completed","response":{ "model":"gpt-5.5", "output":[], "usage":{"output_tokens":28,"output_tokens_details":{"reasoning_tokens":21}} }}Note
output: []even thoughresponse.output_text.deltaevents 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 requestsstream:true)parseFinalResponse(used when client requestsstream:false; the proxy still streams to upstream and collects, so the same shape arrives here)Both paths now:
response.output_text.deltadeltas alongside capturingfinalResponse.finalResponse.outputcontains any non-emptyoutput_textblock.messageblock with the joined deltas so downstreamextractContentFromCodexOutputsees the text.Older models still hit the original code path (since their
output[]is non-empty); behavior unchanged.Test plan
curlagainst/v1/messageswithmodel=gpt-5.5(bothstream:trueandstream:false) returns the expected text.model=gpt-5.4still returns text (regression check on the populated-output path).🤖 Generated with Claude Code