Confirm this is an issue with the Python library and not an underlying OpenAI API
(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:
-
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.
-
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
- 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.)
- Call
client.responses.stream(model=..., input=[...], ...) and iterate.
- 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.
- 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)
Confirm this is an issue with the Python library and not an underlying OpenAI API
(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()raisesTypeError: 'NoneType' object is not iterablefromparse_responsewhen aresponse.completedSSE event arrives withresponse.output == null.Traceback:
The bug is two-fold:
parse_responseis not defensive._parsing/_responses.pyline 61 iteratesresponse.outputdirectly, butResponse.outputisOptionalin the typed schema. Any caller passing aResponsewithoutput=Nonecrashes.The streaming accumulator throws away information.
streaming/responses/_responses.pyline 360 callsparse_response(response=event.response, ...), passing theresponse.completedevent's payload directly. If the server's final payload hasoutput: null, that null reachesparse_response. But the accumulator's__current_snapshot(the localsnapshotvariable a few lines above) already contains the correct list of output items, reconstructed from the precedingresponse.output_item.added/response.output_item.doneevents. 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 thegpt-5.3-codexmodel, used by community wrappers like Hermes Agent). That backend shipsoutput: nullon the finalresponse.completed.responsepayload, even though it correctly streamed all the intermediateresponse.output_item.added/done,response.reasoning_summary_text.*, andresponse.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
OpenAIclient at a Responses endpoint that returnsoutput: nullon the finalresponse.completedSSE event. (The Codex backend athttps://chatgpt.com/backend-api/codex/responsesreproduces this; any mock server returning the same shape will too.)client.responses.stream(model=..., input=[...], ...)and iterate.response.created, thenresponse.output_item.added/response.output_item.donefor each item (reasoning + function_call in the repro), thenresponse.completedwithresponse.output == null.TypeError: 'NoneType' object is not iterable.A mock SSE that's enough to repro:
For comparison: a raw
urllib.requestPOST against the same endpoint parses fine — the events are valid JSON, the stream is well-formed, the only issue is that finaloutput: null.Expected behavior
Either:
parse_responsetoleratesresponse.output is None(treat as empty list), and/orparse_responsewhen theresponse.completedpayload hasoutput 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:
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
mainas of today)