fix(parsing): handle null response.output in parse_response#3322
fix(parsing): handle null response.output in parse_response#3322SteveYuOWO wants to merge 1 commit into
Conversation
The Response API contract types ``output`` as ``List[ResponseOutputItem]``, but some backends (observed on the ChatGPT codex backend for newer models) emit a terminal payload with ``output: null``. Pydantic does not coerce None to ``[]`` because the field is annotated as a list rather than Optional, so ``parse_response()`` raises ``TypeError: 'NoneType' object is not iterable`` during stream accumulation. Treat None defensively as an empty list so streaming clients can recover from the deltas they already received via ``response.output_text.delta`` and ``response.output_item.done`` events.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cef5e9ecd7
ℹ️ 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".
| # which would raise ``TypeError: 'NoneType' object is not iterable`` | ||
| # here. Treat None defensively as an empty list so streaming clients | ||
| # can recover from the deltas they already received. | ||
| for output in (response.output or []): |
There was a problem hiding this comment.
Preserve streamed output when the terminal payload is null
When this path is reached from streaming, ResponseStreamState.accumulate_event() calls parse_response(..., response=event.response) for the response.completed event, and get_final_response() later returns that _completed_response. For the backend case described in this change, the terminal event.response.output is None even though earlier response.output_item.added / delta events populated the stream snapshot, so treating it as [] makes the final ResponseCompletedEvent.response and stream.get_final_response() lose all accumulated message/tool output. This avoids the crash but still breaks callers that rely on the final parsed response for exactly the null-terminal-output stream being fixed.
Useful? React with 👍 / 👎.
This pull request makes
parse_responseinsrc/openai/lib/_parsing/_responses.pyrobust to terminalresponse.completedevents whoseresponse.outputisnullinstead of[].Without this change, the SDK raises
TypeError: 'NoneType' object is not iterableduring stream accumulation (accumulate_event→parse_response), so the streamed deltas the client already received viaresponse.output_text.delta/response.output_item.doneare lost and the caller'sfor event in stream:loop dies beforeget_final_response()is ever reached.This has been observed on the ChatGPT codex backend (
chatgpt.com/backend-api/codex) for newer codex models, where it breaks everyhermes-agentconversation turn against that backend witherror_type=TypeError ... summary='NoneType' object is not iterable. Tracking issues in the downstream consumer: NousResearch/hermes-agent#33059 (which explicitly identifies this SDK function as the broken layer) and #32991. Client-side workarounds in hermes aroundget_final_response()are unreachable because the crash happens earlier, inside the stream iterator.Iterating over
(response.output or [])defensively treatsNoneas empty, matching the existing empty-list path. Added a regression test intests/lib/responses/test_responses.py.