diff --git a/src/openai/types/responses/response.py b/src/openai/types/responses/response.py index 67102d2628..e7c53dd0f4 100644 --- a/src/openai/types/responses/response.py +++ b/src/openai/types/responses/response.py @@ -447,7 +447,7 @@ def output_text(self) -> 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_input_item_param.py b/src/openai/types/responses/response_input_item_param.py index 0b0d68cdbd..e888ff879d 100644 --- a/src/openai/types/responses/response_input_item_param.py +++ b/src/openai/types/responses/response_input_item_param.py @@ -193,10 +193,10 @@ class ImageGenerationCall(TypedDict, total=False): id: Required[str] """The unique ID of the image generation call.""" - result: Required[Optional[str]] + result: Optional[str] """The generated image encoded in base64.""" - status: Required[Literal["in_progress", "completed", "generating", "failed"]] + status: Literal["in_progress", "completed", "generating", "failed"] """The status of the image generation call.""" type: Required[Literal["image_generation_call"]] diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py index 8e5f16df95..19e1f4637a 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.responses.response import Response +from openai.types.responses.response_output_message import ResponseOutputMessage +from openai.types.responses.response_output_text import ResponseOutputText from ...conftest import base_url from ..snapshots import make_snapshot_request @@ -41,6 +44,77 @@ def test_output_text(client: OpenAI, respx_mock: MockRouter) -> None: ) +def _make_response_with_text(text: str | None) -> Response: + """Build a minimal Response via model_construct, bypassing validation to simulate + raw API payloads where `text` may be null.""" + content_block = ResponseOutputText.model_construct( + type="output_text", + annotations=[], + logprobs=None, + text=text, + ) + message = ResponseOutputMessage.model_construct( + id="msg_test", + type="message", + status="completed", + role="assistant", + content=[content_block], + ) + return Response.model_construct( + id="resp_test", + object="response", + created_at=0, + status="completed", + model="gpt-4o-mini", + output=[message], + parallel_tool_calls=True, + text=None, + tool_choice="auto", + tools=[], + truncation="disabled", + ) + + +def test_output_text_null_guard() -> None: + """output_text must not crash when content items have text=None (issue #3063).""" + # Single null text block -> empty string + response = _make_response_with_text(None) + assert response.output_text == "" + + # Valid text block -> the text is returned + response = _make_response_with_text("hello") + assert response.output_text == "hello" + + # Mixed: null and valid in the same message -> only valid text joined + null_block = ResponseOutputText.model_construct( + type="output_text", annotations=[], logprobs=None, text=None + ) + valid_block = ResponseOutputText.model_construct( + type="output_text", annotations=[], logprobs=None, text="world" + ) + message = ResponseOutputMessage.model_construct( + id="msg_mixed", + type="message", + status="completed", + role="assistant", + content=[null_block, valid_block], + ) + response = Response.model_construct( + id="resp_mixed", + object="response", + created_at=0, + status="completed", + model="gpt-4o-mini", + output=[message], + parallel_tool_calls=True, + text=None, + tool_choice="auto", + tools=[], + truncation="disabled", + ) + assert response.output_text == "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