From b4e5546e539c7e0bbd751217c0a67d17f2eca29d Mon Sep 17 00:00:00 2001 From: Marcus Wood Date: Thu, 22 Oct 2026 21:37:58 +0000 Subject: [PATCH 1/3] fix(agents): clarify result schema warnings --- src/openai/lib/beta/agents/_result.py | 24 ++++++++++++++- tests/lib/streaming/agents/test_results.py | 35 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/openai/lib/beta/agents/_result.py b/src/openai/lib/beta/agents/_result.py index 9bcb2ba43d..17808279c7 100644 --- a/src/openai/lib/beta/agents/_result.py +++ b/src/openai/lib/beta/agents/_result.py @@ -1,17 +1,22 @@ from __future__ import annotations +import warnings from copy import deepcopy from typing import Any, Generic, Iterable from dataclasses import dataclass from typing_extensions import Literal, TypeVar +from pydantic import ValidationError + from ._output import validate_output_type +from ...._compat import PYDANTIC_V1, parse_obj from ...._exceptions import OpenAIError from ..._parsing._completions import _parse_content from ....types.beta.agent_session import RequiredAction from ....types.beta.agent_session_event import AgentSessionEvent from ....types.beta.agents.sessions.turn import Turn from ....types.beta.agent_session_message import AgentSessionMessage +from ....types.beta.agent_session_assistant_message import AgentSessionAssistantMessage OutputT = TypeVar("OutputT", default=Any) ParseT = TypeVar("ParseT") @@ -135,7 +140,24 @@ def accept(self, event: AgentSessionEvent) -> None: and item.phase != "commentary" and item.status == "completed" ): - message = AgentSessionMessage.construct(_fields_set=None, **deepcopy(item.to_dict())) + data = item.to_dict(warnings=PYDANTIC_V1) + try: + parse_obj(AgentSessionAssistantMessage, data) + except ValidationError as exc: + # Locations contain schema field names and list indices, not values. + issues = [f"{'.'.join(map(str, error['loc']))}: {error['type']}" for error in exc.errors()] + else: + issues = [] + # Warn outside the handler so warnings-as-errors do not chain the validation error. + if issues: + warnings.warn( + "Agent result message did not match the response schema: " + + "; ".join(issues) + + ". Values were preserved, but result helpers may fail. Check your gateway's response format " + "or report this schema mismatch and your SDK version to the SDK maintainers.", + stacklevel=2, + ) + message = AgentSessionMessage.construct(_fields_set=None, **deepcopy(data)) self._messages[item.id] = (event.output_index, message) elif event.type == "agent.session.in_progress": self.required_actions = [] diff --git a/tests/lib/streaming/agents/test_results.py b/tests/lib/streaming/agents/test_results.py index 958fc3b030..e1752fff81 100644 --- a/tests/lib/streaming/agents/test_results.py +++ b/tests/lib/streaming/agents/test_results.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import Any, Iterator, cast from typing_extensions import override @@ -147,6 +148,40 @@ async def test_empty_success(sdk: OpenAI | AsyncOpenAI, creation: bool) -> None: assert result.messages == [] +async def test_result_schema_warning(sdk: OpenAI | AsyncOpenAI, server: ResultServer, creation: bool) -> None: + sdk._strict_response_validation = False + event = message() + event["item"]["content"][0]["text"] = ["synthetic-answer"] + + def reset() -> None: + server.body = EventBody([turn_event("created"), event, turn_event("completed"), idle()]) + + reset() + with pytest.warns(UserWarning, match=r"content\.0\.text: (string_type|type_error.str)") as captured: + result = await collect(sdk, creation) + assert len(captured) == 1 + warning = captured[0].message + assert "synthetic-answer" not in str(warning) + content = result.messages[0].content[0] + assert content.type == "output_text" + assert content.text == ["synthetic-answer"] + + reset() + with warnings.catch_warnings(): + warnings.simplefilter("error", UserWarning) + with pytest.raises(AgentTurnResultError) as exc: + await collect(sdk, creation) + assert isinstance(exc.value.__cause__, UserWarning) + assert exc.value.__cause__.__context__ is None + + event["item"]["content"][0]["text"] = "synthetic-answer" + reset() + with warnings.catch_warnings(record=True) as captured_valid: + warnings.simplefilter("always") + assert (await collect(sdk, creation)).output_text == "synthetic-answer" + assert not captured_valid + + @pytest.mark.parametrize("status", ["failed", "cancelled"]) async def test_unsuccessful_turn(sdk: OpenAI | AsyncOpenAI, server: ResultServer, creation: bool, status: str) -> None: server.body = EventBody([turn_event("created"), message(), turn_event(status), idle()]) From cd11f09ab52dc4f17ef2e3317a931bf7441e83f3 Mon Sep 17 00:00:00 2001 From: Marcus Wood Date: Thu, 22 Oct 2026 21:42:31 +0000 Subject: [PATCH 2/3] refactor(agents): simplify result warning diagnostics --- src/openai/lib/beta/agents/_result.py | 17 ++++++----------- tests/lib/streaming/agents/test_results.py | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/openai/lib/beta/agents/_result.py b/src/openai/lib/beta/agents/_result.py index 17808279c7..cb213afe10 100644 --- a/src/openai/lib/beta/agents/_result.py +++ b/src/openai/lib/beta/agents/_result.py @@ -16,7 +16,6 @@ from ....types.beta.agent_session_event import AgentSessionEvent from ....types.beta.agents.sessions.turn import Turn from ....types.beta.agent_session_message import AgentSessionMessage -from ....types.beta.agent_session_assistant_message import AgentSessionAssistantMessage OutputT = TypeVar("OutputT", default=Any) ParseT = TypeVar("ParseT") @@ -141,20 +140,16 @@ def accept(self, event: AgentSessionEvent) -> None: and item.status == "completed" ): data = item.to_dict(warnings=PYDANTIC_V1) + issues = [] try: - parse_obj(AgentSessionAssistantMessage, data) + parse_obj(type(item), data) except ValidationError as exc: - # Locations contain schema field names and list indices, not values. - issues = [f"{'.'.join(map(str, error['loc']))}: {error['type']}" for error in exc.errors()] - else: - issues = [] + issues = [(error["loc"], error["type"]) for error in exc.errors()] # Warn outside the handler so warnings-as-errors do not chain the validation error. - if issues: + for location, code in issues: warnings.warn( - "Agent result message did not match the response schema: " - + "; ".join(issues) - + ". Values were preserved, but result helpers may fail. Check your gateway's response format " - "or report this schema mismatch and your SDK version to the SDK maintainers.", + f"Agent result field {location}: {code}. " + "Check the response against the SDK schema; the original value was preserved.", stacklevel=2, ) message = AgentSessionMessage.construct(_fields_set=None, **deepcopy(data)) diff --git a/tests/lib/streaming/agents/test_results.py b/tests/lib/streaming/agents/test_results.py index e1752fff81..fe0d7666bb 100644 --- a/tests/lib/streaming/agents/test_results.py +++ b/tests/lib/streaming/agents/test_results.py @@ -157,7 +157,7 @@ def reset() -> None: server.body = EventBody([turn_event("created"), event, turn_event("completed"), idle()]) reset() - with pytest.warns(UserWarning, match=r"content\.0\.text: (string_type|type_error.str)") as captured: + with pytest.warns(UserWarning, match=r"\('content', 0, 'text'\): (string_type|type_error.str)") as captured: result = await collect(sdk, creation) assert len(captured) == 1 warning = captured[0].message From fec41fde432cb8680bae77646ec60dfab845ad71 Mon Sep 17 00:00:00 2001 From: Marcus Wood Date: Thu, 22 Oct 2026 21:47:32 +0000 Subject: [PATCH 3/3] fix(agents): clarify expected response types in warnings --- src/openai/lib/beta/agents/_result.py | 15 +++++++++++++-- tests/lib/streaming/agents/test_results.py | 4 +++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/openai/lib/beta/agents/_result.py b/src/openai/lib/beta/agents/_result.py index cb213afe10..d1f4c5d99d 100644 --- a/src/openai/lib/beta/agents/_result.py +++ b/src/openai/lib/beta/agents/_result.py @@ -139,6 +139,7 @@ def accept(self, event: AgentSessionEvent) -> None: and item.phase != "commentary" and item.status == "completed" ): + # V1 emits no serialization warnings, but its shim rejects warnings=False. data = item.to_dict(warnings=PYDANTIC_V1) issues = [] try: @@ -147,9 +148,19 @@ def accept(self, event: AgentSessionEvent) -> None: issues = [(error["loc"], error["type"]) for error in exc.errors()] # Warn outside the handler so warnings-as-errors do not chain the validation error. for location, code in issues: + path = "".join(f"[{part}]" if isinstance(part, int) else f".{part}" for part in location).lstrip( + "." + ) + expected = { + "string_type": "a string", + "type_error.str": "a string", + "list_type": "a list", + "type_error.list": "a list", + }.get(code) warnings.warn( - f"Agent result field {location}: {code}. " - "Check the response against the SDK schema; the original value was preserved.", + f"Unexpected response type at {path}: expected {expected}." + if expected + else f"Unexpected response value at {path}: {code}.", stacklevel=2, ) message = AgentSessionMessage.construct(_fields_set=None, **deepcopy(data)) diff --git a/tests/lib/streaming/agents/test_results.py b/tests/lib/streaming/agents/test_results.py index fe0d7666bb..abca51d3b3 100644 --- a/tests/lib/streaming/agents/test_results.py +++ b/tests/lib/streaming/agents/test_results.py @@ -157,7 +157,9 @@ def reset() -> None: server.body = EventBody([turn_event("created"), event, turn_event("completed"), idle()]) reset() - with pytest.warns(UserWarning, match=r"\('content', 0, 'text'\): (string_type|type_error.str)") as captured: + with pytest.warns( + UserWarning, match=r"^Unexpected response type at content\[0\]\.text: expected a string\.$" + ) as captured: result = await collect(sdk, creation) assert len(captured) == 1 warning = captured[0].message