fix(chatgpt): accumulate response.output_item.done for non-streaming responses - #26075
Conversation
…eaming response assembly The ChatGPT Codex backend (chatgpt.com/backend-api/codex/responses) streams output items via `response.output_item.done` events and emits a terminal `response.completed` event whose `response.output` is empty -- it only carries metadata (id, status, usage). The existing non-streaming path in `transform_response_api_response` only reads `response.completed.response.output`, so callers of `/v1/responses` (and the `/v1/chat/completions` bridge that depends on it) get a response with `status: completed` and correct token usage but `output: []`. Fix: accumulate items from `response.output_item.done` while iterating the SSE stream and inject them into `response.output` when the terminal event does not populate it. When `response.completed` does carry a populated output (e.g. standard OpenAI Responses API), it wins over the accumulator. This mirrors the upstream Codex CLI client, which maintains an `items_added: Vec<ResponseItem>` and fills it from `OutputItemDone` events before shipping the `Completed` event (see `codex-rs/core/src/client.rs`, the `map_response_stream` function). Tests: - `test_chatgpt_accumulates_output_item_done_when_completed_output_empty` reproduces the ChatGPT backend behavior and asserts items are recovered. - `test_chatgpt_prefers_nonempty_completed_output_over_accumulated` guarantees standard OpenAI Responses API stays unaffected.
Greptile SummaryThis PR fixes an empty Confidence Score: 5/5Safe to merge — focused bug fix, fully backward-compatible, and no P0/P1 findings. The change is narrow and well-contained: it adds an accumulator list local to each transform_response_api_response call and a small static helper. The fallback logic (not output and accumulated) correctly handles all empty/missing output cases while leaving the standard OpenAI path untouched. Both new tests are mock-only and cover the two critical branches. No existing tests were modified. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/chatgpt/responses/transformation.py | Adds output_item.done accumulation with clean extraction into a static _build_completed_response helper; fallback logic is correct and backward-compatible. |
| tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py | Two new mock-only unit tests added: one for the ChatGPT Codex accumulation path, one to verify existing OpenAI behavior is unchanged; no real network calls. |
Sequence Diagram
sequenceDiagram
participant C as LiteLLM Client
participant T as ChatGPTResponsesAPIConfig
participant B as ChatGPT Codex Backend
C->>B: POST /responses (stream=true)
B-->>T: SSE: response.output_item.done (reasoning item)
Note over T: accumulated_output_items.append(reasoning_item)
B-->>T: SSE: response.output_item.done (message item)
Note over T: accumulated_output_items.append(message_item)
B-->>T: SSE: response.completed (output=[])
Note over T: _build_completed_response():
output=[] is falsy → use accumulator
T-->>C: ResponsesAPIResponse(output=[reasoning, message])
Note over C,B: Standard OpenAI path (unchanged)
B-->>T: SSE: response.completed (output=[item1, item2])
Note over T: output is truthy → use as-is, ignore accumulator
T-->>C: ResponsesAPIResponse(output=[item1, item2])
Reviews (1): Last reviewed commit: "fix(chatgpt): accumulate response.output..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Backport the core fix from upstream BerriAI#26219, which re-landed BerriAI#25403, for ChatGPT/Codex non-stream SSE responses whose terminal response.completed payload has output=[]. Collect response.output_item.done events by output_index, use them when response.completed.response.output is empty, add a chat-completions bridge fallback that can recover output_item.done or output_text.done from the raw SSE body, and preserve the shared ChatGPT backend mode so aliases cannot downgrade responses routing to chat. Related upstream references: BerriAI#25429, BerriAI#26075, BerriAI#26219.
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Summary
Fix empty
output: []returned by the ChatGPT subscription provider (chatgpt/*models) on/v1/responsesand/v1/chat/completions(which bridges through Responses).The bug
Calls succeed (
status: completed,usagereports correct token counts), butresponse.outputis always empty:/v1/chat/completionsfails with:Root cause
litellm/llms/chatgpt/responses/transformation.py::transform_response_api_responsereads the terminalresponse.completedSSE event and buildsResponsesAPIResponsefrom itsresponsefield. This works for the standard OpenAI Responses API where that event carries the fully assembled output.The ChatGPT Codex backend (
chatgpt.com/backend-api/codex/responses) behaves differently: it streams output items viaresponse.output_item.doneevents and emitsresponse.completedwith an emptyresponse.output— the terminal event only carries metadata (id/status/usage). The current code ignores theoutput_item.doneevents, so nothing makes it into the assembled non-streaming response.The upstream Codex CLI client handles this by accumulating items while iterating the stream — see
codex-rs/core/src/client.rs'smap_response_stream, which maintainsitems_added: Vec<ResponseItem>and pushes eachOutputItemDoneitem into it before shipping the terminalCompletedevent.Fix
response.output_item.doneevents into a list while parsing the SSE body.response.completedarrives and itsresponse.outputis empty/missing, fall back to the accumulator.response.completedalready carries a populated output (standard OpenAI behavior), it wins — backward compatible._build_completed_responsestatic helper, keeping the main loop readable.Tests
Two new cases in
tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py:test_chatgpt_accumulates_output_item_done_when_completed_output_empty— reproduces the ChatGPT Codex backend SSE sequence (reasoning item + message item viaoutput_item.done, terminalresponse.completedwith empty output) and asserts all items are recovered andoutput_textreflects the message.test_chatgpt_prefers_nonempty_completed_output_over_accumulated— ensures standard OpenAI Responses behavior is untouched when the terminal event does carry output.All 27 tests in
tests/test_litellm/llms/chatgpt/pass locally. Ruff + black clean on the changed files.Manual verification
Patched
ChatGPTResponsesAPIConfig.transform_response_api_responsevia monkey-patch against a live local proxy (1.83.10) authenticated with a real ChatGPT Plus subscription:/v1/responseschatgpt/gpt-5.4output: []"hello world"/v1/responseschatgpt/gpt-5.4-minioutput: []/v1/responseschatgpt/gpt-5.3-codex-sparkoutput: []/v1/chat/completionschatgpt/gpt-5.4-miniUnknown items in responses API response: []"chat ok"Test plan
pytest tests/test_litellm/llms/chatgpt/)ruff checkclean on changed filesblack --checkclean on changed files