From 3c698caf83211b67f36879388f8782d3e50cc730 Mon Sep 17 00:00:00 2001 From: Michael Engle Date: Sat, 9 May 2026 15:32:24 -0700 Subject: [PATCH] fix(chatgpt): aggregate output items for non-streaming responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a non-streaming client calls /v1/responses (or /v1/chat/completions bridged to responses) for the chatgpt provider, the proxy reads the SSE stream from chatgpt.com/backend-api/codex and aggregates it into a single ResponsesAPIResponse. The aggregator currently builds the response only from the final `response.completed` event's `response` field — but the ChatGPT backend leaves `response.output` empty on that final event and emits the actual items in earlier `response.output_item.done` events. The result: HTTP 200, status `completed`, usage populated, but `output: []` — clients see no content even though the call succeeded and tokens were billed. Fix: collect items from `response.output_item.done` events as the SSE stream is parsed, and if the final `response.completed` payload's `output` is empty, fall back to the collected list. No-op when the backend does populate `output` directly (e.g. future API versions). Verified against a ChatGPT Pro subscription via Codex CLI OAuth tokens: both /v1/responses and /v1/chat/completions (bridged) now return the assistant's text content for chatgpt/gpt-5.4 and chatgpt/gpt-5.3-codex. Streaming path is unchanged. --- litellm/llms/chatgpt/responses/transformation.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index 66acd933416..6aae2c48b58 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -134,6 +134,7 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): completed_response = None error_message = None + collected_output_items: list = [] for chunk in body_text.splitlines(): stripped_chunk = CustomStreamWrapper._strip_sse_data_from_chunk(chunk) if not stripped_chunk: @@ -150,10 +151,21 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): if not isinstance(parsed_chunk, dict): continue event_type = parsed_chunk.get("type") + if event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE: + item = parsed_chunk.get("item") + if isinstance(item, dict): + collected_output_items.append(item) + continue if event_type == ResponsesAPIStreamEvents.RESPONSE_COMPLETED: response_payload = parsed_chunk.get("response") if isinstance(response_payload, dict): response_payload = dict(response_payload) + # ChatGPT backend leaves response.output empty on the final + # `response.completed` event and emits items via earlier + # `response.output_item.done` events; reassemble here so + # non-streaming clients receive a populated output array. + if not response_payload.get("output") and collected_output_items: + response_payload["output"] = collected_output_items if "created_at" in response_payload: response_payload["created_at"] = _safe_convert_created_field( response_payload["created_at"]