pydantic-ai-slim 0.6.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,9 +15,12 @@ import anyio.to_thread
15
15
  from typing_extensions import ParamSpec, assert_never
16
16
 
17
17
  from pydantic_ai import _utils, usage
18
+ from pydantic_ai.exceptions import UserError
18
19
  from pydantic_ai.messages import (
19
20
  AudioUrl,
20
21
  BinaryContent,
22
+ BuiltinToolCallPart,
23
+ BuiltinToolReturnPart,
21
24
  DocumentUrl,
22
25
  ImageUrl,
23
26
  ModelMessage,
@@ -59,6 +62,8 @@ if TYPE_CHECKING:
59
62
  MessageUnionTypeDef,
60
63
  PerformanceConfigurationTypeDef,
61
64
  PromptVariableValuesTypeDef,
65
+ ReasoningContentBlockOutputTypeDef,
66
+ ReasoningTextBlockTypeDef,
62
67
  SystemContentBlockTypeDef,
63
68
  ToolChoiceTypeDef,
64
69
  ToolConfigurationTypeDef,
@@ -276,9 +281,10 @@ class BedrockConverseModel(Model):
276
281
  if reasoning_content := item.get('reasoningContent'):
277
282
  reasoning_text = reasoning_content.get('reasoningText')
278
283
  if reasoning_text: # pragma: no branch
279
- thinking_part = ThinkingPart(content=reasoning_text['text'])
280
- if reasoning_signature := reasoning_text.get('signature'):
281
- thinking_part.signature = reasoning_signature
284
+ thinking_part = ThinkingPart(
285
+ content=reasoning_text['text'],
286
+ signature=reasoning_text.get('signature'),
287
+ )
282
288
  items.append(thinking_part)
283
289
  if text := item.get('text'):
284
290
  items.append(TextPart(content=text))
@@ -339,6 +345,9 @@ class BedrockConverseModel(Model):
339
345
  if tool_config:
340
346
  params['toolConfig'] = tool_config
341
347
 
348
+ if model_request_parameters.builtin_tools:
349
+ raise UserError('Bedrock does not support built-in tools')
350
+
342
351
  # Bedrock supports a set of specific extra parameters
343
352
  if model_settings:
344
353
  if guardrail_config := model_settings.get('bedrock_guardrail_config', None):
@@ -462,7 +471,20 @@ class BedrockConverseModel(Model):
462
471
  if isinstance(item, TextPart):
463
472
  content.append({'text': item.content})
464
473
  elif isinstance(item, ThinkingPart):
465
- # NOTE: We don't pass the thinking part to Bedrock since it raises an error.
474
+ if BedrockModelProfile.from_profile(self.profile).bedrock_send_back_thinking_parts:
475
+ reasoning_text: ReasoningTextBlockTypeDef = {
476
+ 'text': item.content,
477
+ }
478
+ if item.signature:
479
+ reasoning_text['signature'] = item.signature
480
+ reasoning_content: ReasoningContentBlockOutputTypeDef = {
481
+ 'reasoningText': reasoning_text,
482
+ }
483
+ content.append({'reasoningContent': reasoning_content})
484
+ else:
485
+ # NOTE: We don't pass the thinking part to Bedrock for models other than Claude since it raises an error.
486
+ pass
487
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)):
466
488
  pass
467
489
  else:
468
490
  assert isinstance(item, ToolCallPart)
@@ -610,7 +632,11 @@ class BedrockStreamedResponse(StreamedResponse):
610
632
  delta = chunk['contentBlockDelta']['delta']
611
633
  if 'reasoningContent' in delta:
612
634
  if text := delta['reasoningContent'].get('text'):
613
- yield self._parts_manager.handle_thinking_delta(vendor_part_id=index, content=text)
635
+ yield self._parts_manager.handle_thinking_delta(
636
+ vendor_part_id=index,
637
+ content=text,
638
+ signature=delta['reasoningContent'].get('signature'),
639
+ )
614
640
  else: # pragma: no cover
