Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions src/ai/types/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down
19 changes: 13 additions & 6 deletions tests/types/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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]
Expand Down
Loading