fix(parsing): guard against None response.output in parse_response#3517
fix(parsing): guard against None response.output in parse_response#3517rkfshakti wants to merge 3 commits into
Conversation
The chatgpt.com Codex backend sometimes sends response.output: null in the consolidated response.completed event, even when valid output_item.done events were streamed earlier. The SDK then raises TypeError: 'NoneType' object is not iterable inside the stream accumulator, killing the entire stream before the consumer can read the deltas. Fix: iterate over response.output or [] instead of response.output directly. Closes openai#3325
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb666413b4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| output_list: List[ParsedResponseOutputItem[TextFormatT]] = [] | ||
|
|
||
| for output in response.output: | ||
| for output in response.output or []: |
There was a problem hiding this comment.
Preserve streamed output when completed output is null
When this is called from ResponseStreamState.accumulate_event for a response.completed event whose event.response.output is None, response.output or [] turns the final ParsedResponse into an empty output list. The stream has already accumulated output items/text in snapshot.output, but get_final_response() and ResponseCompletedEvent use _completed_response from this parse, so these streams finish with response.output/output_text empty instead of the text or tool calls just emitted.
Useful? React with 👍 / 👎.
|
Thanks for reviewing! 👋 This is my first contribution to the OpenAI Python SDK. I'm actively contributing to GenAI open-source projects — you can see my other PRs at https://github.com/rkfshakti (chroma-core/chroma, langchain-ai/langchain, langgenius/dify, NousResearch/hermes-agent, firecrawl/firecrawl). The fix is minimal — Happy to add a test case if that would help. Thanks! |
…as null output The chatgpt.com Codex backend sometimes sends response.output: null in the consolidated response.completed event even when valid output_item.done events were streamed earlier (issue openai#3325). The previous fix (response.output or []) prevented the TypeError but discarded the already-streamed snapshot.output, causing the final ParsedResponse to have empty output/output_text. Move the guard into ResponseStreamState.accumulate_event: when event.response.output is None and the snapshot has accumulated output items, build the completed response from the snapshot instead of calling parse_response with an empty output list. This preserves streamed text and tool calls in get_final_response() and ResponseCompletedEvent. Addresses Codex review feedback on openai#3517.
|
Thanks for the Codex review — the P2 point about preserving streamed output is correct and I've pushed a fix ( Problem with the previous approach: New fix: Moved the guard into The Happy to add a streaming test that exercises the |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c050f6988
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| self._completed_response = construct_type_unchecked( | ||
| type_=ParsedResponse[TextFormatT], | ||
| value={ | ||
| **event.response.to_dict(), | ||
| "output": [item.to_dict() for item in snapshot.output], | ||
| }, |
There was a problem hiding this comment.
Run response parsing on the streamed fallback
When a streaming response.completed has response.output == None and the caller supplied a text_format or parseable/strict function tool, this fallback constructs the final ParsedResponse directly from snapshot.output instead of passing that output through parse_response. The streamed snapshot only accumulates raw text and raw function-call arguments, so get_final_response() and the ResponseCompletedEvent.response will preserve output_text but leave output_parsed/parsed_arguments as None in exactly the null-output case this branch handles.
Useful? React with 👍 / 👎.
| # Calling `parse_response` with a null `output` would discard the | ||
| # already-accumulated `snapshot.output` and emit an empty final | ||
| # response, so we fall back to the streamed snapshot in that case. | ||
| if event.response.output is None and snapshot.output: |
There was a problem hiding this comment.
Handle null completed output without streamed items
If the backend sends response.output: null on response.completed before any output item has been accumulated, this condition is false and the else path still calls parse_response with event.response.output is None, reintroducing the same TypeError this fix is meant to avoid. This affects legitimately empty completed responses (or any null-output stream with no prior response.output_item.added) instead of returning a parsed response with an empty output list.
Useful? React with 👍 / 👎.
| type_=ParsedResponse[TextFormatT], | ||
| value={ | ||
| **event.response.to_dict(), | ||
| "output": [item.to_dict() for item in snapshot.output], |
There was a problem hiding this comment.
Apply done events before serializing the fallback snapshot
When response.completed has a null output after normal streamed *.done events, this fallback serializes snapshot.output, but the accumulator above never applies response.output_item.done, response.content_part.done, or response.function_call_arguments.done to that snapshot. As a result, get_final_response() can expose stale in_progress item statuses or miss final content metadata carried by the done events even though those finalized items were already streamed.
Useful? React with 👍 / 👎.
| type_=ParsedResponse[TextFormatT], | ||
| value={ | ||
| **event.response.to_dict(), | ||
| "output": [item.to_dict() for item in snapshot.output], |
There was a problem hiding this comment.
Preserve streamed text annotations in the fallback
For streams that emit response.output_text.annotation.added events, such as citation-bearing responses, the accumulator never adds those annotations to snapshot.output; with a null completed output this fallback serializes that incomplete snapshot instead of a final API output. In that scenario get_final_response().output loses annotations that were already streamed, leaving citation/file annotation arrays empty even though consumers saw the annotation events.
Useful? React with 👍 / 👎.
Address Codex P2 review feedback on the previous commit (1c050f6): 1. Run response parsing on the streamed fallback: instead of building ParsedResponse directly from snapshot.output via construct_type_unchecked (which left output_parsed/parsed_arguments as None), inject the streamed items into a shallow copy of the response and pass it through parse_response so text_format and parsed_arguments logic still runs. 2. Handle null completed output without streamed items: re-add the 'response.output or []' guard in parse_response() so a null-output response.completed with no prior output_item.added events returns a parsed response with an empty output list instead of raising TypeError. Together these cover both branches: null output WITH accumulated snapshot items (parse_response runs on the injected items) and null output WITHOUT items (parse_response returns empty output gracefully).
|
Thanks for the re-review on 1. Run response parsing on the streamed fallback — The previous 2. Handle null completed output without streamed items — Re-added the 3 & 4. Apply done events / preserve annotations before serializing — Both of these were symptoms of the same root cause: the fallback was serializing the raw snapshot instead of going through The two branches now cover all cases:
|
Problem
The chatgpt.com Codex backend sometimes sends
response.output: nullin the consolidatedresponse.completedevent, even when validoutput_item.doneevents were streamed earlier. The SDK then raisesTypeError: 'NoneType' object is not iterableinside the stream accumulator, killing the entire stream before the consumer can read the deltas.Closes #3325.
Fix
Change
for output in response.outputtofor output in response.output or []so thatNoneis handled gracefully (empty iteration instead of TypeError).Testing
python -c "import ast; ast.parse(open('src/openai/lib/_parsing/_responses.py').read())"passesparse_response(response=Response(output=None, ...))no longer raises TypeErrorChecklist