From 642303bd3707c6924af3b8b7b54e7d31de7bf5c4 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 17 Jun 2026 16:18:15 -0700 Subject: [PATCH] Fix the other things using the model_input "singleton" also --- src/ai/types/messages.py | 4 ++-- tests/types/test_messages.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/ai/types/messages.py b/src/ai/types/messages.py index 831bc3d..2067259 100644 --- a/src/ai/types/messages.py +++ b/src/ai/types/messages.py @@ -225,7 +225,7 @@ def is_error(self) -> bool: def get_model_input(self) -> Any: """Return the value the LLM should see, falling back to ``result``.""" - if self.model_input is _MODEL_INPUT_UNSET: + if isinstance(self.model_input, _ModelInputUnset): return self.result return self.model_input @@ -236,7 +236,7 @@ def set_model_input(self, value: Any) -> None: @property def has_model_input(self) -> bool: """Whether ``set_model_input`` has been called on this part.""" - return self.model_input is not _MODEL_INPUT_UNSET + return not isinstance(self.model_input, _ModelInputUnset) class ToolCallPart(pydantic.BaseModel): diff --git a/tests/types/test_messages.py b/tests/types/test_messages.py index 937916d..71aa772 100644 --- a/tests/types/test_messages.py +++ b/tests/types/test_messages.py @@ -288,12 +288,12 @@ def test_tool_result_file_part_base64_valid_after_round_trip() -> None: def test_tool_result_without_model_input_serializes_after_deep_copy() -> None: """A deep-copied ToolResultPart with no model_input still serializes. - ``model_input`` defaults to the ``_MODEL_INPUT_UNSET`` singleton and is - dropped from output via ``exclude_if=lambda v: v is _MODEL_INPUT_UNSET``, - an identity check. ``model_copy(deep=True)`` rebuilds the sentinel into a - *new* instance, so the identity check fails, the field is no longer - excluded, and pydantic tries to serialize the bare sentinel. Client apps - that deep-copy messages hit this on serialize. + ``model_input`` defaults to the ``_MODEL_INPUT_UNSET`` singleton, and + the sentinel checks (``exclude_if``, ``has_model_input``, + ``get_model_input``) test for it by type. ``model_copy(deep=True)`` + rebuilds the sentinel into a *new* instance: an identity (``is``) check + would miss it, leave the field un-excluded, and make pydantic choke on + the bare sentinel. Client apps that deep-copy messages hit this. """ msg = messages.Message( role="tool", @@ -309,6 +309,13 @@ def test_tool_result_without_model_input_serializes_after_deep_copy() -> None: cloned = msg.model_copy(deep=True) + # The clone's sentinel is a fresh instance; the type-based checks must + # still treat it as unset. + cpart = cloned.parts[0] + assert isinstance(cpart, messages.ToolResultPart) + assert not cpart.has_model_input + assert cpart.get_model_input() == {"ok": 1} + j = cloned.model_dump_json() restored = messages.Message.model_validate_json(j) rpart = restored.parts[0]