pydantic-ai-slim 0.6.1__py3-none-any.whl → 0.7.0__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.

Potentially problematic release.


This version of pydantic-ai-slim might be problematic. Click here for more details.

Files changed (63) hide show
  1. pydantic_ai/__init__.py +5 -0
  2. pydantic_ai/_a2a.py +6 -4
  3. pydantic_ai/_agent_graph.py +32 -32
  4. pydantic_ai/_cli.py +3 -3
  5. pydantic_ai/_output.py +8 -0
  6. pydantic_ai/_tool_manager.py +3 -0
  7. pydantic_ai/_utils.py +7 -1
  8. pydantic_ai/ag_ui.py +25 -14
  9. pydantic_ai/{agent.py → agent/__init__.py} +217 -1026
  10. pydantic_ai/agent/abstract.py +942 -0
  11. pydantic_ai/agent/wrapper.py +227 -0
  12. pydantic_ai/builtin_tools.py +105 -0
  13. pydantic_ai/direct.py +9 -9
  14. pydantic_ai/durable_exec/__init__.py +0 -0
  15. pydantic_ai/durable_exec/temporal/__init__.py +83 -0
  16. pydantic_ai/durable_exec/temporal/_agent.py +699 -0
  17. pydantic_ai/durable_exec/temporal/_function_toolset.py +92 -0
  18. pydantic_ai/durable_exec/temporal/_logfire.py +48 -0
  19. pydantic_ai/durable_exec/temporal/_mcp_server.py +145 -0
  20. pydantic_ai/durable_exec/temporal/_model.py +168 -0
  21. pydantic_ai/durable_exec/temporal/_run_context.py +50 -0
  22. pydantic_ai/durable_exec/temporal/_toolset.py +77 -0
  23. pydantic_ai/ext/aci.py +10 -9
  24. pydantic_ai/ext/langchain.py +4 -2
  25. pydantic_ai/mcp.py +203 -75
  26. pydantic_ai/messages.py +75 -13
  27. pydantic_ai/models/__init__.py +66 -8
  28. pydantic_ai/models/anthropic.py +135 -18
  29. pydantic_ai/models/bedrock.py +16 -5
  30. pydantic_ai/models/cohere.py +11 -4
  31. pydantic_ai/models/fallback.py +4 -2
  32. pydantic_ai/models/function.py +18 -4
  33. pydantic_ai/models/gemini.py +20 -9
  34. pydantic_ai/models/google.py +53 -15
  35. pydantic_ai/models/groq.py +47 -11
  36. pydantic_ai/models/huggingface.py +26 -11
  37. pydantic_ai/models/instrumented.py +3 -1
  38. pydantic_ai/models/mcp_sampling.py +3 -1
  39. pydantic_ai/models/mistral.py +27 -17
  40. pydantic_ai/models/openai.py +97 -33
  41. pydantic_ai/models/test.py +12 -0
  42. pydantic_ai/models/wrapper.py +6 -2
  43. pydantic_ai/profiles/groq.py +23 -0
  44. pydantic_ai/profiles/openai.py +1 -1
  45. pydantic_ai/providers/google.py +7 -7
  46. pydantic_ai/providers/groq.py +2 -0
  47. pydantic_ai/result.py +21 -55
  48. pydantic_ai/run.py +357 -0
  49. pydantic_ai/tools.py +0 -1
  50. pydantic_ai/toolsets/__init__.py +2 -0
  51. pydantic_ai/toolsets/_dynamic.py +87 -0
  52. pydantic_ai/toolsets/abstract.py +23 -3
  53. pydantic_ai/toolsets/combined.py +19 -4
  54. pydantic_ai/toolsets/deferred.py +10 -2
  55. pydantic_ai/toolsets/function.py +23 -8
  56. pydantic_ai/toolsets/prefixed.py +4 -0
  57. pydantic_ai/toolsets/wrapper.py +14 -1
  58. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/METADATA +7 -5
  59. pydantic_ai_slim-0.7.0.dist-info/RECORD +115 -0
  60. pydantic_ai_slim-0.6.1.dist-info/RECORD +0 -100
  61. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/WHEEL +0 -0
  62. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/entry_points.txt +0 -0
  63. {pydantic_ai_slim-0.6.1.dist-info → pydantic_ai_slim-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -13,19 +13,32 @@ from contextlib import asynccontextmanager, contextmanager
13
13
  from dataclasses import dataclass, field, replace
14
14
  from datetime import datetime
15
15
  from functools import cache, cached_property
16
- from typing import Generic, TypeVar, overload
16
+ from typing import Any, Generic, TypeVar, overload
17
17
 
18
18
  import httpx
19
19
  from typing_extensions import Literal, TypeAliasType, TypedDict
20
20
 
21
- from pydantic_ai.profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec
22
-
23
21
  from .. import _utils
24
22
  from .._output import OutputObjectDefinition
25
23
  from .._parts_manager import ModelResponsePartsManager
24
+ from .._run_context import RunContext
25
+ from ..builtin_tools import AbstractBuiltinTool
26
26
  from ..exceptions import UserError
27
- from ..messages import FileUrl, ModelMessage, ModelRequest, ModelResponse, ModelResponseStreamEvent, VideoUrl
27
+ from ..messages import (
28
+ AgentStreamEvent,
29
+ FileUrl,
30
+ FinalResultEvent,
31
+ ModelMessage,
32
+ ModelRequest,
33
+ ModelResponse,
34
+ ModelResponseStreamEvent,
35
+ PartStartEvent,
36
+ TextPart,
37
+ ToolCallPart,
38
+ VideoUrl,
39
+ )
28
40
  from ..output import OutputMode
41
+ from ..profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec
29
42
  from ..profiles._json_schema import JsonSchemaTransformer
30
43
  from ..settings import ModelSettings
31
44
  from ..tools import ToolDefinition
@@ -336,12 +349,17 @@ class ModelRequestParameters:
336
349
  """Configuration for an agent's request to a model, specifically related to tools and output handling."""
337
350
 
338
351
  function_tools: list[ToolDefinition] = field(default_factory=list)
352
+ builtin_tools: list[AbstractBuiltinTool] = field(default_factory=list)
339
353
 
340
354
  output_mode: OutputMode = 'text'
341
355
  output_object: OutputObjectDefinition | None = None
342
356
  output_tools: list[ToolDefinition] = field(default_factory=list)
343
357
  allow_text_output: bool = True
344
358
 
359
+ @cached_property
360
+ def tool_defs(self) -> dict[str, ToolDefinition]:
361
+ return {tool_def.name: tool_def for tool_def in [*self.function_tools, *self.output_tools]}
362
+
345
363
  __repr__ = _utils.dataclasses_no_defaults_repr
346
364
 
347
365
 
@@ -387,6 +405,7 @@ class Model(ABC):
387
405
  messages: list[ModelMessage],
388
406
  model_settings: ModelSettings | None,
389
407
  model_request_parameters: ModelRequestParameters,
408
+ run_context: RunContext[Any] | None = None,
390
409
  ) -> AsyncIterator[StreamedResponse]:
