Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/openai/lib/_parsing/_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def parse_response(
type_=ParsedResponseOutputText[TextFormatT],
value={
**item.to_dict(),
"parsed": parse_text(item.text, text_format=text_format),
"parsed": None if item.text is None else parse_text(item.text, text_format=text_format),
},
)
)
Expand Down
3 changes: 2 additions & 1 deletion src/openai/types/responses/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ def output_text(self) -> str:
if output.type == "message":
for content in output.content:
if content.type == "output_text":
texts.append(content.text)
if content.text is not None:
texts.append(content.text)

return "".join(texts)
2 changes: 1 addition & 1 deletion src/openai/types/responses/response_output_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class ResponseOutputText(BaseModel):
annotations: List[Annotation]
"""The annotations of the text output."""

text: str
text: Optional[str]
"""The text output from the model."""

type: Literal["output_text"]
Expand Down
87 changes: 87 additions & 0 deletions tests/lib/responses/test_responses.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from __future__ import annotations

from typing import Any
from typing_extensions import TypeVar

import pytest
from respx import MockRouter
from pydantic import BaseModel
from inline_snapshot import snapshot

from openai import OpenAI, AsyncOpenAI
from openai._utils import assert_signatures_in_sync
from openai.lib._parsing._responses import parse_response
from openai.types.responses.response import Response

from ...conftest import base_url
from ..snapshots import make_snapshot_request
Expand All @@ -21,6 +25,56 @@
# `OPENAI_LIVE=1 pytest --inline-snapshot=fix -p no:xdist -o addopts=""`


def _response_with_output_text_items(items: list[dict[str, Any]]) -> Response:
return Response.model_validate(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use v1-compatible validation in test helper

In the test-pydantic-v1 nox session, noxfile.py installs pydantic<2 and runs the whole pytest suite, but this helper now calls Response.model_validate(...) unconditionally. The SDK's BaseModel does not shim model_validate for pydantic v1, so that CI path will fail with AttributeError before these regressions run. Use the repo's compatibility helper or Response.parse_obj(...) here instead.

Useful? React with 👍 / 👎.

{
"id": "resp_null_output_text",
"object": "response",
"created_at": 0,
"status": "completed",
"background": False,
"error": None,
"incomplete_details": None,
"instructions": None,
"max_output_tokens": None,
"max_tool_calls": None,
"model": "gpt-4o-mini",
"output": [
{
"id": "msg_null_output_text",
"type": "message",
"status": "completed",
"role": "assistant",
"content": items,
}
],
"parallel_tool_calls": True,
"previous_response_id": None,
"prompt_cache_key": None,
"reasoning": {"effort": None, "summary": None},
"safety_identifier": None,
"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": 1,
"input_tokens_details": {"cached_tokens": 0},
"output_tokens": 1,
"output_tokens_details": {"reasoning_tokens": 0},
"total_tokens": 2,
},
"user": None,
"metadata": {},
}
)


@pytest.mark.respx(base_url=base_url)
def test_output_text(client: OpenAI, respx_mock: MockRouter) -> None:
response = make_snapshot_request(
Expand All @@ -41,6 +95,39 @@ def test_output_text(client: OpenAI, respx_mock: MockRouter) -> None:
)


def test_output_text_skips_null_content_items() -> None:
response = _response_with_output_text_items(
[
{"type": "output_text", "annotations": [], "logprobs": [], "text": None},
{"type": "output_text", "annotations": [], "logprobs": [], "text": "hello"},
]
)

assert response.output_text == "hello"


def test_parse_response_skips_null_output_text_items() -> None:
class Message(BaseModel):
message: str

response = _response_with_output_text_items(
[
{"type": "output_text", "annotations": [], "logprobs": [], "text": None},
{"type": "output_text", "annotations": [], "logprobs": [], "text": '{"message":"hello"}'},
]
)

parsed = parse_response(text_format=Message, input_tools=None, response=response)
message = parsed.output[0]
assert message.type == "message"
content = message.content

assert content[0].type == "output_text"
assert content[1].type == "output_text"
assert content[0].parsed is None
assert content[1].parsed == Message(message="hello")


@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
Expand Down