Skip to content

Commit 56b045b

Browse files
committed
Return ModelRequestParameters and ModelSettings on the ModelRequest object
1 parent 5544e9f commit 56b045b

File tree

5 files changed

+124
-32
lines changed

5 files changed

+124
-32
lines changed

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,10 @@ async def _prepare_request(
503503
model_request_parameters = await _prepare_request_parameters(ctx)
504504

505505
model_settings = ctx.deps.model_settings
506+
# Record metadata on the ModelRequest (the last request in the original history)
507+
self.request.model_request_parameters = model_request_parameters
508+
self.request.model_settings = model_settings
509+
506510
usage = ctx.state.usage
507511
if ctx.deps.usage_limits.count_tokens_before_request:
508512
# Copy to avoid modifying the original usage object with the counted usage
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations as _annotations
2+
3+
from dataclasses import dataclass, field
4+
from functools import cached_property
5+
from typing import TYPE_CHECKING, Any
6+
7+
from . import _utils
8+
from .builtin_tools import AbstractBuiltinTool
9+
10+
if TYPE_CHECKING:
11+
from .tools import ToolDefinition
12+
else: # pragma: no cover
13+
ToolDefinition = Any
14+
15+
if TYPE_CHECKING:
16+
from ._output import OutputObjectDefinition
17+
from .output import OutputMode
18+
19+
__all__ = ('ModelRequestParameters',)
20+
21+
22+
@dataclass(repr=False, kw_only=True)
23+
class ModelRequestParameters:
24+
"""Configuration for an agent's request to a model, specifically related to tools and output handling."""
25+
26+
function_tools: list[ToolDefinition] = field(default_factory=list)
27+
builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list)
28+
29+
output_mode: OutputMode = 'text'
30+
output_object: OutputObjectDefinition | None = None
31+
output_tools: list[ToolDefinition] = field(default_factory=list)
32+
prompted_output_template: str | None = None
33+
allow_text_output: bool = True
34+
allow_image_output: bool = False
35+
36+
@cached_property
37+
def tool_defs(self) -> dict[str, ToolDefinition]:
38+
return {tool_def.name: tool_def for tool_def in [*self.function_tools, *self.output_tools]}
39+
40+
@cached_property
41+
def prompted_output_instructions(self) -> str | None:
42+
if self.output_mode == 'prompted' and self.prompted_output_template and self.output_object:
43+
from ._output import PromptedOutputSchema
44+
45+
return PromptedOutputSchema.build_instructions(self.prompted_output_template, self.output_object)
46+
return None
47+
48+
__repr__ = _utils.dataclasses_no_defaults_repr

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,21 @@
1818
from typing_extensions import deprecated
1919

2020
from . import _otel_messages, _utils
21+
from ._model_request_parameters import ModelRequestParameters
2122
from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc
2223
from .exceptions import UnexpectedModelBehavior
24+
from .settings import ModelSettings
2325
from .usage import RequestUsage
2426

2527
if TYPE_CHECKING:
2628
from .models.instrumented import InstrumentationSettings
2729

30+
ModelRequestParametersField = ModelRequestParameters | None
31+
ModelSettingsField = ModelSettings | None
32+
else: # pragma: no cover
33+
ModelRequestParametersField = Any
34+
ModelSettingsField = Any
35+
2836

2937
AudioMediaType: TypeAlias = Literal['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac', 'audio/aiff', 'audio/aac']
3038
ImageMediaType: TypeAlias = Literal['image/jpeg', 'image/png', 'image/gif', 'image/webp']
@@ -997,6 +1005,22 @@ class ModelRequest:
9971005
instructions: str | None = None
9981006
"""The instructions for the model."""
9991007

1008+
model_request_parameters: Annotated[ModelRequestParametersField, pydantic.Field(exclude=True, repr=False)] = field(
1009+
default=None, repr=False
1010+
)
1011+
"""Full request parameters captured for this request.
1012+
1013+
Available for introspection during a run. This field is excluded from serialization.
1014+
"""
1015+
1016+
model_settings: Annotated[ModelSettingsField, pydantic.Field(exclude=True, repr=False)] = field(
1017+
default=None, repr=False
1018+
)
1019+
"""Effective model settings that were applied to this request.
1020+
1021+
Available for introspection during a run. This field is excluded from serialization.
1022+
"""
1023+
10001024
kind: Literal['request'] = 'request'
10011025
"""Message type identifier, this is available on all parts as a discriminator."""
10021026

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@
1919
import httpx
2020
from typing_extensions import TypeAliasType, TypedDict
2121

