From 22b98e9963224998ec6ee4bdb3243b340c3e4355 Mon Sep 17 00:00:00 2001
From: Thiago Barbosa
Date: Thu, 23 Jul 2026 13:01:04 +0200
Subject: [PATCH 1/4] fix: parse_response crashes on null output from Codex
backend stream
---
src/openai/lib/_parsing/_responses.py | 2 +-
tests/lib/responses/test_responses.py | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/openai/lib/_parsing/_responses.py b/src/openai/lib/_parsing/_responses.py
index c607587ec1..81e6b2b983 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 4ed6dff47d..8cf96ee1d1 100644
--- a/tests/lib/responses/test_responses.py
+++ b/tests/lib/responses/test_responses.py
@@ -72,6 +72,14 @@ def test_parse_response_preserves_program_items(item: dict[str, object]) -> None
assert parsed.output[0].to_dict() == item
+def test_parse_response_handles_null_output() -> None:
+ response = construct_type_unchecked(type_=Response, value={"output": None})
+
+ parsed = parse_response(text_format=omit, input_tools=omit, response=response)
+
+ assert parsed.output == []
+
+
@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
def test_stream_method_definition_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpenAI) -> None:
checking_client: OpenAI | AsyncOpenAI = client if sync else async_client
From 35ca37d804c280de1bf2cc81dfdc05d481fc04e8 Mon Sep 17 00:00:00 2001
From: Thiago Barbosa
Date: Thu, 23 Jul 2026 14:20:00 +0200
Subject: [PATCH 2/4] fix: handle null output in response.completed events to
preserve accumulated output
---
.../lib/streaming/responses/_responses.py | 15 +++-
tests/lib/responses/test_responses.py | 69 ++++++++++++++++++-
2 files changed, 81 insertions(+), 3 deletions(-)
diff --git a/src/openai/lib/streaming/responses/_responses.py b/src/openai/lib/streaming/responses/_responses.py
index 6975a9260d..05f66888c4 100644
--- a/src/openai/lib/streaming/responses/_responses.py
+++ b/src/openai/lib/streaming/responses/_responses.py
@@ -17,7 +17,7 @@
from ...._utils import is_given, consume_sync_iterator, consume_async_iterator
from ...._models import build, construct_type_unchecked
from ...._streaming import Stream, AsyncStream
-from ....types.responses import ParsedResponse, ResponseStreamEvent as RawResponseStreamEvent
+from ....types.responses import Response, ParsedResponse, ResponseStreamEvent as RawResponseStreamEvent
from ..._parsing._responses import TextFormatT, parse_text, parse_response
from ....types.responses.tool_param import ToolParam
from ....types.responses.parsed_response import (
@@ -357,9 +357,20 @@ def accumulate_event(self, event: RawResponseStreamEvent) -> ParsedResponseSnaps
if output.type == "function_call":
output.arguments += event.delta
elif event.type == "response.completed":
+ response: Response = event.response
+ if response.output is None:
+ # Some backends omit `output` on the completion event even though the
+ # individual output items were streamed via `response.output_item.*` /
+ # `response.*.delta` events. Fall back to the accumulated snapshot so we
+ # don't silently drop the generated content.
+ response = construct_type_unchecked(
+ type_=cast(Any, Response),
+ 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 8cf96ee1d1..0cbbf74d46 100644
--- a/tests/lib/responses/test_responses.py
+++ b/tests/lib/responses/test_responses.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+from typing import Any, cast
from typing_extensions import TypeVar
import pytest
@@ -10,8 +11,14 @@
from openai._types import omit
from openai._utils import assert_signatures_in_sync
from openai._models import construct_type_unchecked
-from openai.types.responses import Response
+from openai.types.responses import (
+ Response,
+ ResponseCreatedEvent,
+ ResponseCompletedEvent,
+ ResponseOutputItemAddedEvent,
+)
from openai.lib._parsing._responses import parse_response
+from openai.lib.streaming.responses._responses import ResponseStreamState
from ...conftest import base_url
from ..snapshots import make_snapshot_request
@@ -80,6 +87,66 @@ def test_parse_response_handles_null_output() -> None:
assert parsed.output == []
+def test_stream_state_preserves_accumulated_output_on_null_completion() -> None:
+ """Regression test: some backends omit `output` on `response.completed` even though
+ the output items were streamed via `response.output_item.*` events beforehand. The
+ accumulated snapshot must be used instead of discarding the generated content.
+ """
+ state: ResponseStreamState[None] = ResponseStreamState(input_tools=omit, text_format=omit)
+
+ state.handle_event(
+ construct_type_unchecked(
+ type_=cast(Any, ResponseCreatedEvent),
+ value={
+ "type": "response.created",
+ "sequence_number": 0,
+ "response": {"output": []},
+ },
+ )
+ )
+
+ message_item = {
+ "id": "msg_123",
+ "type": "message",
+ "status": "completed",
+ "role": "assistant",
+ "content": [{"type": "output_text", "text": "hello world", "annotations": []}],
+ }
+ state.handle_event(
+ construct_type_unchecked(
+ type_=cast(Any, ResponseOutputItemAddedEvent),
+ value={
+ "type": "response.output_item.added",
+ "sequence_number": 1,
+ "output_index": 0,
+ "item": message_item,
+ },
+ )
+ )
+
+ events = state.handle_event(
+ construct_type_unchecked(
+ type_=cast(Any, ResponseCompletedEvent),
+ value={
+ "type": "response.completed",
+ "sequence_number": 2,
+ "response": {"status": "completed", "output": None},
+ },
+ )
+ )
+
+ assert state._completed_response is not None
+ output = state._completed_response.output
+ assert len(output) == 1
+ assert output[0].type == "message"
+ assert output[0].content[0].type == "output_text"
+ assert output[0].content[0].text == "hello world"
+
+ completed_event = events[-1]
+ assert completed_event.type == "response.completed"
+ assert completed_event.response.output == output
+
+
@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"])
def test_stream_method_definition_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpenAI) -> None:
checking_client: OpenAI | AsyncOpenAI = client if sync else async_client
From 42ea50d8d497008278e3b621da25a1bc5ee60846 Mon Sep 17 00:00:00 2001
From: Thiago Barbosa
Date: Thu, 23 Jul 2026 14:27:20 +0200
Subject: [PATCH 3/4] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
tests/lib/responses/test_responses.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py
index 0cbbf74d46..b82f5765bc 100644
--- a/tests/lib/responses/test_responses.py
+++ b/tests/lib/responses/test_responses.py
@@ -17,9 +17,7 @@
ResponseCompletedEvent,
ResponseOutputItemAddedEvent,
)
-from openai.lib._parsing._responses import parse_response
-from openai.lib.streaming.responses._responses import ResponseStreamState
-
+from openai.lib.streaming.responses import ResponseStreamState
from ...conftest import base_url
from ..snapshots import make_snapshot_request
From be08cd3b899da7d657c4b210dc9dc759aa744a0c Mon Sep 17 00:00:00 2001
From: Thiago Barbosa
Date: Thu, 23 Jul 2026 14:53:00 +0200
Subject: [PATCH 4/4] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
tests/lib/responses/test_responses.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py
index b82f5765bc..35213872f2 100644
--- a/tests/lib/responses/test_responses.py
+++ b/tests/lib/responses/test_responses.py
@@ -17,6 +17,7 @@
ResponseCompletedEvent,
ResponseOutputItemAddedEvent,
)
+from openai.lib._parsing._responses import parse_response
from openai.lib.streaming.responses import ResponseStreamState
from ...conftest import base_url
from ..snapshots import make_snapshot_request