391
410
  """Make a request to the model and return a streaming response."""
392
411
  # This method is not required, but you need to implement it if you want to support streamed responses
@@ -499,14 +518,40 @@ class Model(ABC):
499
518
  class StreamedResponse(ABC):
500
519
  """Streamed response from an LLM when calling a tool."""
501
520
 
521
+ model_request_parameters: ModelRequestParameters
522
+ final_result_event: FinalResultEvent | None = field(default=None, init=False)
523
+
502
524
  _parts_manager: ModelResponsePartsManager = field(default_factory=ModelResponsePartsManager, init=False)
503
- _event_iterator: AsyncIterator[ModelResponseStreamEvent] | None = field(default=None, init=False)
525
+ _event_iterator: AsyncIterator[AgentStreamEvent] | None = field(default=None, init=False)
504
526
  _usage: Usage = field(default_factory=Usage, init=False)
505
527
 
506
- def __aiter__(self) -> AsyncIterator[ModelResponseStreamEvent]:
507
- """Stream the response as an async iterable of [`ModelResponseStreamEvent`][pydantic_ai.messages.ModelResponseStreamEvent]s."""
528
+ def __aiter__(self) -> AsyncIterator[AgentStreamEvent]:
529
+ """Stream the response as an async iterable of [`AgentStreamEvent`][pydantic_ai.messages.AgentStreamEvent]s.
530
+
531
+ This proxies the `_event_iterator()` and emits all events, while also checking for matches
532
+ on the result schema and emitting a [`FinalResultEvent`][pydantic_ai.messages.FinalResultEvent] if/when the
533
+ first match is found.
534
+ """
508
535
  if self._event_iterator is None:
