From 88c5b765f7b529f99abd41e18f6c3eb2ff62569c Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Mon, 22 Jun 2026 11:26:15 +0800 Subject: [PATCH 1/2] fix: handle null response output in parser Fixes openai/openai-python#3325 --- src/openai/lib/_parsing/_responses.py | 2 +- tests/lib/responses/test_responses.py | 31 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/openai/lib/_parsing/_responses.py b/src/openai/lib/_parsing/_responses.py index 232718cef6..6fdadf9379 100644 --- 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: diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py index 8e5f16df95..045326cfcf 100644 --- a/tests/lib/responses/test_responses.py +++ b/tests/lib/responses/test_responses.py @@ -61,3 +61,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: + from openai._models import construct_type_unchecked + from openai._types import omit + from openai.lib._parsing._responses import parse_response + from openai.types.responses import Response + + response = construct_type_unchecked( + type_=Response, + value={ + "id": "resp_test", + "object": "response", + "created_at": 0, + "model": "gpt-4o-mini", + "output": None, + "parallel_tool_calls": True, + "temperature": 1, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "metadata": {}, + "reasoning": {}, + "status": "completed", + "text": {"format": {"type": "text"}}, + }, + ) + + parsed = parse_response(text_format=omit, input_tools=None, response=response) + + assert parsed.output == [] From ea14c99ed8ce993f46a7236834db5400d5cf96f0 Mon Sep 17 00:00:00 2001 From: nightcityblade Date: Wed, 24 Jun 2026 11:14:34 +0800 Subject: [PATCH 2/2] fix: preserve streamed responses with null final output --- .../lib/streaming/responses/_responses.py | 12 ++- tests/lib/responses/test_responses.py | 101 +++++++++++++++++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/openai/lib/streaming/responses/_responses.py b/src/openai/lib/streaming/responses/_responses.py index 6975a9260d..0467c9c6da 100644 --- a/src/openai/lib/streaming/responses/_responses.py +++ b/src/openai/lib/streaming/responses/_responses.py @@ -357,9 +357,19 @@ def accumulate_event(self, event: RawResponseStreamEvent) -> ParsedResponseSnaps if output.type == "function_call": output.arguments += event.delta elif event.type == "response.completed": + response = event.response + if response.output is None: + response = construct_type_unchecked( + type_=ParsedResponseSnapshot, + value={ + **response.to_dict(), + "output": snapshot.output, + }, + ) + self._completed_response = parse_response( text_format=self._text_format, - response=event.response, + response=response, input_tools=self._input_tools, ) diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py index 045326cfcf..a7f75de72d 100644 --- a/tests/lib/responses/test_responses.py +++ b/tests/lib/responses/test_responses.py @@ -64,10 +64,10 @@ def test_parse_method_definition_in_sync(sync: bool, client: OpenAI, async_clien def test_parse_response_handles_null_output() -> None: - from openai._models import construct_type_unchecked from openai._types import omit - from openai.lib._parsing._responses import parse_response + from openai._models import construct_type_unchecked from openai.types.responses import Response + from openai.lib._parsing._responses import parse_response response = construct_type_unchecked( type_=Response, @@ -92,3 +92,100 @@ def test_parse_response_handles_null_output() -> None: parsed = parse_response(text_format=omit, input_tools=None, response=response) assert parsed.output == [] + + +def test_stream_completed_event_with_null_output_preserves_accumulated_output() -> None: + from openai._types import omit + from openai._models import construct_type_unchecked + from openai.lib.streaming.responses._responses import ResponseStreamState + from openai.types.responses.response_created_event import ResponseCreatedEvent + from openai.types.responses.response_completed_event import ResponseCompletedEvent + from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent + from openai.types.responses.response_output_item_added_event import ResponseOutputItemAddedEvent + from openai.types.responses.response_content_part_added_event import ResponseContentPartAddedEvent + + def event(type_: object, value: object): + return construct_type_unchecked(type_=type_, value=value) + + response = { + "id": "resp_test", + "object": "response", + "created_at": 0, + "model": "gpt-4o-mini", + "output": [], + "parallel_tool_calls": True, + "temperature": 1, + "tool_choice": "auto", + "tools": [], + "top_p": 1, + "metadata": {}, + "reasoning": {}, + "status": "completed", + "text": {"format": {"type": "text"}}, + } + state = ResponseStreamState(text_format=omit, input_tools=omit) + + for type_, value in [ + (ResponseCreatedEvent, {"type": "response.created", "sequence_number": 0, "response": response}), + ( + ResponseOutputItemAddedEvent, + { + "type": "response.output_item.added", + "sequence_number": 1, + "output_index": 0, + "item": { + "id": "msg_test", + "type": "message", + "status": "in_progress", + "role": "assistant", + "content": [], + }, + }, + ), + ( + ResponseContentPartAddedEvent, + { + "type": "response.content_part.added", + "sequence_number": 2, + "item_id": "msg_test", + "output_index": 0, + "content_index": 0, + "part": {"type": "output_text", "text": "", "annotations": []}, + }, + ), + ( + ResponseTextDeltaEvent, + { + "type": "response.output_text.delta", + "sequence_number": 3, + "item_id": "msg_test", + "output_index": 0, + "content_index": 0, + "delta": "Hello", + "logprobs": [], + }, + ), + ]: + state.handle_event(event(type_, value)) + + events = state.handle_event( + event( + ResponseCompletedEvent, + { + "type": "response.completed", + "sequence_number": 4, + "response": { + **response, + "output": None, + "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2}, + }, + }, + ) + ) + + assert state._completed_response is not None + assert state._completed_response.output_text == "Hello" + assert state._completed_response.usage is not None + assert state._completed_response.usage.total_tokens == 2 + assert events[0].type == "response.completed" + assert events[0].response.output_text == "Hello"