22-
from .. import _utils
2322
from .._json_schema import JsonSchemaTransformer
24-
from .._output import OutputObjectDefinition, PromptedOutputSchema
23+
from .._model_request_parameters import ModelRequestParameters
24+
from .._output import OutputObjectDefinition
2525
from .._parts_manager import ModelResponsePartsManager
2626
from .._run_context import RunContext
27-
from ..builtin_tools import AbstractBuiltinTool
2827
from ..exceptions import UserError
2928
from ..messages import (
3029
BaseToolCallPart,
@@ -45,7 +44,6 @@
4544
ToolCallPart,
4645
VideoUrl,
4746
)
48-
from ..output import OutputMode
4947
from ..profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec
5048
from ..providers import Provider, infer_provider
5149
from ..settings import ModelSettings, merge_model_settings
@@ -320,33 +318,6 @@
320318
"""
321319

322320

323-
@dataclass(repr=False, kw_only=True)
324-
class ModelRequestParameters:
325-
"""Configuration for an agent's request to a model, specifically related to tools and output handling."""
326-
327-
function_tools: list[ToolDefinition] = field(default_factory=list)
328-
builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list)
329-
330-
output_mode: OutputMode = 'text'
331-
output_object: OutputObjectDefinition | None = None
332-
output_tools: list[ToolDefinition] = field(default_factory=list)
333-
prompted_output_template: str | None = None
334-
allow_text_output: bool = True
335-
allow_image_output: bool = False
336-
337-
@cached_property
338-
def tool_defs(self) -> dict[str, ToolDefinition]:
339-
return {tool_def.name: tool_def for tool_def in [*self.function_tools, *self.output_tools]}
340-
341-
@cached_property
342-
def prompted_output_instructions(self) -> str | None:
343-
if self.output_mode == 'prompted' and self.prompted_output_template and self.output_object:
344-
return PromptedOutputSchema.build_instructions(self.prompted_output_template, self.output_object)
345-
return None
346-
347-
__repr__ = _utils.dataclasses_no_defaults_repr
348-
349-
350321
class Model(ABC):
351322
"""Abstract class for a model."""
352323

tests/test_messages.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
UserPromptPart,
2828
VideoUrl,
2929
)
30+
from pydantic_ai.builtin_tools import ImageGenerationTool
31+
from pydantic_ai.models import ModelRequestParameters, ToolDefinition
32+
from pydantic_ai.settings import ModelSettings
3033

3134
from .conftest import IsDatetime, IsNow, IsStr
3235

@@ -416,7 +419,7 @@ def test_pre_usage_refactor_messages_deserializable():
416419
content='What is the capital of Mexico?',
417420
timestamp=IsNow(tz=timezone.utc),
418421
)
419-
]
422+
],
420423
),
421424
ModelResponse(
422425
parts=[TextPart(content='Mexico City.')],
@@ -659,3 +662,45 @@ def test_binary_content_from_path(tmp_path: Path):
659662
assert binary_content == snapshot(
660663
BinaryImage(data=b'\xff\xd8\xff\xe0' + b'0' * 100, media_type='image/jpeg', _identifier='bc8d49')
661664
)
665+
666+
667+
def test_model_request_tool_tracking_excluded_from_serialization():
668+
"""Test that request metadata is accessible but not serialized."""
669+
tool_def = ToolDefinition(
670+
name='test_tool',
671+
description='A test tool',
672+
parameters_json_schema={'type': 'object', 'properties': {}},
673+
)
674+
output_tool_def = ToolDefinition(
675+
name='request_output',
676+
description='An output tool',
677+
parameters_json_schema={'type': 'object', 'properties': {}},
678+
)
679+
680+
model_request_parameters = ModelRequestParameters(
681+
function_tools=[tool_def],
682+
builtin_tools=[ImageGenerationTool()],
683+
output_tools=[output_tool_def],
684+
)
685+
model_settings = ModelSettings(max_tokens=256)
686+
687+
request = ModelRequest(
688+
parts=[UserPromptPart('test prompt')],
689+
instructions='test instructions',
690+
model_request_parameters=model_request_parameters,
691+
model_settings=model_settings,
692+
)
693+
694+
# Verify the metadata is accessible
695+
assert request.model_request_parameters is model_request_parameters
696+
assert request.model_settings == model_settings
697+
params = request.model_request_parameters
698+
assert params is not None
699+
assert params.function_tools == [tool_def]
700+
assert params.builtin_tools == [ImageGenerationTool()]
701+
assert params.output_tools == [output_tool_def]
702+
703+
# Serialize - fields ARE excluded
704+
serialized = ModelMessagesTypeAdapter.dump_python([request], mode='json')
705+
assert 'model_request_parameters' not in serialized[0]
706+
assert 'model_settings' not in serialized[0]

0 commit comments

Comments
 (0)