615
641
  warnings.warn(
616
642
  f'Only text reasoning content is supported yet, but you got {delta["reasoningContent"]}. '
@@ -7,10 +7,13 @@ from typing import Literal, Union, cast
7
7
  from typing_extensions import assert_never
8
8
 
9
9
  from pydantic_ai._thinking_part import split_content_into_text_and_thinking
10
+ from pydantic_ai.exceptions import UserError
10
11
 
11
12
  from .. import ModelHTTPError, usage
12
13
  from .._utils import generate_tool_call_id as _generate_tool_call_id, guard_tool_call_id as _guard_tool_call_id
13
14
  from ..messages import (
15
+ BuiltinToolCallPart,
16
+ BuiltinToolReturnPart,
14
17
  ModelMessage,
15
18
  ModelRequest,
16
19
  ModelResponse,
@@ -166,6 +169,10 @@ class CohereModel(Model):
166
169
  model_request_parameters: ModelRequestParameters,
167
170
  ) -> V2ChatResponse:
168
171
  tools = self._get_tools(model_request_parameters)
172
+
173
+ if model_request_parameters.builtin_tools:
174
+ raise UserError('Cohere does not support built-in tools')
175
+
169
176
  cohere_messages = self._map_messages(messages)
170
177
  try:
171
178
  return await self.client.chat(
@@ -223,6 +230,9 @@ class CohereModel(Model):
223
230
  pass
224
231
  elif isinstance(item, ToolCallPart):
225
232
  tool_calls.append(self._map_tool_call(item))
233
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
234
+ # This is currently never returned from cohere
235
+ pass
226
236
  else:
227
237
  assert_never(item)
228
238
  message_param = AssistantChatMessageV2(role='assistant')
@@ -17,6 +17,8 @@ from .. import _utils, usage
17
17
  from .._utils import PeekableAsyncStream
18
18
  from ..messages import (
19
19
  BinaryContent,
20
+ BuiltinToolCallPart,
21
+ BuiltinToolReturnPart,
20
22
  ModelMessage,
21
23
  ModelRequest,
22
24
  ModelResponse,
@@ -331,6 +333,13 @@ def _estimate_usage(messages: Iterable[ModelMessage]) -> usage.Usage:
331
333
  response_tokens += _estimate_string_tokens(part.content)
332
334
  elif isinstance(part, ToolCallPart):
333
335
  response_tokens += 1 + _estimate_string_tokens(part.args_as_json_str())
336
+ # TODO(Marcelo): We need to add coverage here.
337
+ elif isinstance(part, BuiltinToolCallPart): # pragma: no cover
338
+ call = part
339
+ response_tokens += 1 + _estimate_string_tokens(call.args_as_json_str())
340
+ # TODO(Marcelo): We need to add coverage here.
341
+ elif isinstance(part, BuiltinToolReturnPart): # pragma: no cover
342
+ response_tokens += _estimate_string_tokens(part.model_response_str())
334
343
  else:
335
344
  assert_never(part)
336
345
  else:
@@ -20,6 +20,8 @@ from .._output import OutputObjectDefinition
20
20
  from ..exceptions import UserError
21
21
  from ..messages import (
22
22
  BinaryContent,
23
+ BuiltinToolCallPart,
24
+ BuiltinToolReturnPart,
23
25
  FileUrl,
24
26
  ModelMessage,
25
27
  ModelRequest,
@@ -610,6 +612,9 @@ def _content_model_response(m: ModelResponse) -> _GeminiContent:
610
612
  elif isinstance(item, TextPart):
611
613
  if item.content:
612
614
  parts.append(_GeminiTextPart(text=item.content))
615
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
616
+ # This is currently never returned from gemini
617
+ pass
613
618
  else:
614
619
  assert_never(item)
615
620
  return _GeminiContent(role='model', parts=parts)
@@ -8,13 +8,17 @@ from datetime import datetime
8
8
  from typing import Any, Literal, Union, cast, overload
9
9
  from uuid import uuid4
10
10
 
11
+ from google.genai.types import ExecutableCodeDict
11
12
  from typing_extensions import assert_never
12
13
 
13
14
  from .. import UnexpectedModelBehavior, _utils, usage
14
15
  from .._output import OutputObjectDefinition
16
+ from ..builtin_tools import CodeExecutionTool, WebSearchTool
15
17
  from ..exceptions import UserError
16
18
  from ..messages import (
17
19
  BinaryContent,
20
+ BuiltinToolCallPart,
21
+ BuiltinToolReturnPart,
18
22
  FileUrl,
19
23
  ModelMessage,
20
24
  ModelRequest,
@@ -54,12 +58,14 @@ try:
54
58
  FunctionDeclarationDict,
55
59
  GenerateContentConfigDict,
56
60
  GenerateContentResponse,
61
+ GoogleSearchDict,
57
62
  HttpOptionsDict,
58
63
  MediaResolution,
59
64
  Part,
60
65
  PartDict,
61
66
  SafetySettingDict,
62
67
  ThinkingConfigDict,
68
+ ToolCodeExecutionDict,
63
69
  ToolConfigDict,
64
70
  ToolDict,
65
71
  ToolListUnionDict,
@@ -213,6 +219,11 @@ class GoogleModel(Model):
213
219
  ToolDict(function_declarations=[_function_declaration_from_tool(t)])
214
220
  for t in model_request_parameters.output_tools
215
221
  ]
222
+ for tool in model_request_parameters.builtin_tools:
223
+ if isinstance(tool, WebSearchTool):
224
+ tools.append(ToolDict(google_search=GoogleSearchDict()))
225
+ elif isinstance(tool, CodeExecutionTool): # pragma: no branch
226
+ tools.append(ToolDict(code_execution=ToolCodeExecutionDict()))
216
227
  return tools or None
217
228
 
218
229
  def _get_tool_config(
@@ -499,6 +510,14 @@ def _content_model_response(m: ModelResponse) -> ContentDict:
499
510
  # please open an issue. The below code is the code to send thinking to the provider.
500
511
  # parts.append({'text': item.content, 'thought': True})
501
512
  pass
513
+ elif isinstance(item, BuiltinToolCallPart):
514
+ if item.provider_name == 'google':
515
+ if item.tool_name == 'code_execution': # pragma: no branch
516
+ parts.append({'executable_code': cast(ExecutableCodeDict, item.args)})
517
+ elif isinstance(item, BuiltinToolReturnPart):
518
+ if item.provider_name == 'google':
519
+ if item.tool_name == 'code_execution': # pragma: no branch
520
+ parts.append({'code_execution_result': item.content})
502
521
  else:
503
522
  assert_never(item)
504
523
  return ContentDict(role='model', parts=parts)
@@ -513,7 +532,22 @@ def _process_response_from_parts(
513
532
  ) -> ModelResponse:
514
533
  items: list[ModelResponsePart] = []
515
534
  for part in parts:
516
- if part.text is not None:
535
+ if part.executable_code is not None:
536
+ items.append(
537
+ BuiltinToolCallPart(
538
+ provider_name='google', args=part.executable_code.model_dump(), tool_name='code_execution'
539
+ )
540
+ )
541
+ elif part.code_execution_result is not None:
542
+ items.append(
543
+ BuiltinToolReturnPart(
544
+ provider_name='google',
545
+ tool_name='code_execution',
546
+ content=part.code_execution_result,
547
+ tool_call_id='not_provided',
548
+ )
549
+ )
550
+ elif part.text is not None:
517
551
  if part.thought:
518
552
  items.append(ThinkingPart(content=part.text))
519
553
  else:
@@ -563,7 +597,7 @@ def _metadata_as_usage(response: GenerateContentResponse) -> usage.Usage:
563
597
  details['thoughts_tokens'] = thoughts_token_count
564
598
 
565
599
  if tool_use_prompt_token_count := metadata.get('tool_use_prompt_token_count'):
566
- details['tool_use_prompt_tokens'] = tool_use_prompt_token_count # pragma: no cover
600
+ details['tool_use_prompt_tokens'] = tool_use_prompt_token_count
567
601
 
568
602
  for key, metadata_details in metadata.items():
569
603
  if key.endswith('_details') and metadata_details:
@@ -10,11 +10,16 @@ from typing import Literal, Union, cast, overload
10
10
  from typing_extensions import assert_never
11
11
 
12
12
  from pydantic_ai._thinking_part import split_content_into_text_and_thinking
13
+ from pydantic_ai.exceptions import UserError
14
+ from pydantic_ai.profiles.groq import GroqModelProfile
13
15
 
14
16
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
15
- from .._utils import guard_tool_call_id as _guard_tool_call_id, number_to_datetime
17
+ from .._utils import generate_tool_call_id, guard_tool_call_id as _guard_tool_call_id, number_to_datetime
18
+ from ..builtin_tools import CodeExecutionTool, WebSearchTool
16
19
  from ..messages import (
17
20
  BinaryContent,
21
+ BuiltinToolCallPart,
22
+ BuiltinToolReturnPart,
18
23
  DocumentUrl,
19
24
  ImageUrl,
20
25
  ModelMessage,
@@ -212,7 +217,7 @@ class GroqModel(Model):
212
217
  model_request_parameters: ModelRequestParameters,
213
218
  ) -> chat.ChatCompletion | AsyncStream[chat.ChatCompletionChunk]:
214
219
  tools = self._get_tools(model_request_parameters)
215
- # standalone function to make it easier to override
220
+ tools += self._get_builtin_tools(model_request_parameters)
216
221
  if not tools:
217
222
  tool_choice: Literal['none', 'required', 'auto'] | None = None
218
223
  elif not model_request_parameters.allow_text_output:
@@ -226,7 +231,7 @@ class GroqModel(Model):
226
231
  extra_headers = model_settings.get('extra_headers', {})
227
232
  extra_headers.setdefault('User-Agent', get_user_agent())
228
233
  return await self.client.chat.completions.create(
229
- model=str(self._model_name),
234
+ model=self._model_name,
230
235
  messages=groq_messages,
231
236
  n=1,
232
237
  parallel_tool_calls=model_settings.get('parallel_tool_calls', NOT_GIVEN),
@@ -256,6 +261,19 @@ class GroqModel(Model):
256
261
  timestamp = number_to_datetime(response.created)
257
262
  choice = response.choices[0]
258
263
  items: list[ModelResponsePart] = []
264
+ if choice.message.executed_tools:
265
+ for tool in choice.message.executed_tools:
266
+ tool_call_id = generate_tool_call_id()
267
+ items.append(
268
+ BuiltinToolCallPart(
269
+ tool_name=tool.type, args=tool.arguments, provider_name='groq', tool_call_id=tool_call_id
270
+ )
271
+ )
272
+ items.append(
273
+ BuiltinToolReturnPart(
274
+ provider_name='groq', tool_name=tool.type, content=tool.output, tool_call_id=tool_call_id
275
+ )
276
+ )
259
277
  # NOTE: The `reasoning` field is only present if `groq_reasoning_format` is set to `parsed`.
260
278
  if choice.message.reasoning is not None:
261
279
  items.append(ThinkingPart(content=choice.message.reasoning))
@@ -291,6 +309,18 @@ class GroqModel(Model):
291
309
  tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
292
310
  return tools
293
311
 
312
+ def _get_builtin_tools(
313
+ self, model_request_parameters: ModelRequestParameters
314
+ ) -> list[chat.ChatCompletionToolParam]:
315
+ tools: list[chat.ChatCompletionToolParam] = []
316
+ for tool in model_request_parameters.builtin_tools:
317
+ if isinstance(tool, WebSearchTool):
318
+ if not GroqModelProfile.from_profile(self.profile).groq_always_has_web_search_builtin_tool:
319
+ raise UserError('`WebSearchTool` is not supported by Groq') # pragma: no cover
320
+ elif isinstance(tool, CodeExecutionTool): # pragma: no branch
321
+ raise UserError('`CodeExecutionTool` is not supported by Groq')
322
+ return tools
323
+
294
324
  def _map_messages(self, messages: list[ModelMessage]) -> list[chat.ChatCompletionMessageParam]:
295
325
  """Just maps a `pydantic_ai.Message` to a `groq.types.ChatCompletionMessageParam`."""
296
326
  groq_messages: list[chat.ChatCompletionMessageParam] = []
@@ -308,6 +338,9 @@ class GroqModel(Model):
308
338
  elif isinstance(item, ThinkingPart):
309
339
  # Skip thinking parts when mapping to Groq messages
310
340
  continue
341
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
342
+ # This is currently never returned from groq
343
+ pass
311
344
  else:
312
345
  assert_never(item)
313
346
  message_param = chat.ChatCompletionAssistantMessageParam(role='assistant')
@@ -10,6 +10,7 @@ from typing import Literal, Union, cast, overload
10
10
  from typing_extensions import assert_never
11
11
 
12
12
  from pydantic_ai._thinking_part import split_content_into_text_and_thinking
13
+ from pydantic_ai.exceptions import UserError
13
14
  from pydantic_ai.providers import Provider, infer_provider
14
15
 
15
16
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
@@ -17,6 +18,8 @@ from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_
17
18
  from ..messages import (
18
19
  AudioUrl,
19
20
  BinaryContent,
21
+ BuiltinToolCallPart,
22
+ BuiltinToolReturnPart,
20
23
  DocumentUrl,
21
24
  ImageUrl,
22
25
  ModelMessage,
@@ -198,6 +201,9 @@ class HuggingFaceModel(Model):
198
201
  else:
199
202
  tool_choice = 'auto'
200
203
 
204
+ if model_request_parameters.builtin_tools:
205
+ raise UserError('HuggingFace does not support built-in tools')
206
+
201
207
  hf_messages = await self._map_messages(messages)
202
208
 
203
209
  try:
@@ -301,6 +307,9 @@ class HuggingFaceModel(Model):
301
307
  # please open an issue. The below code is the code to send thinking to the provider.
302
308
  # texts.append(f'<think>\n{item.content}\n</think>')
303
309
  pass
310
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
311
+ # This is currently never returned from huggingface
312
+ pass
304
313
  else:
305
314
  assert_never(item)
306
315
  message_param = ChatCompletionInputMessage(role='assistant') # type: ignore
@@ -12,11 +12,14 @@ from httpx import Timeout
12
12
  from typing_extensions import assert_never
13
13
 
14
14
  from pydantic_ai._thinking_part import split_content_into_text_and_thinking
15
+ from pydantic_ai.exceptions import UserError
15
16
 
16
17
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils
17
18
  from .._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc, number_to_datetime
18
19
  from ..messages import (
19
20
  BinaryContent,
21
+ BuiltinToolCallPart,
22
+ BuiltinToolReturnPart,
20
23
  DocumentUrl,
21
24
  ImageUrl,
22
25
  ModelMessage,
@@ -199,6 +202,11 @@ class MistralModel(Model):
199
202
  model_request_parameters: ModelRequestParameters,
200
203
  ) -> MistralChatCompletionResponse:
201
204
  """Make a non-streaming request to the model."""
205
+ # TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
206
+ # See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
207
+ if model_request_parameters.builtin_tools:
208
+ raise UserError('Mistral does not support built-in tools')
209
+
202
210
  try:
203
211
  response = await self.client.chat.complete_async(
204
212
  model=str(self._model_name),
@@ -233,6 +241,11 @@ class MistralModel(Model):
233
241
  response: MistralEventStreamAsync[MistralCompletionEvent] | None
234
242
  mistral_messages = self._map_messages(messages)
235
243
 
244
+ # TODO(Marcelo): We need to replace the current MistralAI client to use the beta client.
245
+ # See https://docs.mistral.ai/agents/connectors/websearch/ to support web search.
246
+ if model_request_parameters.builtin_tools:
247
+ raise UserError('Mistral does not support built-in tools')
248
+
236
249
  if (
237
250
  model_request_parameters.output_tools
238
251
  and model_request_parameters.function_tools
@@ -502,6 +515,9 @@ class MistralModel(Model):
502
515
  pass
503
516
  elif isinstance(part, ToolCallPart):
504
517
  tool_calls.append(self._map_tool_call(part))
518
+ elif isinstance(part, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
519
+ # This is currently never returned from mistral
520
+ pass
505
521
  else:
506
522
  assert_never(part)
507
523
  mistral_messages.append(MistralAssistantMessage(content=content_chunks, tool_calls=tool_calls))
@@ -11,16 +11,18 @@ from typing import Any, Literal, Union, cast, overload
11
11
  from pydantic import ValidationError
12
12
  from typing_extensions import assert_never
13
13
 
14
- from pydantic_ai._thinking_part import split_content_into_text_and_thinking
15
- from pydantic_ai.profiles.openai import OpenAIModelProfile
16
- from pydantic_ai.providers import Provider, infer_provider
14
+ from pydantic_ai.exceptions import UserError
17
15
 
18
16
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
19
17
  from .._output import DEFAULT_OUTPUT_TOOL_NAME, OutputObjectDefinition
18
+ from .._thinking_part import split_content_into_text_and_thinking
20
19
  from .._utils import guard_tool_call_id as _guard_tool_call_id, now_utc as _now_utc, number_to_datetime
20
+ from ..builtin_tools import CodeExecutionTool, WebSearchTool
21
21
  from ..messages import (
22
22
  AudioUrl,
23
23
  BinaryContent,
24
+ BuiltinToolCallPart,
25
+ BuiltinToolReturnPart,
24
26
  DocumentUrl,
25
27
  ImageUrl,
26
28
  ModelMessage,
@@ -38,16 +40,11 @@ from ..messages import (
38
40
  VideoUrl,
39
41
  )
40
42
  from ..profiles import ModelProfile, ModelProfileSpec
43
+ from ..profiles.openai import OpenAIModelProfile
44
+ from ..providers import Provider, infer_provider
41
45
  from ..settings import ModelSettings
42
46
  from ..tools import ToolDefinition
43
- from . import (
44
- Model,
45
- ModelRequestParameters,
46
- StreamedResponse,
47
- check_allow_model_requests,
48
- download_item,
49
- get_user_agent,
50
- )
47
+ from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent
51
48
 
52
49
  try:
53
50
  from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
@@ -63,6 +60,11 @@ try:
63
60
  from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio
64
61
  from openai.types.chat.chat_completion_content_part_param import File, FileFile
65
62
  from openai.types.chat.chat_completion_prediction_content_param import ChatCompletionPredictionContentParam
63
+ from openai.types.chat.completion_create_params import (
64
+ WebSearchOptions,
65
+ WebSearchOptionsUserLocation,
66
+ WebSearchOptionsUserLocationApproximate,
67
+ )
66
68
  from openai.types.responses import ComputerToolParam, FileSearchToolParam, WebSearchToolParam
67
69
  from openai.types.responses.response_input_param import FunctionCallOutput, Message
68
70
  from openai.types.shared import ReasoningEffort
@@ -298,6 +300,8 @@ class OpenAIModel(Model):
298
300
  model_request_parameters: ModelRequestParameters,
299
301
  ) -> chat.ChatCompletion | AsyncStream[ChatCompletionChunk]:
300
302
  tools = self._get_tools(model_request_parameters)
303
+ web_search_options = self._get_web_search_options(model_request_parameters)
304
+
301
305
  if not tools:
302
306
  tool_choice: Literal['none', 'required', 'auto'] | None = None
303
307
  elif (
@@ -344,6 +348,7 @@ class OpenAIModel(Model):
344
348
  seed=model_settings.get('seed', NOT_GIVEN),
345
349
  reasoning_effort=model_settings.get('openai_reasoning_effort', NOT_GIVEN),
346
350
  user=model_settings.get('openai_user', NOT_GIVEN),
351
+ web_search_options=web_search_options or NOT_GIVEN,
347
352
  service_tier=model_settings.get('openai_service_tier', NOT_GIVEN),
348
353
  prediction=model_settings.get('openai_prediction', NOT_GIVEN),
349
354
  temperature=sampling_settings.get('temperature', NOT_GIVEN),
@@ -444,6 +449,21 @@ class OpenAIModel(Model):
444
449
  tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
445
450
  return tools
446
451
 
452
+ def _get_web_search_options(self, model_request_parameters: ModelRequestParameters) -> WebSearchOptions | None:
453
+ for tool in model_request_parameters.builtin_tools:
454
+ if isinstance(tool, WebSearchTool): # pragma: no branch
455
+ if tool.user_location:
456
+ return WebSearchOptions(
457
+ search_context_size=tool.search_context_size,
458
+ user_location=WebSearchOptionsUserLocation(
459
+ type='approximate',
460
+ approximate=WebSearchOptionsUserLocationApproximate(**tool.user_location),
461
+ ),
462
+ )
463
+ return WebSearchOptions(search_context_size=tool.search_context_size)
464
+ elif isinstance(tool, CodeExecutionTool): # pragma: no branch
465
+ raise UserError('`CodeExecutionTool` is not supported by OpenAI')
466
+
447
467
  async def _map_messages(self, messages: list[ModelMessage]) -> list[chat.ChatCompletionMessageParam]:
448
468
  """Just maps a `pydantic_ai.Message` to a `openai.types.ChatCompletionMessageParam`."""
449
469
  openai_messages: list[chat.ChatCompletionMessageParam] = []
@@ -464,6 +484,9 @@ class OpenAIModel(Model):
464
484
  pass
465
485
  elif isinstance(item, ToolCallPart):
466
486
  tool_calls.append(self._map_tool_call(item))
487
+ # OpenAI doesn't return built-in tool calls
488
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
489
+ pass
467
490
  else:
468
491
  assert_never(item)
469
492
  message_param = chat.ChatCompletionAssistantMessageParam(role='assistant')
@@ -753,7 +776,7 @@ class OpenAIResponsesModel(Model):
753
776
  model_request_parameters: ModelRequestParameters,
754
777
  ) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]:
755
778
  tools = self._get_tools(model_request_parameters)
756
- tools = list(model_settings.get('openai_builtin_tools', [])) + tools
779
+ tools = self._get_builtin_tools(model_request_parameters) + tools
757
780
 
758
781
  if not tools:
759
782
  tool_choice: Literal['none', 'required', 'auto'] | None = None
@@ -841,6 +864,22 @@ class OpenAIResponsesModel(Model):
841
864
  tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
842
865
  return tools
843
866
 
867
+ def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.ToolParam]:
868
+ tools: list[responses.ToolParam] = []
869
+ for tool in model_request_parameters.builtin_tools:
870
+ if isinstance(tool, WebSearchTool):
871
+ web_search_tool = responses.WebSearchToolParam(
872
+ type='web_search_preview', search_context_size=tool.search_context_size
873
+ )
874
+ if tool.user_location:
875
+ web_search_tool['user_location'] = responses.web_search_tool_param.UserLocation(
876
+ type='approximate', **tool.user_location
877
+ )
878
+ tools.append(web_search_tool)
879
+ elif isinstance(tool, CodeExecutionTool): # pragma: no branch
880
+ tools.append({'type': 'code_interpreter', 'container': {'type': 'auto'}})
881
+ return tools
882
+
844
883
  def _map_tool_definition(self, f: ToolDefinition) -> responses.FunctionToolParam:
845
884
  return {
846
885
  'name': f.name,
@@ -895,6 +934,9 @@ class OpenAIResponsesModel(Model):
895
934
  openai_messages.append(responses.EasyInputMessageParam(role='assistant', content=item.content))
896
935
  elif isinstance(item, ToolCallPart):
897
936
  openai_messages.append(self._map_tool_call(item))
937
+ # OpenAI doesn't return built-in tool calls
938
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)):
939
+ pass
898
940
  elif isinstance(item, ThinkingPart):
899
941
  # NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
900
942
  # please open an issue. The below code is the code to send thinking to the provider.
@@ -1071,6 +1113,7 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1071
1113
 
1072
1114
  async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # noqa: C901
1073
1115
  async for chunk in self._response:
1116
+ # NOTE: You can inspect the builtin tools used checking the `ResponseCompletedEvent`.
1074
1117
  if isinstance(chunk, responses.ResponseCompletedEvent):
1075
1118
  self._usage += _map_usage(chunk.response)
1076
1119
 
@@ -1122,6 +1165,8 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1122
1165
  )
1123
1166
  elif isinstance(chunk.item, responses.ResponseOutputMessage):
1124
1167
  pass
1168
+ elif isinstance(chunk.item, responses.ResponseFunctionWebSearch):
1169
+ pass
1125
1170
  else:
1126
1171
  warnings.warn( # pragma: no cover
1127
1172
  f'Handling of this item type is not yet implemented. Please report on our GitHub: {chunk}',
@@ -1148,6 +1193,10 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1148
1193
  signature=chunk.item_id,
1149
1194
  )
1150
1195
 
1196
+ # TODO(Marcelo): We should support annotations in the future.
1197
+ elif isinstance(chunk, responses.ResponseOutputTextAnnotationAddedEvent):
1198
+ pass # there's nothing we need to do here
1199
+
1151
1200
  elif isinstance(chunk, responses.ResponseTextDeltaEvent):
1152
1201
  maybe_event = self._parts_manager.handle_text_delta(
1153
1202
  vendor_part_id=chunk.content_index, content=chunk.delta
@@ -1158,6 +1207,18 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1158
1207
  elif isinstance(chunk, responses.ResponseTextDoneEvent):
1159
1208
  pass # there's nothing we need to do here
1160
1209
 
1210
+ elif isinstance(chunk, responses.ResponseWebSearchCallInProgressEvent):
1211
+ pass # there's nothing we need to do here
1212
+
1213
+ elif isinstance(chunk, responses.ResponseWebSearchCallSearchingEvent):
1214
+ pass # there's nothing we need to do here
1215
+
1216
+ elif isinstance(chunk, responses.ResponseWebSearchCallCompletedEvent):
1217
+ pass # there's nothing we need to do here
1218
+
1219
+ elif isinstance(chunk, responses.ResponseAudioDeltaEvent): # pragma: lax no cover
1220
+ pass # there's nothing we need to do here
1221
+
1161
1222
  else: # pragma: no cover
1162
1223
  warnings.warn(
1163
1224
  f'Handling of this event type is not yet implemented. Please report on our GitHub: {chunk}',
@@ -12,7 +12,10 @@ import pydantic_core
12
12
  from typing_extensions import assert_never
13
13
 
14
14
  from .. import _utils
15
+ from ..exceptions import UserError
15
16
  from ..messages import (
17
+ BuiltinToolCallPart,
18
+ BuiltinToolReturnPart,
16
19
  ModelMessage,
17
20
  ModelRequest,
18
21
  ModelResponse,
@@ -179,6 +182,9 @@ class TestModel(Model):
179
182
  model_settings: ModelSettings | None,
180
183
  model_request_parameters: ModelRequestParameters,
181
184
  ) -> ModelResponse:
185
+ if model_request_parameters.builtin_tools:
186
+ raise UserError('TestModel does not support built-in tools')
187
+
182
188
  tool_calls = self._get_tool_calls(model_request_parameters)
183
189
  output_wrapper = self._get_output(model_request_parameters)
184
190
  output_tools = model_request_parameters.output_tools
@@ -283,6 +289,9 @@ class TestStreamedResponse(StreamedResponse):
283
289
  yield self._parts_manager.handle_tool_call_part(
284
290
  vendor_part_id=i, tool_name=part.tool_name, args=part.args, tool_call_id=part.tool_call_id
285
291
  )
292
+ elif isinstance(part, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
293
+ # NOTE: These parts are not generated by TestModel, but we need to handle them for type checking
294
+ assert False, f'Unexpected part type in TestModel: {type(part).__name__}'
286
295
  elif isinstance(part, ThinkingPart): # pragma: no cover
287
296
  # NOTE: There's no way to reach this part of the code, since we don't generate ThinkingPart on TestModel.
288
297
  assert False, "This should be unreachable — we don't generate ThinkingPart on TestModel."
@@ -7,7 +7,15 @@ from typing import Callable, Union
7
7
  from typing_extensions import Self
8
8
 
9
9
  from ..output import StructuredOutputMode
10
- from ._json_schema import JsonSchemaTransformer
10
+ from ._json_schema import InlineDefsJsonSchemaTransformer, JsonSchemaTransformer
11
+
12
+ __all__ = [
13
+ 'ModelProfile',
14
+ 'ModelProfileSpec',
15
+ 'DEFAULT_PROFILE',
16
+ 'InlineDefsJsonSchemaTransformer',
17
+ 'JsonSchemaTransformer',
18
+ ]
11
19
 
12
20
 
13
21
  @dataclass
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- from . import ModelProfile
4
- from ._json_schema import InlineDefsJsonSchemaTransformer
3
+ from . import InlineDefsJsonSchemaTransformer, ModelProfile
5
4
 
6
5
 
7
6
  def amazon_model_profile(model_name: str) -> ModelProfile | None: