Skip to content

Commit 40b5c78

Browse files
committed
move builtin tool support to profile and move check to base model class
1 parent 0701b22 commit 40b5c78

File tree

18 files changed

+90
-100
lines changed

18 files changed

+90
-100
lines changed

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,16 @@ def prepare_request(
486486
if params.allow_image_output and not self.profile.supports_image_output:
487487
raise UserError('Image output is not supported by this model.')
488488

489+
# Check if builtin tools are supported
490+
if params.builtin_tools:
491+
supported_types = self.profile.supported_builtin_tools
492+
for tool in params.builtin_tools:
493+
if not isinstance(tool, tuple(supported_types)):
494+
raise UserError(
495+
f'Builtin tool {type(tool).__name__} is not supported by this model. '
496+
f'Supported tools: {[t.__name__ for t in supported_types]}'
497+
)
498+
489499
return model_settings, params
490500

491501
@property
@@ -499,24 +509,39 @@ def label(self) -> str:
499509
"""Human-friendly display label for the model."""
500510
return _format_model_label(self.model_name)
501511

502-
@classmethod
503-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
504-
"""Return the set of builtin tool types this model class can handle.
512+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
513+
"""Return the set of builtin tool types this model can handle.
514+
515+
Args:
516+
profile: The resolved model profile (passed to avoid circular dependency with self.profile)
505517
506518
Subclasses should override this to reflect their actual capabilities.
507519
Default is empty set - subclasses must explicitly declare support.
508520
"""
509-
return frozenset() # pragma: no cover
521+
return frozenset()
510522

511523
@cached_property
512524
def profile(self) -> ModelProfile:
513-
"""The model profile."""
525+
"""The model profile.
526+
527+
We use this to compute the intersection of the profile's supported_builtin_tools
528+
and the model's implemented tools, ensuring model.profile.supported_builtin_tools
529+
is the single source of truth for what builtin tools are actually usable.
530+
"""
514531
_profile = self._profile
515532
if callable(_profile):
516533
_profile = _profile(self.model_name)
517534

518535
if _profile is None:
519-
return DEFAULT_PROFILE
536+
_profile = DEFAULT_PROFILE
537+
538+
# Compute intersection: profile's allowed tools & model's implemented tools
539+
model_supported = self.supported_builtin_tools(_profile)
540+
profile_supported = _profile.supported_builtin_tools
541+
effective_tools = profile_supported & model_supported
542+
543+
if effective_tools != profile_supported:
544+
_profile = replace(_profile, supported_builtin_tools=effective_tools)
520545

521546
return _profile
522547

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
ToolReturnPart,
4545
UserPromptPart,
4646
)
47-
from ..profiles import ModelProfileSpec
47+
from ..profiles import ModelProfile, ModelProfileSpec
4848
from ..providers import Provider, infer_provider
4949
from ..providers.anthropic import AsyncAnthropicClient
5050
from ..settings import ModelSettings, merge_model_settings
@@ -261,9 +261,8 @@ def system(self) -> str:
261261
"""The model provider."""
262262
return self._provider.name
263263

264-
@classmethod
265-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
266-
"""Return the set of builtin tool types this model class can handle."""
264+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
265+
"""Return the set of builtin tool types this model can handle."""
267266
return frozenset({WebSearchTool, CodeExecutionTool, WebFetchTool, MemoryTool, MCPServerTool})
268267

269268
async def request(

pydantic_ai_slim/pydantic_ai/models/bedrock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,8 @@ async def _messages_create(
433433
if tool_config:
434434
params['toolConfig'] = tool_config
435435

436-
if model_request_parameters.builtin_tools:
436+
if model_request_parameters.builtin_tools: # pragma: no cover
437+
# this check is done in the base Model class - leave this as a placeholder for when Bedrock supports built-in tools
437438
raise UserError('Bedrock does not support built-in tools')
438439

439440
# Bedrock supports a set of specific extra parameters

pydantic_ai_slim/pydantic_ai/models/cohere.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from typing_extensions import assert_never
88

9-
from pydantic_ai.exceptions import ModelAPIError, UserError
9+
from pydantic_ai.exceptions import ModelAPIError
1010

1111
from .. import ModelHTTPError, usage
1212
from .._utils import generate_tool_call_id as _generate_tool_call_id, guard_tool_call_id as _guard_tool_call_id
@@ -175,9 +175,6 @@ async def _chat(
175175
) -> V2ChatResponse:
176176
tools = self._get_tools(model_request_parameters)
177177

178-
if model_request_parameters.builtin_tools:
179-
raise UserError('Cohere does not support built-in tools')
180-
181178
cohere_messages = self._map_messages(messages, model_request_parameters)
182179
try:
183180
return await self.client.chat(

pydantic_ai_slim/pydantic_ai/models/function.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,7 @@ def system(self) -> str:
201201
"""The system / model provider."""
202202
return self._system
203203

204-
@classmethod
205-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
204+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
206205
"""FunctionModel supports all builtin tools for testing flexibility."""
207206
from ..builtin_tools import get_builtin_tool_types
208207

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
UserPromptPart,
3838
VideoUrl,
3939
)
40-
from ..profiles import ModelProfileSpec
40+
from ..profiles import ModelProfile, ModelProfileSpec
4141
from ..profiles.google import GoogleModelProfile
4242
from ..providers import Provider, infer_provider
4343
from ..settings import ModelSettings
@@ -229,9 +229,8 @@ def system(self) -> str:
229229
"""The model provider."""
230230
return self._provider.name
231231

232-
@classmethod
233-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
234-
"""Return the set of builtin tool types this model class can handle."""
232+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
233+
"""Return the set of builtin tool types this model can handle."""
235234
return frozenset({WebSearchTool, CodeExecutionTool, WebFetchTool, ImageGenerationTool})
236235

237236
def prepare_request(

pydantic_ai_slim/pydantic_ai/models/groq.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,8 @@ def system(self) -> str:
179179
"""The model provider."""
180180
return self._provider.name
181181

182-
@classmethod
183-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
184-
"""Return the set of builtin tool types this model class can handle."""
182+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
183+
"""Return the set of builtin tool types this model can handle."""
185184
return frozenset({WebSearchTool})
186185

187186
async def request(
@@ -389,7 +388,7 @@ def _get_builtin_tools(
389388
if isinstance(tool, WebSearchTool):
390389
if not GroqModelProfile.from_profile(self.profile).groq_always_has_web_search_builtin_tool:
391390
raise UserError('`WebSearchTool` is not supported by Groq') # pragma: no cover
392-
else:
391+
else: # pragma: no cover
393392
raise UserError(
394393
f'`{tool.__class__.__name__}` is not supported by `GroqModel`. If it should be, please file an issue.'
395394
)

pydantic_ai_slim/pydantic_ai/models/huggingface.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ async def _completions_create(
229229
else:
230230
tool_choice = 'auto'
231231

232-
if model_request_parameters.builtin_tools:
232+
if model_request_parameters.builtin_tools: # pragma: no cover
233+
# this check is done in the base Model class - leave this as a placeholder for when Bedrock supports built-in tools
233234
raise UserError('HuggingFace does not support built-in tools')
234235

235236
hf_messages = await self._map_messages(messages, model_request_parameters)

pydantic_ai_slim/pydantic_ai/models/mistral.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,8 @@ async def _completions_create(
224224
"""Make a non-streaming request to the model."""
225225
# TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
226226
# See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
227-
if model_request_parameters.builtin_tools:
227+
if model_request_parameters.builtin_tools: # pragma: no cover
228+
# this check is done in the base Model class - leave this as a placeholder for when Bedrock supports built-in tools
228229
raise UserError('Mistral does not support built-in tools')
229230

230231
try:

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,15 @@ def system(self) -> str:
424424
"""The model provider."""
425425
return self._provider.name
426426

427-
@classmethod
428-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
429-
"""Return the set of builtin tool types this model class can handle."""
430-
return frozenset({WebSearchTool})
427+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
428+
"""Return the set of builtin tool types this model can handle.
429+
430+
WebSearchTool is only supported if openai_chat_supports_web_search is True.
431+
"""
432+
openai_profile = OpenAIModelProfile.from_profile(profile)
433+
if openai_profile.openai_chat_supports_web_search:
434+
return frozenset({WebSearchTool})
435+
return frozenset()
431436

432437
@property
433438
@deprecated('Set the `system_prompt_role` in the `OpenAIModelProfile` instead.')
@@ -699,12 +704,6 @@ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[c
699704
def _get_web_search_options(self, model_request_parameters: ModelRequestParameters) -> WebSearchOptions | None:
700705
for tool in model_request_parameters.builtin_tools:
701706
if isinstance(tool, WebSearchTool): # pragma: no branch
702-
if not OpenAIModelProfile.from_profile(self.profile).openai_chat_supports_web_search:
703-
raise UserError(
704-
f'WebSearchTool is not supported with `OpenAIChatModel` and model {self.model_name!r}. '
705-
f'Please use `OpenAIResponsesModel` instead.'
706-
)
707-
708707
if tool.user_location:
709708
return WebSearchOptions(
710709
search_context_size=tool.search_context_size,
@@ -714,10 +713,7 @@ def _get_web_search_options(self, model_request_parameters: ModelRequestParamete
714713
),
715714
)
716715
return WebSearchOptions(search_context_size=tool.search_context_size)
717-
else:
718-
raise UserError(
719-
f'`{tool.__class__.__name__}` is not supported by `OpenAIChatModel`. If it should be, please file an issue.'
720-
)
716+
return None
721717

722718
@dataclass
723719
class _MapModelResponseContext:
@@ -1096,9 +1092,8 @@ def system(self) -> str:
10961092
"""The model provider."""
10971093
return self._provider.name
10981094

1099-
@classmethod
1100-
def supported_builtin_tools(cls) -> frozenset[type[AbstractBuiltinTool]]:
1101-
"""Return the set of builtin tool types this model class can handle."""
1095+
def supported_builtin_tools(self, profile: ModelProfile) -> frozenset[type[AbstractBuiltinTool]]:
1096+
"""Return the set of builtin tool types this model can handle."""
11021097
return frozenset({WebSearchTool, CodeExecutionTool, MCPServerTool, ImageGenerationTool})
11031098

11041099
async def request(

0 commit comments

Comments
 (0)