509
- self._event_iterator = self._get_event_iterator()
536
+
537
+ async def iterator_with_final_event(
538
+ iterator: AsyncIterator[ModelResponseStreamEvent],
539
+ ) -> AsyncIterator[AgentStreamEvent]:
540
+ async for event in iterator:
541
+ yield event
542
+ if (
543
+ final_result_event := _get_final_result_event(event, self.model_request_parameters)
544
+ ) is not None:
545
+ self.final_result_event = final_result_event
546
+ yield final_result_event
547
+ break
548
+
549
+ # If we broke out of the above loop, we need to yield the rest of the events
550
+ # If we didn't, this will just be a no-op
551
+ async for event in iterator:
552
+ yield event
553
+
554
+ self._event_iterator = iterator_with_final_event(self._get_event_iterator())
510
555
  return self._event_iterator
511
556
 
512
557
  @abstractmethod
@@ -808,3 +853,16 @@ def _customize_output_object(transformer: type[JsonSchemaTransformer], o: Output
808
853
  json_schema=json_schema,
809
854
  strict=schema_transformer.is_strict_compatible if o.strict is None else o.strict,
810
855
  )
856
+
857
+
858
+ def _get_final_result_event(e: ModelResponseStreamEvent, params: ModelRequestParameters) -> FinalResultEvent | None:
859
+ """Return an appropriate FinalResultEvent if `e` corresponds to a part that will produce a final result."""
860
+ if isinstance(e, PartStartEvent):
861
+ new_part = e.part
862
+ if isinstance(new_part, TextPart) and params.allow_text_output: # pragma: no branch
863
+ return FinalResultEvent(tool_name=None, tool_call_id=None)
864
+ elif isinstance(new_part, ToolCallPart) and (tool_def := params.tool_defs.get(new_part.tool_name)):
865
+ if tool_def.kind == 'output':
866
+ return FinalResultEvent(tool_name=new_part.tool_name, tool_call_id=new_part.tool_call_id)
867
+ elif tool_def.kind == 'deferred':
868
+ return FinalResultEvent(tool_name=None, tool_call_id=None)
@@ -8,12 +8,26 @@ from dataclasses import dataclass, field
8
8
  from datetime import datetime, timezone
9
9
  from typing import Any, Literal, Union, cast, overload
10
10
 
11
+ from anthropic.types.beta import (
12
+ BetaCitationsDelta,
13
+ BetaCodeExecutionToolResultBlock,
14
+ BetaCodeExecutionToolResultBlockParam,
15
+ BetaInputJSONDelta,
16
+ BetaServerToolUseBlockParam,
17
+ BetaWebSearchToolResultBlockParam,
18
+ )
11
19
  from typing_extensions import assert_never
12
20
 
21
+ from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool
22
+
13
23
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
24
+ from .._run_context import RunContext
14
25
  from .._utils import guard_tool_call_id as _guard_tool_call_id
