fix(chatgpt): aggregate output items for non-streaming responses

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.
This commit is contained in:
Michael Engle 2026-05-09 15:32:24 -07:00
parent fa81017e12
commit 3c698caf83

View file

@ -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"]