From 8a1664f6091087aa3b471b4b41879fb35ad10ee7 Mon Sep 17 00:00:00 2001 From: VitalCheffe Date: Thu, 2 Apr 2026 11:14:15 +0000 Subject: [PATCH 1/2] fix(responses): handle null text values in output_text property Closes #3011 - The output_text property now skips content items where text is null instead of raising TypeError during string concatenation - Updated ResponseOutputText.text type from str to Optional[str] to accurately reflect the API contract where text can be null - Added regression tests for null-only and mixed null/text responses Impact: Fixes crashes when models return null text fields in output_text content items, which occurs with certain models like openai/gpt-oss-safeguard-120b. --- src/openai/types/responses/response.py | 3 +- .../types/responses/response_output_text.py | 2 +- tests/lib/responses/test_responses.py | 42 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/openai/types/responses/response.py b/src/openai/types/responses/response.py index ada0783bce..d5f3e2b50a 100644 --- a/src/openai/types/responses/response.py +++ b/src/openai/types/responses/response.py @@ -310,12 +310,13 @@ def output_text(self) -> str: """Convenience property that aggregates all `output_text` items from the `output` list. If no `output_text` content blocks exist, then an empty string is returned. + Content items with a null `text` field are silently skipped. """ texts: List[str] = [] for output in self.output: if output.type == "message": for content in output.content: - if content.type == "output_text": + if content.type == "output_text" and content.text is not None: texts.append(content.text) return "".join(texts) diff --git a/src/openai/types/responses/response_output_text.py b/src/openai/types/responses/response_output_text.py index 2386fcb3c0..4c10877109 100644 --- a/src/openai/types/responses/response_output_text.py +++ b/src/openai/types/responses/response_output_text.py @@ -122,7 +122,7 @@ class ResponseOutputText(BaseModel): annotations: List[Annotation] """The annotations of the text output.""" - text: str + text: Optional[str] = None """The text output from the model.""" type: Literal["output_text"] diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py index 8e5f16df95..43928ed798 100644 --- a/tests/lib/responses/test_responses.py +++ b/tests/lib/responses/test_responses.py @@ -41,6 +41,48 @@ def test_output_text(client: OpenAI, respx_mock: MockRouter) -> None: ) +@pytest.mark.respx(base_url=base_url) +def test_output_text_with_null_text(client: OpenAI, respx_mock: MockRouter) -> None: + """Regression test: output_text should handle null text values in content items. + + See: https://github.com/openai/openai-python/issues/3011 + """ + response = make_snapshot_request( + lambda c: c.responses.create( + model="gpt-4o-mini", + input="test", + ), + content_snapshot=snapshot( + '{"id": "resp_test", "object": "response", "created_at": 1754925861, "status": "completed", "background": false, "error": null, "incomplete_details": null, "instructions": null, "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [{"id": "msg_test", "type": "message", "status": "completed", "content": [{"type": "output_text", "annotations": [], "logprobs": [], "text": null}], "role": "assistant"}], "parallel_tool_calls": true, "previous_response_id": null, "prompt_cache_key": null, "reasoning": {"effort": null, "summary": null}, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": {"format": {"type": "text"}, "verbosity": "medium"}, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": {"input_tokens": 5, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 5}, "user": null, "metadata": {}}' + ), + path="/responses", + mock_client=client, + respx_mock=respx_mock, + ) + + # Should not raise TypeError when text is null + assert response.output_text == snapshot("") + + +@pytest.mark.respx(base_url=base_url) +def test_output_text_mixed_null_and_text(client: OpenAI, respx_mock: MockRouter) -> None: + """output_text should skip null text items and concatenate valid ones.""" + response = make_snapshot_request( + lambda c: c.responses.create( + model="gpt-4o-mini", + input="test", + ), + content_snapshot=snapshot( + '{"id": "resp_test2", "object": "response", "created_at": 1754925861, "status": "completed", "background": false, "error": null, "incomplete_details": null, "instructions": null, "max_output_tokens": null, "max_tool_calls": null, "model": "gpt-4o-mini-2024-07-18", "output": [{"id": "msg_test2a", "type": "message", "status": "completed", "content": [{"type": "output_text", "annotations": [], "logprobs": [], "text": "Hello "}], "role": "assistant"}, {"id": "msg_test2b", "type": "message", "status": "completed", "content": [{"type": "output_text", "annotations": [], "logprobs": [], "text": null}], "role": "assistant"}, {"id": "msg_test2c", "type": "message", "status": "completed", "content": [{"type": "output_text", "annotations": [], "logprobs": [], "text": "world!"}], "role": "assistant"}], "parallel_tool_calls": true, "previous_response_id": null, "prompt_cache_key": null, "reasoning": {"effort": null, "summary": null}, "safety_identifier": null, "service_tier": "default", "store": true, "temperature": 1.0, "text": {"format": {"type": "text"}, "verbosity": "medium"}, "tool_choice": "auto", "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled", "usage": {"input_tokens": 5, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 10, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 15}, "user": null, "metadata": {}}' + ), + path="/responses", + mock_client=client, + respx_mock=respx_mock, + ) + + assert response.output_text == snapshot("Hello world!") + + @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 966d13dec9f6121121709a94ebeeea8b92feb278 Mon Sep 17 00:00:00 2001 From: VitalCheffe Date: Thu, 2 Apr 2026 11:26:57 +0000 Subject: [PATCH 2/2] fix(responses): filter null text items without changing public type signature - Keeps ResponseOutputText.text as str (no breaking change) - Guards output_text property against null values at aggregation time - Updates tests to reflect corrected approach --- src/openai/types/responses/response_output_text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openai/types/responses/response_output_text.py b/src/openai/types/responses/response_output_text.py index 4c10877109..2386fcb3c0 100644 --- a/src/openai/types/responses/response_output_text.py +++ b/src/openai/types/responses/response_output_text.py @@ -122,7 +122,7 @@ class ResponseOutputText(BaseModel): annotations: List[Annotation] """The annotations of the text output.""" - text: Optional[str] = None + text: str """The text output from the model.""" type: Literal["output_text"]