26
+ from ..exceptions import UserError
15
27
  from ..messages import (
16
28
  BinaryContent,
29
+ BuiltinToolCallPart,
30
+ BuiltinToolReturnPart,
17
31
  DocumentUrl,
18
32
  ImageUrl,
19
33
  ModelMessage,
@@ -33,13 +47,21 @@ from ..profiles import ModelProfileSpec
33
47
  from ..providers import Provider, infer_provider
34
48
  from ..settings import ModelSettings
35
49
  from ..tools import ToolDefinition
36
- from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent
50
+ from . import (
51
+ Model,
52
+ ModelRequestParameters,
53
+ StreamedResponse,
54
+ check_allow_model_requests,
55
+ download_item,
56
+ get_user_agent,
57
+ )
37
58
 
38
59
  try:
39
60
  from anthropic import NOT_GIVEN, APIStatusError, AsyncAnthropic, AsyncStream
40
61
  from anthropic.types.beta import (
41
62
  BetaBase64PDFBlockParam,
42
63
  BetaBase64PDFSourceParam,
64
+ BetaCodeExecutionTool20250522Param,
43
65
  BetaContentBlock,
44
66
  BetaContentBlockParam,
45
67
  BetaImageBlockParam,
@@ -55,6 +77,7 @@ try:
55
77
  BetaRawMessageStopEvent,
56
78
  BetaRawMessageStreamEvent,
57
79
  BetaRedactedThinkingBlock,
80
+ BetaServerToolUseBlock,
58
81
  BetaSignatureDelta,
59
82
  BetaTextBlock,
60
83
  BetaTextBlockParam,
@@ -66,9 +89,13 @@ try:
66
89
  BetaToolChoiceParam,
67
90
  BetaToolParam,
68
91
  BetaToolResultBlockParam,
92
+ BetaToolUnionParam,
69
93
  BetaToolUseBlock,
70
94
  BetaToolUseBlockParam,
95
+ BetaWebSearchTool20250305Param,
96
+ BetaWebSearchToolResultBlock,
71
97
  )
98
+ from anthropic.types.beta.beta_web_search_tool_20250305_param import UserLocation
72
99
  from anthropic.types.model_param import ModelParam
73
100
 
74
101
  except ImportError as _import_error:
@@ -171,13 +198,14 @@ class AnthropicModel(Model):
171
198
  messages: list[ModelMessage],
172
199
  model_settings: ModelSettings | None,
173
200
  model_request_parameters: ModelRequestParameters,
201
+ run_context: RunContext[Any] | None = None,
174
202
  ) -> AsyncIterator[StreamedResponse]:
175
203
  check_allow_model_requests()
176
204
  response = await self._messages_create(
177
205
  messages, True, cast(AnthropicModelSettings, model_settings or {}), model_request_parameters
178
206
  )
179
207
  async with response:
180
- yield await self._process_streamed_response(response)
208
+ yield await self._process_streamed_response(response, model_request_parameters)
181
209
 
182
210
  @property
183
211
  def model_name(self) -> AnthropicModelName:
@@ -218,6 +246,7 @@ class AnthropicModel(Model):
218
246
  ) -> BetaMessage | AsyncStream[BetaRawMessageStreamEvent]:
219
247
  # standalone function to make it easier to override
220
248
  tools = self._get_tools(model_request_parameters)
249
+ tools += self._get_builtin_tools(model_request_parameters)
221
250
  tool_choice: BetaToolChoiceParam | None
222
251
 
223
252
  if not tools:
@@ -236,6 +265,7 @@ class AnthropicModel(Model):
236
265
  try:
237
266
  extra_headers = model_settings.get('extra_headers', {})
238
267
  extra_headers.setdefault('User-Agent', get_user_agent())
268
+ extra_headers.setdefault('anthropic-beta', 'code-execution-2025-05-22')
239
269
  return await self.client.beta.messages.create(
240
270
  max_tokens=model_settings.get('max_tokens', 4096),
241
271
  system=system_prompt or NOT_GIVEN,
@@ -264,6 +294,24 @@ class AnthropicModel(Model):
264
294
  for item in response.content:
265
295
  if isinstance(item, BetaTextBlock):
266
296
  items.append(TextPart(content=item.text))
297
+ elif isinstance(item, (BetaWebSearchToolResultBlock, BetaCodeExecutionToolResultBlock)):
298
+ items.append(
299
+ BuiltinToolReturnPart(
300
+ provider_name='anthropic',
301
+ tool_name=item.type,
302
+ content=item.content,
303
+ tool_call_id=item.tool_use_id,
304
+ )
305
+ )
306
+ elif isinstance(item, BetaServerToolUseBlock):
307
+ items.append(
308
+ BuiltinToolCallPart(
309
+ provider_name='anthropic',
310
+ tool_name=item.name,
311
+ args=cast(dict[str, Any], item.input),
312
+ tool_call_id=item.id,
313
+ )
314
+ )
267
315
  elif isinstance(item, BetaRedactedThinkingBlock): # pragma: no cover
268
316
  warnings.warn(
269
317
  'Pydantic AI currently does not handle redacted thinking blocks. '
@@ -284,7 +332,9 @@ class AnthropicModel(Model):
284
332
 
285
333
  return ModelResponse(items, usage=_map_usage(response), model_name=response.model, vendor_id=response.id)
286
334
 
287
- async def _process_streamed_response(self, response: AsyncStream[BetaRawMessageStreamEvent]) -> StreamedResponse:
335
+ async def _process_streamed_response(
336
+ self, response: AsyncStream[BetaRawMessageStreamEvent], model_request_parameters: ModelRequestParameters
337
+ ) -> StreamedResponse:
288
338
  peekable_response = _utils.PeekableAsyncStream(response)
289
339
  first_chunk = await peekable_response.peek()
290
340
  if isinstance(first_chunk, _utils.Unset):
@@ -293,13 +343,35 @@ class AnthropicModel(Model):
293
343
  # Since Anthropic doesn't provide a timestamp in the message, we'll use the current time
294
344
  timestamp = datetime.now(tz=timezone.utc)
295
345
  return AnthropicStreamedResponse(
296
- _model_name=self._model_name, _response=peekable_response, _timestamp=timestamp
346
+ model_request_parameters=model_request_parameters,
347
+ _model_name=self._model_name,
348
+ _response=peekable_response,
349
+ _timestamp=timestamp,
297
350
  )
298
351
 
299
352
  def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[BetaToolParam]:
300
- tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
301
- if model_request_parameters.output_tools:
302
- tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
353
+ return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()]
354
+
355
+ def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[BetaToolUnionParam]:
356
+ tools: list[BetaToolUnionParam] = []
357
+ for tool in model_request_parameters.builtin_tools:
358
+ if isinstance(tool, WebSearchTool):
359
+ user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None
360
+ tools.append(
361
+ BetaWebSearchTool20250305Param(
362
+ name='web_search',
363
+ type='web_search_20250305',
364
+ allowed_domains=tool.allowed_domains,
365
+ blocked_domains=tool.blocked_domains,
366
+ user_location=user_location,
367
+ )
368
+ )
369
+ elif isinstance(tool, CodeExecutionTool): # pragma: no branch
370
+ tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522'))
371
+ else: # pragma: no cover
372
+ raise UserError(
373
+ f'`{tool.__class__.__name__}` is not supported by `AnthropicModel`. If it should be, please file an issue.'
374
+ )
303
375
  return tools
304
376
 
305
377
  async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[BetaMessageParam]]: # noqa: C901
@@ -338,11 +410,26 @@ class AnthropicModel(Model):
338
410
  if len(user_content_params) > 0:
339
411
  anthropic_messages.append(BetaMessageParam(role='user', content=user_content_params))
340
412
  elif isinstance(m, ModelResponse):
341
- assistant_content_params: list[BetaTextBlockParam | BetaToolUseBlockParam | BetaThinkingBlockParam] = []
413
+ assistant_content_params: list[
414
+ BetaTextBlockParam
415
+ | BetaToolUseBlockParam
416
+ | BetaServerToolUseBlockParam
417
+ | BetaWebSearchToolResultBlockParam
418
+ | BetaCodeExecutionToolResultBlockParam
419
+ | BetaThinkingBlockParam
420
+ ] = []
342
421
  for response_part in m.parts:
343
422
  if isinstance(response_part, TextPart):
344
- if response_part.content: # Only add non-empty text
423
+ if response_part.content:
345
424
  assistant_content_params.append(BetaTextBlockParam(text=response_part.content, type='text'))
425
+ elif isinstance(response_part, ToolCallPart):
426
+ tool_use_block_param = BetaToolUseBlockParam(
427
+ id=_guard_tool_call_id(t=response_part),
428
+ type='tool_use',
429
+ name=response_part.tool_name,
430
+ input=response_part.args_as_dict(),
431
+ )
432
+ assistant_content_params.append(tool_use_block_param)
346
433
  elif isinstance(response_part, ThinkingPart):
