Skip to content

Commit 91a21ca

Browse files
committed
Fall back to model_dump_json for OpenAI payload serialization
OpenAI response and stream event types whose pydantic serializer is a lazily-built MockValSer cannot be serialized by the generic any-schema serializer, raising PydanticSerializationError (e.g. when streaming via WorkflowStreamClient). The model's own model_dump_json() handles them. Fixes #1585
1 parent 8d66724 commit 91a21ca

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

temporalio/contrib/openai_agents/_temporal_openai_agents.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from datetime import timedelta
99

1010
import pydantic
11+
import pydantic_core
1112
from agents import ModelProvider, Trace, set_trace_provider
1213
from agents.run import get_default_agent_runner, set_default_agent_runner
1314
from agents.tracing import get_trace_provider
@@ -110,6 +111,21 @@ class _OpenAIJSONPlainPayloadConverter(PydanticJSONPlainPayloadConverter):
110111
side does not, so fall back to lenient construction when validation fails.
111112
"""
112113

114+
def to_payload(self, value: typing.Any) -> temporalio.api.common.v1.Payload | None:
115+
try:
116+
return super().to_payload(value)
117+
except pydantic_core.PydanticSerializationError:
118+
dump = getattr(value, "model_dump_json", None)
119+
if dump is None:
120+
raise
121+
exclude_unset = (
122+
self._to_json_options.exclude_unset if self._to_json_options else False
123+
)
124+
return temporalio.api.common.v1.Payload(
125+
metadata={"encoding": self.encoding.encode()},
126+
data=dump(exclude_unset=exclude_unset).encode(),
127+
)
128+
113129
def from_payload(
114130
self,
115131
payload: temporalio.api.common.v1.Payload,

tests/contrib/openai_agents/test_openai.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,6 +1415,43 @@ async def test_response_serialization():
14151415
await pydantic_data_converter.encode([model_response])
14161416

14171417

1418+
def test_openai_converter_serialization_fallback(monkeypatch: pytest.MonkeyPatch):
1419+
import pydantic_core
1420+
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
1421+
1422+
from temporalio.contrib.openai_agents._temporal_openai_agents import (
1423+
_OpenAIJSONPlainPayloadConverter,
1424+
)
1425+
from temporalio.contrib.pydantic import ToJsonOptions
1426+
1427+
converter = _OpenAIJSONPlainPayloadConverter(ToJsonOptions(exclude_unset=True))
1428+
1429+
class _RaisingSerializer:
1430+
def to_json(self, *_args: Any, **_kwargs: Any) -> bytes:
1431+
raise pydantic_core.PydanticSerializationError("forced")
1432+
1433+
monkeypatch.setattr(converter, "_schema_serializer", _RaisingSerializer())
1434+
1435+
value = ResponseOutputMessage(
1436+
id="",
1437+
content=[ResponseOutputText(text="hello", annotations=[], type="output_text")],
1438+
role="assistant",
1439+
status="completed",
1440+
type="message",
1441+
)
1442+
payload = converter.to_payload(value)
1443+
assert payload is not None
1444+
assert payload.metadata["encoding"] == b"json/plain"
1445+
assert payload.data == value.model_dump_json(exclude_unset=True).encode()
1446+
1447+
decoded = converter.from_payload(payload, type(value))
1448+
assert decoded == value
1449+
1450+
# A non-model value has no model_dump_json, so the error propagates.
1451+
with pytest.raises(pydantic_core.PydanticSerializationError):
1452+
converter.to_payload(object())
1453+
1454+
14181455
async def assert_status_retry_behavior(status: int, client: Client, should_retry: bool):
14191456
def status_error(status: int):
14201457
with workflow.unsafe.imports_passed_through():

0 commit comments

Comments
 (0)