From cef5e9ecd734cd0a315cdc36ff669b7ed6af7a18 Mon Sep 17 00:00:00 2001 From: SteveYuOWO Date: Wed, 27 May 2026 16:29:06 +0900 Subject: [PATCH] fix(parsing): handle null response.output in parse_response 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. --- src/openai/lib/_parsing/_responses.py | 9 ++++++- tests/lib/responses/test_responses.py | 34 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/openai/lib/_parsing/_responses.py b/src/openai/lib/_parsing/_responses.py index 8853a0749f..a71be5a71e 100644 --- a/src/openai/lib/_parsing/_responses.py +++ b/src/openai/lib/_parsing/_responses.py @@ -58,7 +58,14 @@ def parse_response( ) -> ParsedResponse[TextFormatT]: output_list: List[ParsedResponseOutputItem[TextFormatT]] = [] - for output in response.output: + # Some Responses-API backends emit a terminal payload with + # ``output: null`` instead of ``output: []`` (observed on the ChatGPT + # codex backend for newer models). The Response type annotation is + # ``List[ResponseOutputItem]`` so pydantic doesn't coerce None to [], + # 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 []): if output.type == "message": content_list: List[ParsedContent[TextFormatT]] = [] for item in output.content: diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py index 8e5f16df95..c08d45af6f 100644 --- a/tests/lib/responses/test_responses.py +++ b/tests/lib/responses/test_responses.py @@ -8,6 +8,9 @@ from openai import OpenAI, AsyncOpenAI from openai._utils import assert_signatures_in_sync +from openai._types import omit +from openai.types.responses import Response +from openai.lib._parsing._responses import parse_response from ...conftest import base_url from ..snapshots import make_snapshot_request @@ -61,3 +64,34 @@ def test_parse_method_definition_in_sync(sync: bool, client: OpenAI, async_clien checking_client.responses.parse, exclude_params={"tools"}, ) + + +def test_parse_response_handles_null_output() -> None: + """Regression test: some Responses-API backends emit a terminal + payload with ``output: null`` (observed on the ChatGPT codex backend + for newer models). ``parse_response`` should treat None defensively + as an empty list rather than raising + ``TypeError: 'NoneType' object is not iterable``. + """ + response = Response.model_construct( + id="resp_test_null_output", + object="response", + created_at=0, + status="completed", + error=None, + incomplete_details=None, + instructions=None, + model="test-model", + output=None, # the bug-trigger + parallel_tool_calls=True, + temperature=1.0, + tool_choice="auto", + tools=[], + top_p=1.0, + metadata={}, + ) + + parsed = parse_response(text_format=omit, input_tools=omit, response=response) + + assert parsed.output == [] + assert parsed.output_parsed is None