347
434
  # NOTE: We only send thinking part back for Anthropic, otherwise they raise an error.
348
435
  if response_part.signature is not None: # pragma: no branch
@@ -351,14 +438,31 @@ class AnthropicModel(Model):
351
438
  thinking=response_part.content, signature=response_part.signature, type='thinking'
352
439
  )
353
440
  )
441
+ elif isinstance(response_part, BuiltinToolCallPart):
442
+ if response_part.provider_name == 'anthropic':
443
+ server_tool_use_block_param = BetaServerToolUseBlockParam(
444
+ id=_guard_tool_call_id(t=response_part),
445
+ type='server_tool_use',
446
+ name=cast(Literal['web_search', 'code_execution'], response_part.tool_name),
447
+ input=response_part.args_as_dict(),
448
+ )
449
+ assistant_content_params.append(server_tool_use_block_param)
450
+ elif isinstance(response_part, BuiltinToolReturnPart):
451
+ if response_part.provider_name == 'anthropic':
452
+ tool_use_id = _guard_tool_call_id(t=response_part)
453
+ if response_part.tool_name == 'web_search_tool_result':
454
+ server_tool_result_block_param = BetaWebSearchToolResultBlockParam(
455
+ tool_use_id=tool_use_id, type=response_part.tool_name, content=response_part.content
456
+ )
457
+ elif response_part.tool_name == 'code_execution_tool_result':
458
+ server_tool_result_block_param = BetaCodeExecutionToolResultBlockParam(
459
+ tool_use_id=tool_use_id, type=response_part.tool_name, content=response_part.content
460
+ )
461
+ else:
462
+ raise ValueError(f'Unsupported tool name: {response_part.tool_name}')
463
+ assistant_content_params.append(server_tool_result_block_param)
354
464
  else:
355
- tool_use_block_param = BetaToolUseBlockParam(
356
- id=_guard_tool_call_id(t=response_part),
357
- type='tool_use',
358
- name=response_part.tool_name,
359
- input=response_part.args_as_dict(),
360
- )
361
- assistant_content_params.append(tool_use_block_param)
465
+ assert_never(response_part)
362
466
  if len(assistant_content_params) > 0:
363
467
  anthropic_messages.append(BetaMessageParam(role='assistant', content=assistant_content_params))
364
468
  else:
@@ -476,7 +580,10 @@ class AnthropicStreamedResponse(StreamedResponse):
476
580
  async for event in self._response:
477
581
  self._usage += _map_usage(event)
478
582
 
479
- if isinstance(event, BetaRawContentBlockStartEvent):
583
+ if isinstance(event, BetaRawMessageStartEvent):
584
+ pass
585
+
586
+ elif isinstance(event, BetaRawContentBlockStartEvent):
480
587
  current_block = event.content_block
481
588
  if isinstance(current_block, BetaTextBlock) and current_block.text:
482
589
  maybe_event = self._parts_manager.handle_text_delta(
@@ -499,6 +606,8 @@ class AnthropicStreamedResponse(StreamedResponse):
499
606
  )
500
607
  if maybe_event is not None: # pragma: no branch
501
608
  yield maybe_event
609
+ elif isinstance(current_block, BetaServerToolUseBlock):
610
+ pass
502
611
 
503
612
  elif isinstance(event, BetaRawContentBlockDeltaEvent):
504
613
  if isinstance(event.delta, BetaTextDelta):
@@ -528,8 +637,16 @@ class AnthropicStreamedResponse(StreamedResponse):
528
637
  )
529
638
  if maybe_event is not None: # pragma: no branch
530
639
  yield maybe_event
640
+ elif isinstance(event.delta, BetaInputJSONDelta):
641
+ pass
642
+ # TODO(Marcelo): We need to handle citations.
643
+ elif isinstance(event.delta, BetaCitationsDelta):
644
+ pass
645
+
646
+ elif isinstance(event, BetaRawMessageDeltaEvent):
647
+ pass
531
648
 
532
- elif isinstance(event, (BetaRawContentBlockStopEvent, BetaRawMessageStopEvent)):
649
+ elif isinstance(event, (BetaRawContentBlockStopEvent, BetaRawMessageStopEvent)): # pragma: no branch
533
650
  current_block = None
534
651
 
535
652
  @property
@@ -15,9 +15,13 @@ 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._run_context import RunContext
19
+ from pydantic_ai.exceptions import UserError
18
20
  from pydantic_ai.messages import (
19
21
  AudioUrl,
20
22
  BinaryContent,
23
+ BuiltinToolCallPart,
24
+ BuiltinToolReturnPart,
21
25
  DocumentUrl,
22
26
  ImageUrl,
23
27
  ModelMessage,
@@ -227,10 +231,7 @@ class BedrockConverseModel(Model):
227
231
  super().__init__(settings=settings, profile=profile or provider.model_profile)
228
232
 
229
233
  def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ToolTypeDef]:
230
- tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
231
- if model_request_parameters.output_tools:
232
- tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
233
- return tools
234
+ return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()]
234
235
 
235
236
  @staticmethod
236
237
  def _map_tool_definition(f: ToolDefinition) -> ToolTypeDef:
@@ -266,10 +267,15 @@ class BedrockConverseModel(Model):
266
267
  messages: list[ModelMessage],
267
268
  model_settings: ModelSettings | None,
268
269
  model_request_parameters: ModelRequestParameters,
270
+ run_context: RunContext[Any] | None = None,
269
271
  ) -> AsyncIterator[StreamedResponse]:
270
272
  settings = cast(BedrockModelSettings, model_settings or {})
271
273
  response = await self._messages_create(messages, True, settings, model_request_parameters)
272
- yield BedrockStreamedResponse(_model_name=self.model_name, _event_stream=response)
274
+ yield BedrockStreamedResponse(
275
+ model_request_parameters=model_request_parameters,
276
+ _model_name=self.model_name,
277
+ _event_stream=response,
278
+ )
273
279
 
274
280
  async def _process_response(self, response: ConverseResponseTypeDef) -> ModelResponse:
275
281
  items: list[ModelResponsePart] = []
@@ -342,6 +348,9 @@ class BedrockConverseModel(Model):
342
348
  if tool_config:
343
349
  params['toolConfig'] = tool_config
344
350
 
351
+ if model_request_parameters.builtin_tools:
352
+ raise UserError('Bedrock does not support built-in tools')
353
+
345
354
  # Bedrock supports a set of specific extra parameters
346
355
  if model_settings:
347
356
  if guardrail_config := model_settings.get('bedrock_guardrail_config', None):
@@ -478,6 +487,8 @@ class BedrockConverseModel(Model):
478
487
  else:
479
488
  # NOTE: We don't pass the thinking part to Bedrock for models other than Claude since it raises an error.
480
489
  pass
490
+ elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)):
491
+ pass
481
492
  else:
482
493
  assert isinstance(item, ToolCallPart)
483
494
  content.append(self._map_tool_call(item))
@@ -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')
@@ -238,10 +248,7 @@ class CohereModel(Model):
238
248
  return cohere_messages
239
249
 
240
250
  def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ToolV2]:
241
- tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
242
- if model_request_parameters.output_tools:
243
- tools += [self._map_tool_definition(r) for r in model_request_parameters.output_tools]
244
- return tools
251
+ return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()]
245
252
 
246
253
  @staticmethod
247
254
  def _map_tool_call(t: ToolCallPart) -> ToolCallV2:
@@ -3,10 +3,11 @@ from __future__ import annotations as _annotations
3
3
  from collections.abc import AsyncIterator
4
4
  from contextlib import AsyncExitStack, asynccontextmanager, suppress
5
5
  from dataclasses import dataclass, field
6
- from typing import TYPE_CHECKING, Callable
6
+ from typing import TYPE_CHECKING, Any, Callable
7
7
 
8
8
  from opentelemetry.trace import get_current_span
9
9
 
10
+ from pydantic_ai._run_context import RunContext
10
11
  from pydantic_ai.models.instrumented import InstrumentedModel
