Skip to content

responses.stream() crashes with NoneType iteration when response.completed payload has output=null #3321

@Matt-CL

Description

@Matt-CL

Confirm this is an issue with the Python library and not an underlying OpenAI API

  • This is an issue with the Python library

(See "Note on backend" below — the upstream backend payload is the root cause, but the SDK can and should handle it gracefully because the data needed to reconstruct the response is already in the stream snapshot.)

Describe the bug

client.responses.stream() raises TypeError: 'NoneType' object is not iterable from parse_response when a response.completed SSE event arrives with response.output == null.

Traceback:

File ".../openai/lib/streaming/responses/_responses.py", line 360, in accumulate_event
    self._completed_response = parse_response(
File ".../openai/lib/_parsing/_responses.py", line 61, in parse_response
    for output in response.output:
TypeError: 'NoneType' object is not iterable

The bug is two-fold:

  1. parse_response is not defensive. _parsing/_responses.py line 61 iterates response.output directly, but Response.output is Optional in the typed schema. Any caller passing a Response with output=None crashes.

  2. The streaming accumulator throws away information. streaming/responses/_responses.py line 360 calls parse_response(response=event.response, ...), passing the response.completed event's payload directly. If the server's final payload has output: null, that null reaches parse_response. But the accumulator's __current_snapshot (the local snapshot variable a few lines above) already contains the correct list of output items, reconstructed from the preceding response.output_item.added / response.output_item.done events. The information is right there in the SDK's hand — it's just not being used as a fallback.

Note on backend

This was hit in the wild against https://chatgpt.com/backend-api/codex/responses (the Codex CLI backend exposed for ChatGPT-OAuth users on the gpt-5.3-codex model, used by community wrappers like Hermes Agent). That backend ships output: null on the final response.completed.response payload, even though it correctly streamed all the intermediate response.output_item.added/done, response.reasoning_summary_text.*, and response.function_call_arguments.* events. So while the strictest reading is "the backend is wrong", the SDK breaks unrecoverably for users of that backend, and the fix is one defensive line + one snapshot fallback.

To Reproduce

  1. Point an OpenAI client at a Responses endpoint that returns output: null on the final response.completed SSE event. (The Codex backend at https://chatgpt.com/backend-api/codex/responses reproduces this; any mock server returning the same shape will too.)
  2. Call client.responses.stream(model=..., input=[...], ...) and iterate.
  3. The stream emits response.created, then response.output_item.added / response.output_item.done for each item (reasoning + function_call in the repro), then response.completed with response.output == null.
  4. On step 3 the SDK raises TypeError: 'NoneType' object is not iterable.

A mock SSE that's enough to repro:

event: response.created
data: {\"type\":\"response.created\",\"response\":{\"id\":\"r_1\",\"object\":\"response\",\"output\":[],\"status\":\"in_progress\", ...}}

event: response.output_item.added
data: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"type\":\"reasoning\", ...}}

event: response.output_item.done
data: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"type\":\"reasoning\", ...}}

event: response.completed
data: {\"type\":\"response.completed\",\"response\":{\"id\":\"r_1\",\"object\":\"response\",\"output\":null,\"status\":\"completed\", ...}}

For comparison: a raw urllib.request POST against the same endpoint parses fine — the events are valid JSON, the stream is well-formed, the only issue is that final output: null.

Expected behavior

Either:

  • (a) parse_response tolerates response.output is None (treat as empty list), and/or
  • (b) The stream accumulator passes the snapshot-reconstructed output to parse_response when the response.completed payload has output is None (since the snapshot has the correct list).

(b) is the load-bearing fix — (a) alone keeps the SDK from crashing but throws away every streamed item, so consumers see an empty response. (b) preserves the actual data the user already received over the wire.

Code snippets

Suggested two-line diff:

--- a/src/openai/lib/_parsing/_responses.py
+++ b/src/openai/lib/_parsing/_responses.py
@@ -58,7 +58,7 @@ def parse_response(
 ) -> ParsedResponse[TextFormatT]:
     output_list: List[ParsedResponseOutputItem[TextFormatT]] = []

-    for output in response.output:
+    for output in (response.output or []):
         if output.type == \"message\":
             content_list: List[ParsedContent[TextFormatT]] = []
             for item in output.content:

--- a/src/openai/lib/streaming/responses/_responses.py
+++ b/src/openai/lib/streaming/responses/_responses.py
@@ -357,8 +357,11 @@ class ResponseStreamManager(...):
             if output.type == \"function_call\":
                 output.arguments += event.delta
         elif event.type == \"response.completed\":
+            completed_response = event.response
+            if completed_response.output is None and snapshot.output is not None:
+                completed_response = completed_response.model_copy(update={\"output\": snapshot.output})
             self._completed_response = parse_response(
                 text_format=self._text_format,
-                response=event.response,
+                response=completed_response,
                 input_tools=self._input_tools,
             )

         return snapshot

Confirmed working as a monkey-patch in a downstream user's deployment.

OS

Linux (Ubuntu, VPS); also reproducible on macOS.

Python version

Python 3.12

Library version

openai v2.24.0 (and the equivalent lines on main as of today)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions