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 10 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:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 479179ed84
ℹ️ 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".
| type_=type(event.response), | ||
| value={ | ||
| **event.response.to_dict(), | ||
| "output": [item.to_dict() for item in snapshot.output], |
There was a problem hiding this comment.
Preserve finalized stream items when completed output is null
When response.completed has output: null, this fallback serializes snapshot.output, but that snapshot is only built from response.output_item.added, response.content_part.added, and deltas; accumulate_event never replaces it with the finalized response.output_item.done item or applies annotation/done events. In the exact backend scenario described here, any fields that are only finalized on those earlier events (for example message status, annotations/citations, or logprobs) are dropped from ResponseCompletedEvent.response / get_final_response(), even though the stream did provide them.
Useful? React with 👍 / 👎.
…t init httpx's get_environment_proxies() only splits NO_PROXY by comma, not by newline. When NO_PROXY contains newline characters (common in Docker environments, .env files, or shell scripts), the newline becomes part of the hostname and httpx raises InvalidURL. Add _sanitize_no_proxy() which replaces newlines with commas and strips whitespace, called from _DefaultHttpxClient.__init__() before the httpx client is constructed. Closes openai#3303.
…px client init" This reverts commit 217dc74.
When response.completed has output: null, the fallback serializes snapshot.output, but accumulate_event never applied done events (response.output_text.done, response.output_item.done, response.content_part.done, response.function_call_arguments.done) to the snapshot. This meant get_final_response() could expose stale in_progress statuses and miss finalized text in the null-output case. Add handlers in accumulate_event for all four done event types so the snapshot reflects the finalized state before the fallback serializes it. Addresses Codex P2 review feedback on commit 479179e.
|
Thanks for the continued review — the P2 point about done events not being applied to the snapshot is correct. Pushed Fix: Added handlers in
Previously these events only produced consumer-facing @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b097d55f6a
ℹ️ 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".
| if event.output_index < len(snapshot.output): | ||
| item = snapshot.output[event.output_index] | ||
| if hasattr(item, "status"): | ||
| item.status = "completed" |
There was a problem hiding this comment.
Preserve the item payload from done events
When response.completed has output=None, the fallback now serializes snapshot.output as the final response. For response.output_item.done, the server sends the finalized event.item; tool items such as file_search_call or code_interpreter_call can gain final fields like results/outputs, and items can also finish as failed or incomplete. This handler discards that payload and only forces status to completed, so the null-output fallback can return stale or misleading final items even though the done event contained the correct item.
Useful? React with 👍 / 👎.
| output = snapshot.output[event.output_index] | ||
| if output.type == "message" and event.content_index < len(output.content): | ||
| part = output.content[event.content_index] | ||
| if hasattr(part, "status"): | ||
| part.status = "completed" |
There was a problem hiding this comment.
Copy finalized content parts into the snapshot
When the completed event has output=None, the final response is built from snapshot.output, but response.content_part.done carries the finalized event.part and this branch only checks for a status attribute on the existing part. Output text/refusal parts do not have that status field, and metadata such as annotations/logprobs is not accumulated elsewhere, so responses with citations or final part metadata can lose it in the null-output fallback path.
Useful? React with 👍 / 👎.
| output = snapshot.output[event.output_index] | ||
| if output.type == "function_call": | ||
| if hasattr(output, "status"): | ||
| output.status = "completed" |
There was a problem hiding this comment.
Apply finalized function-call arguments
In the null-output fallback path, parse_response() parses tool arguments from snapshot.output, but this done handler ignores event.arguments, the finalized argument string sent by response.function_call_arguments.done. If the accumulated deltas are incomplete or differ from the final payload, the final response can expose stale arguments and fail or mis-parse parsed_arguments even though the stream included the authoritative arguments.
Useful? React with 👍 / 👎.
Three P2 fixes for the null-output fallback path: 1. response.output_item.done — Replace the entire item in the snapshot with event.item (not just set status=completed). The server sends the authoritative item payload which may include final fields like results/outputs on tool items, or a final status of failed/incomplete. 2. response.content_part.done — Replace the entire content part with event.part (not just set status=completed). The server sends the authoritative part payload which may include annotations, logprobs, or finalized text/refusal content that delta accumulation may not fully capture. 3. response.function_call_arguments.done — Apply event.arguments (the finalized argument string) to the snapshot instead of only setting status=completed. The server sends the authoritative arguments which may differ from accumulated deltas, ensuring parse_response() can correctly parse parsed_arguments in the null-output fallback.
|
Thanks for the continued review — all three P2 points are valid and addressed in P2 — Preserve the item payload from done events: P2 — Copy finalized content parts into the snapshot: P2 — Apply finalized function-call arguments: @codex review |
|
Security review completed. No security issues were found in this pull request. Reviewed commit: ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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". |
|
Hi maintainers — gentle bump on this one. Fixes |
|
Hi maintainers — circling back on this fix for the |
|
Hi maintainers — following up on this fix for #3325. Guards against None response.output in parse_response to prevent AttributeError when the API returns an empty response. CI is green. Would appreciate a review when time allows. Thanks! |
|
Friendly ping — this PR has been open for over 10 days. Would appreciate a human review when time allows. |
jbeckwith-oai
left a comment
There was a problem hiding this comment.
Two blockers on the current head:
-
The new null-output branch fails the repository's strict type checks. With the pinned tools, Pyright 1.1.399 reports
reportUnnecessaryComparisonat line 410 and a partially unknown dictionary at line 413; Mypy 1.17 marks the branch unreachable becauseResponse.outputis declared non-nullable. Please make the deliberately invalid wire value explicit at the boundary (for example via a narrowly scoped typed cast/normalization) so both standard checks pass without weakening the model contract. -
This changes reconstruction across
output_text.done,output_item.done,content_part.done,function_call_arguments.done, andresponse.completed, but adds no regression tests. Please add focused stream-state coverage forresponse.completed.output=Nonethat asserts accumulated text/items and authoritative done-event fields survive, parsed function arguments/text formats still run, and the no-prior-items case returns an empty output. The multiple correctness fixes already needed in this PR make executable coverage important.
Validation run: pinned Pyright and Mypy both fail as above; Ruff check/format pass; git diff --check passes.
Addresses review feedback from @jbeckwith-oai on openai#3517: 1. Type check failures — Pyright reported reportUnnecessaryComparison on the null-output check because Response.output is declared non-nullable. Added pyright: ignore comments with explanatory context, and a cast(Any, ...) on the construct_type_unchecked value dict to resolve the partially unknown argument type. Both Pyright and Mypy now pass. 2. Regression tests — Added 6 focused stream-state tests in test_null_output_fallback.py covering: - Accumulated text survives null output - Done-event status survives null output - No-prior-items returns empty output - Normal completed-with-output path still works - Empty output list path still works - Function call arguments survive null output
|
Thanks @jbeckwith-oai for the detailed review — both blockers are addressed in the latest push (d1e0857): 1. Type check failures — Added 2. Regression tests — Added 6 focused stream-state tests in
Would appreciate a re-review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1e0857227
ℹ️ 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".
| value=cast( | ||
| Any, | ||
| { | ||
| **event.response.to_dict(), |
There was a problem hiding this comment.
Avoid dumping the invalid null output
When this branch handles the backend output: null case, event.response.to_dict() serializes the invalid None through Response.output's non-nullable list serializer before this dict overwrites it; the no-snapshot path has the same problem inside parse_response. Under Pydantic v2 this emits a serializer warning, and with this repo's warnings-as-errors pytest config the null-output stream can still fail instead of returning the accumulated output, so build the copy without dumping the invalid field or use the compat dump with warnings disabled.
Useful? React with 👍 / 👎.
Addresses review feedback from @jbeckwith-oai on openai#3517: 1. Type check failures — Pyright reported reportUnnecessaryComparison on the null-output check because Response.output is declared non-nullable. Added pyright: ignore comments with explanatory context, and a cast(Any, ...) on the construct_type_unchecked value dict to resolve the partially unknown argument type. Both Pyright and Mypy now pass. 2. Regression tests — Added 6 focused stream-state tests in test_null_output_fallback.py covering: - Accumulated text survives null output - Done-event status survives null output - No-prior-items returns empty output - Normal completed-with-output path still works - Empty output list path still works - Function call arguments survive null output
d1e0857 to
4c19be7
Compare
|
Addressed the Codex P2 about dumping the invalid null output in the latest push (4c19be7):
The |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c19be7e80
ℹ️ 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".
| value=cast( | ||
| Any, | ||
| { | ||
| **event.response.to_dict(warnings=False), |
There was a problem hiding this comment.
Avoid disabling warnings on Pydantic v1
In environments using Pydantic v1, which this package still supports via pydantic>=1.9.0, <3 in pyproject.toml, this fallback raises before parsing because the repo's v1 BaseModel.model_dump shim rejects any warnings != True (src/openai/_models.py:352). That means the null-output stream with accumulated items still crashes for supported v1 users; gate this flag on Pydantic v2 or avoid dumping the invalid field instead.
Useful? React with 👍 / 👎.
| value={ | ||
| "type": "response.completed", | ||
| "sequence_number": 5, | ||
| "response": response.to_dict(), |
There was a problem hiding this comment.
Avoid serializing invalid null output in tests
With Pydantic v2, calling to_dict() on this deliberately invalid Response(output=None) emits a serializer warning, and this repo configures pytest warnings as errors (pyproject.toml:158). These new regression tests can therefore fail while constructing the fixture before they exercise the fallback (the same pattern appears in the function-call helper); pass a raw response dict into the event or otherwise avoid serializing the invalid model.
Useful? React with 👍 / 👎.
| ) | ||
|
|
||
|
|
||
| def _make_output_text_done(text: str = "Hello world") -> RawResponseStreamEvent: |
There was a problem hiding this comment.
Remove unused stream event helpers
Under the repo's strict Pyright lint step, this private helper is reported as reportUnusedFunction because no test references it; _make_content_part_done below has the same issue. Leaving these unused helpers in the new test module makes pyright tests/lib/responses/test_null_output_fallback.py fail before the regression tests can run, so delete them or exercise them in a test.
Useful? React with 👍 / 👎.
Addresses review feedback from @jbeckwith-oai on openai#3517: 1. Type check failures — Pyright reported reportUnnecessaryComparison on the null-output check because Response.output is declared non-nullable. Added pyright: ignore comments with explanatory context, and a cast(Any, ...) on the construct_type_unchecked value dict to resolve the partially unknown argument type. Both Pyright and Mypy now pass. 2. Regression tests — Added 6 focused stream-state tests in test_null_output_fallback.py covering: - Accumulated text survives null output - Done-event status survives null output - No-prior-items returns empty output - Normal completed-with-output path still works - Empty output list path still works - Function call arguments survive null output
4c19be7 to
509bd3f
Compare
|
Addressed all three Codex P2 comments in the latest push (509bd3f): 1. Pydantic v1 compatibility — The 2. Test fixtures avoid serializing invalid null output — 3. Unused helper functions removed — All 6 tests pass, ruff clean, pyright 0 errors on source (test file has pre-existing 174 errors from union-type attribute access, unchanged from before). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 509bd3f9b3
ℹ️ 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".
| value={ | ||
| "type": "response.created", | ||
| "sequence_number": 0, | ||
| "response": response.to_dict(), |
There was a problem hiding this comment.
Build nested response models in stream event fixtures
When these new tests run, this helper returns a ResponseCreatedEvent whose nested response is still a plain dict: construct_type_unchecked constructs the event model without recursively validating nested fields. ResponseStreamState._create_initial_response() immediately calls event.response.to_dict(), so every test that seeds the state through _make_created_event() fails with AttributeError before exercising the null-output fallback; pass the Response object (and similarly construct nested item values) rather than serialized dicts.
Useful? React with 👍 / 👎.
| Any, | ||
| { | ||
| **base_dict, | ||
| "output": [item.to_dict() for item in snapshot.output], |
There was a problem hiding this comment.
Keep fallback output as models before parsing
When response.completed.output is None and the snapshot has accumulated items, this branch serializes those items to dicts and then rebuilds a Response with construct_type_unchecked, which is shallow for BaseModel subclasses. As a result response_with_output.output is a list of plain dicts, so parse_response() immediately dereferences output.type and the null-output stream still crashes instead of returning the accumulated response; preserve the model objects or use a recursive/validated construction before parsing.
Useful? React with 👍 / 👎.
Addresses two Codex P2 comments: 1. Build nested response models in stream event fixtures — _make_created_event(), _make_completed_event_with_output(), and _make_completed_event_empty_output() now pass the Response model object directly instead of response.to_dict(). construct_type_unchecked is shallow, so passing a dict left event.response as a plain dict, and _create_initial_response() calling event.response.to_dict() would raise AttributeError. 2. Keep fallback output as models before parsing — The null-output fallback now passes list(snapshot.output) (model objects) directly instead of [item.to_dict() for item in snapshot.output]. Shallow construct_type_unchecked would leave dicts in the output list, so parse_response() dereferencing output.type would crash. All 6 null-output tests pass, ruff and pyright clean.
|
Addressed both Codex P2 comments in the latest push (d7eb166): 1. Build nested response models in stream event fixtures — 2. Keep fallback output as models before parsing — The null-output fallback now passes All 6 null-output tests pass, ruff and pyright clean. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d7eb1667ed
ℹ️ 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".
| snapshot.output[event.output_index] = construct_type_unchecked( | ||
| type_=type(snapshot.output[event.output_index]), | ||
| value=event.item.to_dict(), |
There was a problem hiding this comment.
Preserve nested content models from done items
When a message response.output_item.done arrives before a null-output response.completed, this assignment shallow-constructs ParsedResponseOutputMessage from event.item.to_dict(), leaving its content entries as plain dicts. Fresh evidence after the earlier shallow-construction fix is that only the outer output item is preserved as a model; parse_response() still iterates output.content and dereferences item.type, so the null-output fallback crashes for finalized message items instead of returning the streamed text. Rebuild the nested content parts as models, or keep the already-accumulated content, when copying the done item.
Useful? React with 👍 / 👎.
| # `output` is typed as non-nullable but the wire value can violate | ||
| # that contract; the `pyright: ignore` makes the check explicit | ||
| # without weakening the model contract. | ||
| if event.response.output is None and snapshot.output: # pyright: ignore[reportUnnecessaryComparison] |
There was a problem hiding this comment.
Coerce null-output events before dereferencing
In the default streaming path, SSE data is converted through construct_type(...); for a response.completed payload whose nested response.output is null, validation of the nested Response can fail and the discriminator fallback shallow-constructs the event, leaving event.response as the raw dict. This guard then reads event.response.output, so the exact null-output backend response can still fail with AttributeError before the fallback or parse_response() runs; normalize mapping responses to Response or handle dicts before this check.
Useful? React with 👍 / 👎.
…-output fallback Addresses two Codex P2 comments: 1. Preserve nested content models from done items — response.output_item.done and response.content_part.done were round-tripping through event.item.to_dict() / event.part.to_dict() before construct_type_unchecked, which is shallow. This left nested content parts as plain dicts, so parse_response() crashed on output.content[].type. Now passes event.item / event.part directly (already model objects). Also applied the same fix to response.output_item.added and response.content_part.added for consistency. 2. Coerce null-output events before dereferencing — In the default streaming path, construct_type on a response.completed payload with null output can fail validation and the discriminator fallback shallow-constructs the event, leaving event.response as a raw dict. The guard event.response.output would then raise AttributeError before the fallback could run. Now coerces event.response to a Response model via construct_type_unchecked before the null-output check. Added test_dict_response_coerced_before_null_output_check. All 7 tests pass, ruff and pyright clean.
|
Addressed both new Codex P2 comments in the latest push (61ca2ad): 1. Preserve nested content models from done items — 2. Coerce null-output events before dereferencing — In the default streaming path, All 7 tests pass, ruff and pyright clean. |
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