11
12
 
12
13
  from ..exceptions import FallbackExceptionGroup, ModelHTTPError
@@ -83,6 +84,7 @@ class FallbackModel(Model):
83
84
  messages: list[ModelMessage],
84
85
  model_settings: ModelSettings | None,
85
86
  model_request_parameters: ModelRequestParameters,
87
+ run_context: RunContext[Any] | None = None,
86
88
  ) -> AsyncIterator[StreamedResponse]:
87
89
  """Try each model in sequence until one succeeds."""
88
90
  exceptions: list[Exception] = []
@@ -92,7 +94,7 @@ class FallbackModel(Model):
92
94
  async with AsyncExitStack() as stack:
93
95
  try:
94
96
  response = await stack.enter_async_context(
95
- model.request_stream(messages, model_settings, customized_model_request_parameters)
97
+ model.request_stream(messages, model_settings, customized_model_request_parameters, run_context)
96
98
  )
97
99
  except Exception as exc:
98
100
  if self._fallback_on(exc):
@@ -7,16 +7,17 @@ from contextlib import asynccontextmanager
7
7
  from dataclasses import dataclass, field
8
8
  from datetime import datetime
9
9
  from itertools import chain
10
- from typing import Callable, Union
10
+ from typing import Any, Callable, Union
11
11
 
12
12
  from typing_extensions import TypeAlias, assert_never, overload
13
13
 
14
- from pydantic_ai.profiles import ModelProfileSpec
15
-
16
14
  from .. import _utils, usage
15
+ from .._run_context import RunContext
17
16
  from .._utils import PeekableAsyncStream
18
17
  from ..messages import (
19
18
  BinaryContent,
19
+ BuiltinToolCallPart,
20
+ BuiltinToolReturnPart,
20
21
  ModelMessage,
21
22
  ModelRequest,
22
23
  ModelResponse,
@@ -30,6 +31,7 @@ from ..messages import (
30
31
  UserContent,
31
32
  UserPromptPart,
32
33
  )
34
+ from ..profiles import ModelProfileSpec
33
35
  from ..settings import ModelSettings
34
36
  from ..tools import ToolDefinition
35
37
  from . import Model, ModelRequestParameters, StreamedResponse
@@ -145,6 +147,7 @@ class FunctionModel(Model):
145
147
  messages: list[ModelMessage],
146
148
  model_settings: ModelSettings | None,
147
149
  model_request_parameters: ModelRequestParameters,
150
+ run_context: RunContext[Any] | None = None,
148
151
  ) -> AsyncIterator[StreamedResponse]:
149
152
  agent_info = AgentInfo(
150
153
  model_request_parameters.function_tools,
@@ -163,7 +166,11 @@ class FunctionModel(Model):
163
166
  if isinstance(first, _utils.Unset):
164
167
  raise ValueError('Stream function must return at least one item')
165
168
 
166
- yield FunctionStreamedResponse(_model_name=self._model_name, _iter=response_stream)
169
+ yield FunctionStreamedResponse(
170
+ model_request_parameters=model_request_parameters,
171
+ _model_name=self._model_name,
172
+ _iter=response_stream,
173
+ )
167
174
 
168
175
  @property
169
176
  def model_name(self) -> str:
@@ -331,6 +338,13 @@ def _estimate_usage(messages: Iterable[ModelMessage]) -> usage.Usage:
331
338
  response_tokens += _estimate_string_tokens(part.content)
332
339
  elif isinstance(part, ToolCallPart):
333
340
  response_tokens += 1 + _estimate_string_tokens(part.args_as_json_str())
341
+ # TODO(Marcelo): We need to add coverage here.
342
+ elif isinstance(part, BuiltinToolCallPart): # pragma: no cover
343
+ call = part
344
+ response_tokens += 1 + _estimate_string_tokens(call.args_as_json_str())
345
+ # TODO(Marcelo): We need to add coverage here.
346
+ elif isinstance(part, BuiltinToolReturnPart): # pragma: no cover
347
+ response_tokens += _estimate_string_tokens(part.model_response_str())
334
348
  else:
335
349
  assert_never(part)
336
350
  else: