From dce4950cabb64c35770eee10984ed0103493f4eb Mon Sep 17 00:00:00 2001 From: Michael Hahn Date: Wed, 12 Nov 2025 15:53:59 -0800 Subject: [PATCH 1/2] Return ModelRequestParameters and ModelSettings on the ModelRequest object --- pydantic_ai_slim/pydantic_ai/_agent_graph.py | 4 ++ .../pydantic_ai/_model_request_parameters.py | 48 +++++++++++++++++++ pydantic_ai_slim/pydantic_ai/messages.py | 24 ++++++++++ .../pydantic_ai/models/__init__.py | 33 +------------ tests/test_messages.py | 47 +++++++++++++++++- 5 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 pydantic_ai_slim/pydantic_ai/_model_request_parameters.py diff --git a/pydantic_ai_slim/pydantic_ai/_agent_graph.py b/pydantic_ai_slim/pydantic_ai/_agent_graph.py index 91cda373a5..69c95251b2 100644 --- a/pydantic_ai_slim/pydantic_ai/_agent_graph.py +++ b/pydantic_ai_slim/pydantic_ai/_agent_graph.py @@ -496,6 +496,10 @@ async def _prepare_request( model_request_parameters = await _prepare_request_parameters(ctx) model_settings = ctx.deps.model_settings + # Record metadata on the ModelRequest (the last request in the original history) + self.request.model_request_parameters = model_request_parameters + self.request.model_settings = model_settings + usage = ctx.state.usage if ctx.deps.usage_limits.count_tokens_before_request: # Copy to avoid modifying the original usage object with the counted usage diff --git a/pydantic_ai_slim/pydantic_ai/_model_request_parameters.py b/pydantic_ai_slim/pydantic_ai/_model_request_parameters.py new file mode 100644 index 0000000000..58ff4527d1 --- /dev/null +++ b/pydantic_ai_slim/pydantic_ai/_model_request_parameters.py @@ -0,0 +1,48 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass, field +from functools import cached_property +from typing import TYPE_CHECKING, Any + +from . import _utils +from .builtin_tools import AbstractBuiltinTool + +if TYPE_CHECKING: + from .tools import ToolDefinition +else: # pragma: no cover + ToolDefinition = Any + +if TYPE_CHECKING: + from ._output import OutputObjectDefinition + from .output import OutputMode + +__all__ = ('ModelRequestParameters',) + + +@dataclass(repr=False, kw_only=True) +class ModelRequestParameters: + """Configuration for an agent's request to a model, specifically related to tools and output handling.""" + + function_tools: list[ToolDefinition] = field(default_factory=list) + builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list) + + output_mode: OutputMode = 'text' + output_object: OutputObjectDefinition | None = None + output_tools: list[ToolDefinition] = field(default_factory=list) + prompted_output_template: str | None = None + allow_text_output: bool = True + allow_image_output: bool = False + + @cached_property + def tool_defs(self) -> dict[str, ToolDefinition]: + return {tool_def.name: tool_def for tool_def in [*self.function_tools, *self.output_tools]} + + @cached_property + def prompted_output_instructions(self) -> str | None: + if self.output_mode == 'prompted' and self.prompted_output_template and self.output_object: + from ._output import PromptedOutputSchema + + return PromptedOutputSchema.build_instructions(self.prompted_output_template, self.output_object) + return None + + __repr__ = _utils.dataclasses_no_defaults_repr diff --git a/pydantic_ai_slim/pydantic_ai/messages.py b/pydantic_ai_slim/pydantic_ai/messages.py index f2e3d5eef8..299e260b24 100644 --- a/pydantic_ai_slim/pydantic_ai/messages.py +++ b/pydantic_ai_slim/pydantic_ai/messages.py @@ -16,13 +16,21 @@ from typing_extensions import deprecated from . import _otel_messages, _utils +from ._model_request_parameters import ModelRequestParameters from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc from .exceptions import UnexpectedModelBehavior +from .settings import ModelSettings from .usage import RequestUsage if TYPE_CHECKING: from .models.instrumented import InstrumentationSettings + ModelRequestParametersField = ModelRequestParameters | None + ModelSettingsField = ModelSettings | None +else: # pragma: no cover + ModelRequestParametersField = Any + ModelSettingsField = Any + AudioMediaType: TypeAlias = Literal['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac', 'audio/aiff', 'audio/aac'] ImageMediaType: TypeAlias = Literal['image/jpeg', 'image/png', 'image/gif', 'image/webp'] @@ -945,6 +953,22 @@ class ModelRequest: instructions: str | None = None """The instructions for the model.""" + model_request_parameters: Annotated[ModelRequestParametersField, pydantic.Field(exclude=True, repr=False)] = field( + default=None, repr=False + ) + """Full request parameters captured for this request. + + Available for introspection during a run. This field is excluded from serialization. + """ + + model_settings: Annotated[ModelSettingsField, pydantic.Field(exclude=True, repr=False)] = field( + default=None, repr=False + ) + """Effective model settings that were applied to this request. + + Available for introspection during a run. This field is excluded from serialization. + """ + kind: Literal['request'] = 'request' """Message type identifier, this is available on all parts as a discriminator.""" diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index f89773a3e7..f86cba3090 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -19,12 +19,11 @@ import httpx from typing_extensions import TypeAliasType, TypedDict -from .. import _utils from .._json_schema import JsonSchemaTransformer -from .._output import OutputObjectDefinition, PromptedOutputSchema +from .._model_request_parameters import ModelRequestParameters +from .._output import OutputObjectDefinition from .._parts_manager import ModelResponsePartsManager from .._run_context import RunContext -from ..builtin_tools import AbstractBuiltinTool from ..exceptions import UserError from ..messages import ( BaseToolCallPart, @@ -45,7 +44,6 @@ ToolCallPart, VideoUrl, ) -from ..output import OutputMode from ..profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec from ..providers import Provider, infer_provider from ..settings import ModelSettings, merge_model_settings @@ -308,33 +306,6 @@ """ -@dataclass(repr=False, kw_only=True) -class ModelRequestParameters: - """Configuration for an agent's request to a model, specifically related to tools and output handling.""" - - function_tools: list[ToolDefinition] = field(default_factory=list) - builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list) - - output_mode: OutputMode = 'text' - output_object: OutputObjectDefinition | None = None - output_tools: list[ToolDefinition] = field(default_factory=list) - prompted_output_template: str | None = None - allow_text_output: bool = True - allow_image_output: bool = False - - @cached_property - def tool_defs(self) -> dict[str, ToolDefinition]: - return {tool_def.name: tool_def for tool_def in [*self.function_tools, *self.output_tools]} - - @cached_property - def prompted_output_instructions(self) -> str | None: - if self.output_mode == 'prompted' and self.prompted_output_template and self.output_object: - return PromptedOutputSchema.build_instructions(self.prompted_output_template, self.output_object) - return None - - __repr__ = _utils.dataclasses_no_defaults_repr - - class Model(ABC): """Abstract class for a model.""" diff --git a/tests/test_messages.py b/tests/test_messages.py index 9b6ad3fbb8..803d760561 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -26,6 +26,9 @@ UserPromptPart, VideoUrl, ) +from pydantic_ai.builtin_tools import ImageGenerationTool +from pydantic_ai.models import ModelRequestParameters, ToolDefinition +from pydantic_ai.settings import ModelSettings from .conftest import IsDatetime, IsNow, IsStr @@ -404,7 +407,7 @@ def test_pre_usage_refactor_messages_deserializable(): content='What is the capital of Mexico?', timestamp=IsNow(tz=timezone.utc), ) - ] + ], ), ModelResponse( parts=[TextPart(content='Mexico City.')], @@ -605,3 +608,45 @@ def test_binary_content_validation_with_optional_identifier(): 'identifier': 'foo', } ) + + +def test_model_request_tool_tracking_excluded_from_serialization(): + """Test that request metadata is accessible but not serialized.""" + tool_def = ToolDefinition( + name='test_tool', + description='A test tool', + parameters_json_schema={'type': 'object', 'properties': {}}, + ) + output_tool_def = ToolDefinition( + name='request_output', + description='An output tool', + parameters_json_schema={'type': 'object', 'properties': {}}, + ) + + model_request_parameters = ModelRequestParameters( + function_tools=[tool_def], + builtin_tools=[ImageGenerationTool()], + output_tools=[output_tool_def], + ) + model_settings = ModelSettings(max_tokens=256) + + request = ModelRequest( + parts=[UserPromptPart('test prompt')], + instructions='test instructions', + model_request_parameters=model_request_parameters, + model_settings=model_settings, + ) + + # Verify the metadata is accessible + assert request.model_request_parameters is model_request_parameters + assert request.model_settings == model_settings + params = request.model_request_parameters + assert params is not None + assert params.function_tools == [tool_def] + assert params.builtin_tools == [ImageGenerationTool()] + assert params.output_tools == [output_tool_def] + + # Serialize - fields ARE excluded + serialized = ModelMessagesTypeAdapter.dump_python([request], mode='json') + assert 'model_request_parameters' not in serialized[0] + assert 'model_settings' not in serialized[0] From 618e9a1719cee8f44749e352c07d26b7334ee684 Mon Sep 17 00:00:00 2001 From: Michael Hahn Date: Wed, 12 Nov 2025 17:14:28 -0800 Subject: [PATCH 2/2] Set compare=False to avoid breaking snapshots --- pydantic_ai_slim/pydantic_ai/messages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/messages.py b/pydantic_ai_slim/pydantic_ai/messages.py index 299e260b24..28f9f0ec91 100644 --- a/pydantic_ai_slim/pydantic_ai/messages.py +++ b/pydantic_ai_slim/pydantic_ai/messages.py @@ -954,7 +954,7 @@ class ModelRequest: """The instructions for the model.""" model_request_parameters: Annotated[ModelRequestParametersField, pydantic.Field(exclude=True, repr=False)] = field( - default=None, repr=False + default=None, repr=False, compare=False ) """Full request parameters captured for this request. @@ -962,7 +962,7 @@ class ModelRequest: """ model_settings: Annotated[ModelSettingsField, pydantic.Field(exclude=True, repr=False)] = field( - default=None, repr=False + default=None, repr=False, compare=False ) """Effective model settings that were applied to this request.