pydantic-ai-slim 0.8.1__py3-none-any.whl → 1.0.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 (75) hide show
  1. pydantic_ai/__init__.py +28 -2
  2. pydantic_ai/_a2a.py +1 -1
  3. pydantic_ai/_agent_graph.py +323 -156
  4. pydantic_ai/_function_schema.py +5 -5
  5. pydantic_ai/_griffe.py +2 -1
  6. pydantic_ai/_otel_messages.py +2 -2
  7. pydantic_ai/_output.py +31 -35
  8. pydantic_ai/_parts_manager.py +7 -5
  9. pydantic_ai/_run_context.py +3 -1
  10. pydantic_ai/_system_prompt.py +2 -2
  11. pydantic_ai/_tool_manager.py +32 -28
  12. pydantic_ai/_utils.py +14 -26
  13. pydantic_ai/ag_ui.py +82 -51
  14. pydantic_ai/agent/__init__.py +70 -9
  15. pydantic_ai/agent/abstract.py +35 -4
  16. pydantic_ai/agent/wrapper.py +6 -0
  17. pydantic_ai/builtin_tools.py +2 -2
  18. pydantic_ai/common_tools/duckduckgo.py +4 -2
  19. pydantic_ai/durable_exec/temporal/__init__.py +4 -2
  20. pydantic_ai/durable_exec/temporal/_agent.py +93 -11
  21. pydantic_ai/durable_exec/temporal/_function_toolset.py +53 -6
  22. pydantic_ai/durable_exec/temporal/_logfire.py +1 -1
  23. pydantic_ai/durable_exec/temporal/_mcp_server.py +2 -1
  24. pydantic_ai/durable_exec/temporal/_model.py +2 -2
  25. pydantic_ai/durable_exec/temporal/_run_context.py +2 -1
  26. pydantic_ai/durable_exec/temporal/_toolset.py +2 -1
  27. pydantic_ai/exceptions.py +45 -2
  28. pydantic_ai/format_prompt.py +2 -2
  29. pydantic_ai/mcp.py +15 -27
  30. pydantic_ai/messages.py +149 -42
  31. pydantic_ai/models/__init__.py +6 -4
  32. pydantic_ai/models/anthropic.py +9 -16
  33. pydantic_ai/models/bedrock.py +50 -56
  34. pydantic_ai/models/cohere.py +3 -3
  35. pydantic_ai/models/fallback.py +2 -2
  36. pydantic_ai/models/function.py +25 -23
  37. pydantic_ai/models/gemini.py +12 -13
  38. pydantic_ai/models/google.py +18 -4
  39. pydantic_ai/models/groq.py +126 -38
  40. pydantic_ai/models/huggingface.py +4 -4
  41. pydantic_ai/models/instrumented.py +35 -16
  42. pydantic_ai/models/mcp_sampling.py +3 -1
  43. pydantic_ai/models/mistral.py +6 -6
  44. pydantic_ai/models/openai.py +35 -40
  45. pydantic_ai/models/test.py +24 -4
  46. pydantic_ai/output.py +27 -32
  47. pydantic_ai/profiles/__init__.py +3 -3
  48. pydantic_ai/profiles/groq.py +1 -1
  49. pydantic_ai/profiles/openai.py +25 -4
  50. pydantic_ai/providers/__init__.py +4 -0
  51. pydantic_ai/providers/anthropic.py +2 -3
  52. pydantic_ai/providers/bedrock.py +3 -2
  53. pydantic_ai/providers/google_vertex.py +2 -1
  54. pydantic_ai/providers/groq.py +21 -2
  55. pydantic_ai/providers/litellm.py +134 -0
  56. pydantic_ai/result.py +144 -41
  57. pydantic_ai/retries.py +52 -31
  58. pydantic_ai/run.py +12 -5
  59. pydantic_ai/tools.py +127 -23
  60. pydantic_ai/toolsets/__init__.py +4 -1
  61. pydantic_ai/toolsets/_dynamic.py +4 -4
  62. pydantic_ai/toolsets/abstract.py +18 -2
  63. pydantic_ai/toolsets/approval_required.py +32 -0
  64. pydantic_ai/toolsets/combined.py +7 -12
  65. pydantic_ai/toolsets/{deferred.py → external.py} +11 -5
  66. pydantic_ai/toolsets/filtered.py +1 -1
  67. pydantic_ai/toolsets/function.py +58 -21
  68. pydantic_ai/toolsets/wrapper.py +2 -1
  69. pydantic_ai/usage.py +44 -8
  70. {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0.dist-info}/METADATA +8 -9
  71. pydantic_ai_slim-1.0.0.dist-info/RECORD +121 -0
  72. pydantic_ai_slim-0.8.1.dist-info/RECORD +0 -119
  73. {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0.dist-info}/WHEEL +0 -0
  74. {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0.dist-info}/entry_points.txt +0 -0
  75. {pydantic_ai_slim-0.8.1.dist-info → pydantic_ai_slim-1.0.0.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/messages.py CHANGED
@@ -1,24 +1,22 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import base64
4
+ import hashlib
4
5
  from abc import ABC, abstractmethod
5
6
  from collections.abc import Sequence
6
- from dataclasses import dataclass, field, replace
7
+ from dataclasses import KW_ONLY, dataclass, field, replace
7
8
  from datetime import datetime
8
9
  from mimetypes import guess_type
9
- from typing import TYPE_CHECKING, Annotated, Any, Literal, Union, cast, overload
10
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAlias, cast, overload
10
11
 
11
12
  import pydantic
12
13
  import pydantic_core
13
14
  from genai_prices import calc_price, types as genai_types
14
15
  from opentelemetry._events import Event # pyright: ignore[reportPrivateImportUsage]
15
- from typing_extensions import TypeAlias, deprecated
16
+ from typing_extensions import deprecated
16
17
 
17
18
  from . import _otel_messages, _utils
18
- from ._utils import (
19
- generate_tool_call_id as _generate_tool_call_id,
20
- now_utc as _now_utc,
21
- )
19
+ from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc
22
20
  from .exceptions import UnexpectedModelBehavior
23
21
  from .usage import RequestUsage
24
22
 
@@ -65,6 +63,8 @@ class SystemPromptPart:
65
63
  content: str
66
64
  """The content of the prompt."""
67
65
 
66
+ _: KW_ONLY
67
+
68
68
  timestamp: datetime = field(default_factory=_now_utc)
69
69
  """The timestamp of the prompt."""
70
70
 
@@ -89,6 +89,13 @@ class SystemPromptPart:
89
89
  __repr__ = _utils.dataclasses_no_defaults_repr
90
90
 
91
91
 
92
+ def _multi_modal_content_identifier(identifier: str | bytes) -> str:
93
+ """Generate stable identifier for multi-modal content to help LLM in finding a specific file in tool call responses."""
94
+ if isinstance(identifier, str):
95
+ identifier = identifier.encode('utf-8')
96
+ return hashlib.sha1(identifier).hexdigest()[:6]
97
+
98
+
92
99
  @dataclass(init=False, repr=False)
93
100
  class FileUrl(ABC):
94
101
  """Abstract base class for any URL-based file."""
@@ -96,6 +103,8 @@ class FileUrl(ABC):
96
103
  url: str
97
104
  """The URL of the file."""
98
105
 
106
+ _: KW_ONLY
107
+
99
108
  force_download: bool = False
100
109
  """If the model supports it:
101
110
 
@@ -114,17 +123,31 @@ class FileUrl(ABC):
114
123
  compare=False, default=None
115
124
  )
116
125
 
126
+ identifier: str | None = None
127
+ """The identifier of the file, such as a unique ID. generating one from the url if not explicitly set
128
+
129
+ This identifier can be provided to the model in a message to allow it to refer to this file in a tool call argument,
130
+ and the tool can look up the file in question by iterating over the message history and finding the matching `FileUrl`.
131
+
132
+ This identifier is only automatically passed to the model when the `FileUrl` is returned by a tool.
133
+ If you're passing the `FileUrl` as a user message, it's up to you to include a separate text part with the identifier,
134
+ e.g. "This is file <identifier>:" preceding the `FileUrl`.
135
+ """
136
+
117
137
  def __init__(
118
138
  self,
119
139
  url: str,
140
+ *,
120
141
  force_download: bool = False,
121
142
  vendor_metadata: dict[str, Any] | None = None,
122
143
  media_type: str | None = None,
144
+ identifier: str | None = None,
123
145
  ) -> None:
124
146
  self.url = url
125
- self.vendor_metadata = vendor_metadata
126
147
  self.force_download = force_download
148
+ self.vendor_metadata = vendor_metadata
127
149
  self._media_type = media_type
150
+ self.identifier = identifier or _multi_modal_content_identifier(url)
128
151
 
129
152
  @pydantic.computed_field
130
153
  @property
@@ -153,17 +176,20 @@ class VideoUrl(FileUrl):
153
176
  url: str
154
177
  """The URL of the video."""
155
178
 
179
+ _: KW_ONLY
180
+
156
181
  kind: Literal['video-url'] = 'video-url'
157
182
  """Type identifier, this is available on all parts as a discriminator."""
158
183
 
159
184
  def __init__(
160
185
  self,
161
186
  url: str,
187
+ *,
162
188
  force_download: bool = False,
163
189
  vendor_metadata: dict[str, Any] | None = None,
164
190
  media_type: str | None = None,
165
191
  kind: Literal['video-url'] = 'video-url',
166
- *,
192
+ identifier: str | None = None,
167
193
  # Required for inline-snapshot which expects all dataclass `__init__` methods to take all field names as kwargs.
168
194
  _media_type: str | None = None,
169
195
  ) -> None:
@@ -172,6 +198,7 @@ class VideoUrl(FileUrl):
172
198
  force_download=force_download,
173
199
  vendor_metadata=vendor_metadata,
174
200
  media_type=media_type or _media_type,
201
+ identifier=identifier,
175
202
  )
176
203
  self.kind = kind
177
204
 
@@ -224,17 +251,20 @@ class AudioUrl(FileUrl):
224
251
  url: str
225
252
  """The URL of the audio file."""
226
253
 
254
+ _: KW_ONLY
255
+
227
256
  kind: Literal['audio-url'] = 'audio-url'
228
257
  """Type identifier, this is available on all parts as a discriminator."""
229
258
 
230
259
  def __init__(
231
260
  self,
232
261
  url: str,
262
+ *,
233
263
  force_download: bool = False,
234
264
  vendor_metadata: dict[str, Any] | None = None,
235
265
  media_type: str | None = None,
236
266
  kind: Literal['audio-url'] = 'audio-url',
237
- *,
267
+ identifier: str | None = None,
238
268
  # Required for inline-snapshot which expects all dataclass `__init__` methods to take all field names as kwargs.
239
269
  _media_type: str | None = None,
240
270
  ) -> None:
@@ -243,6 +273,7 @@ class AudioUrl(FileUrl):
243
273
  force_download=force_download,
244
274
  vendor_metadata=vendor_metadata,
245
275
  media_type=media_type or _media_type,
276
+ identifier=identifier,
246
277
  )
247
278
  self.kind = kind
248
279
 
@@ -282,17 +313,20 @@ class ImageUrl(FileUrl):
282
313
  url: str
283
314
  """The URL of the image."""
284
315
 
316
+ _: KW_ONLY
317
+
285
318
  kind: Literal['image-url'] = 'image-url'
286
319
  """Type identifier, this is available on all parts as a discriminator."""
287
320
 
288
321
  def __init__(
289
322
  self,
290
323
  url: str,
324
+ *,
291
325
  force_download: bool = False,
292
326
  vendor_metadata: dict[str, Any] | None = None,
293
327
  media_type: str | None = None,
294
328
  kind: Literal['image-url'] = 'image-url',
295
- *,
329
+ identifier: str | None = None,
296
330
  # Required for inline-snapshot which expects all dataclass `__init__` methods to take all field names as kwargs.
297
331
  _media_type: str | None = None,
298
332
  ) -> None:
@@ -301,6 +335,7 @@ class ImageUrl(FileUrl):
301
335
  force_download=force_download,
302
336
  vendor_metadata=vendor_metadata,
303
337
  media_type=media_type or _media_type,
338
+ identifier=identifier,
304
339
  )
305
340
  self.kind = kind
306
341
 
@@ -335,17 +370,20 @@ class DocumentUrl(FileUrl):
335
370
  url: str
336
371
  """The URL of the document."""
337
372
 
373
+ _: KW_ONLY
374
+
338
375
  kind: Literal['document-url'] = 'document-url'
339
376
  """Type identifier, this is available on all parts as a discriminator."""
340
377
 
341
378
  def __init__(
342
379
  self,
343
380
  url: str,
381
+ *,
344
382
  force_download: bool = False,
345
383
  vendor_metadata: dict[str, Any] | None = None,
346
384
  media_type: str | None = None,
347
385
  kind: Literal['document-url'] = 'document-url',
348
- *,
386
+ identifier: str | None = None,
349
387
  # Required for inline-snapshot which expects all dataclass `__init__` methods to take all field names as kwargs.
350
388
  _media_type: str | None = None,
351
389
  ) -> None:
@@ -354,6 +392,7 @@ class DocumentUrl(FileUrl):
354
392
  force_download=force_download,
355
393
  vendor_metadata=vendor_metadata,
356
394
  media_type=media_type or _media_type,
395
+ identifier=identifier,
357
396
  )
358
397
  self.kind = kind
359
398
 
@@ -396,22 +435,26 @@ class DocumentUrl(FileUrl):
396
435
  raise ValueError(f'Unknown document media type: {media_type}') from e
397
436
 
398
437
 
399
- @dataclass(repr=False)
438
+ @dataclass(init=False, repr=False)
400
439
  class BinaryContent:
401
440
  """Binary content, e.g. an audio or image file."""
402
441
 
403
442
  data: bytes
404
443
  """The binary data."""
405
444
 
445
+ _: KW_ONLY
446
+
406
447
  media_type: AudioMediaType | ImageMediaType | DocumentMediaType | str
407
448
  """The media type of the binary data."""
408
449
 
409
- identifier: str | None = None
410
- """Identifier for the binary content, such as a URL or unique ID.
411
-
412
- This identifier can be provided to the model in a message to allow it to refer to this file in a tool call argument, and the tool can look up the file in question by iterating over the message history and finding the matching `BinaryContent`.
450
+ identifier: str
451
+ """Identifier for the binary content, such as a unique ID. generating one from the data if not explicitly set
452
+ This identifier can be provided to the model in a message to allow it to refer to this file in a tool call argument,
453
+ and the tool can look up the file in question by iterating over the message history and finding the matching `BinaryContent`.
413
454
 
414
- This identifier is only automatically passed to the model when the `BinaryContent` is returned by a tool. If you're passing the `BinaryContent` as a user message, it's up to you to include a separate text part with the identifier, e.g. "This is file <identifier>:" preceding the `BinaryContent`.
455
+ This identifier is only automatically passed to the model when the `BinaryContent` is returned by a tool.
456
+ If you're passing the `BinaryContent` as a user message, it's up to you to include a separate text part with the identifier,
457
+ e.g. "This is file <identifier>:" preceding the `BinaryContent`.
415
458
  """
416
459
 
417
460
  vendor_metadata: dict[str, Any] | None = None
@@ -424,6 +467,21 @@ class BinaryContent:
424
467
  kind: Literal['binary'] = 'binary'
425
468
  """Type identifier, this is available on all parts as a discriminator."""
426
469
 
470
+ def __init__(
471
+ self,
472
+ data: bytes,
473
+ *,
474
+ media_type: AudioMediaType | ImageMediaType | DocumentMediaType | str,
475
+ identifier: str | None = None,
476
+ vendor_metadata: dict[str, Any] | None = None,
477
+ kind: Literal['binary'] = 'binary',
478
+ ) -> None:
479
+ self.data = data
480
+ self.media_type = media_type
481
+ self.identifier = identifier or _multi_modal_content_identifier(data)
482
+ self.vendor_metadata = vendor_metadata
483
+ self.kind = kind
484
+
427
485
  @property
428
486
  def is_audio(self) -> bool:
429
487
  """Return `True` if the media type is an audio type."""
@@ -462,7 +520,8 @@ class BinaryContent:
462
520
  __repr__ = _utils.dataclasses_no_defaults_repr
463
521
 
464
522
 
465
- UserContent: TypeAlias = 'str | ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent'
523
+ MultiModalContent = ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent
524
+ UserContent: TypeAlias = str | MultiModalContent
466
525
 
467
526
 
468
527
  @dataclass(repr=False)
@@ -478,17 +537,19 @@ class ToolReturn:
478
537
  return_value: Any
479
538
  """The return value to be used in the tool response."""
480
539
 
540
+ _: KW_ONLY
541
+
481
542
  content: str | Sequence[UserContent] | None = None
482
543
  """The content to be sent to the model as a UserPromptPart."""
483
544
 
484
545
  metadata: Any = None
485
546
  """Additional data that can be accessed programmatically by the application but is not sent to the LLM."""
486
547
 
548
+ kind: Literal['tool-return'] = 'tool-return'
549
+
487
550
  __repr__ = _utils.dataclasses_no_defaults_repr
488
551
 
489
552
 
490
- # Ideally this would be a Union of types, but Python 3.9 requires it to be a string, and strings don't work with `isinstance``.
491
- MultiModalContentTypes = (ImageUrl, AudioUrl, DocumentUrl, VideoUrl, BinaryContent)
492
553
  _document_format_lookup: dict[str, DocumentFormat] = {
493
554
  'application/pdf': 'pdf',
494
555
  'text/plain': 'txt',
@@ -536,6 +597,8 @@ class UserPromptPart:
536
597
  content: str | Sequence[UserContent]
537
598
  """The content of the prompt."""
538
599
 
600
+ _: KW_ONLY
601
+
539
602
  timestamp: datetime = field(default_factory=_now_utc)
540
603
  """The timestamp of the prompt."""
541
604
 
@@ -562,7 +625,7 @@ class UserPromptPart:
562
625
  parts.append(
563
626
  _otel_messages.TextPart(type='text', **({'content': part} if settings.include_content else {}))
564
627
  )
565
- elif isinstance(part, (ImageUrl, AudioUrl, DocumentUrl, VideoUrl)):
628
+ elif isinstance(part, ImageUrl | AudioUrl | DocumentUrl | VideoUrl):
566
629
  parts.append(
567
630
  _otel_messages.MediaUrlPart(
568
631
  type=part.kind,
@@ -599,6 +662,8 @@ class BaseToolReturnPart:
599
662
  tool_call_id: str
600
663
  """The tool call identifier, this is used by some models including OpenAI."""
601
664
 
665
+ _: KW_ONLY
666
+
602
667
  metadata: Any = None
603
668
  """Additional data that can be accessed programmatically by the application but is not sent to the LLM."""
604
669
 
@@ -654,6 +719,8 @@ class BaseToolReturnPart:
654
719
  class ToolReturnPart(BaseToolReturnPart):
655
720
  """A tool return message, this encodes the result of running a tool."""
656
721
 
722
+ _: KW_ONLY
723
+
657
724
  part_kind: Literal['tool-return'] = 'tool-return'
658
725
  """Part type identifier, this is available on all parts as a discriminator."""
659
726
 
@@ -662,6 +729,8 @@ class ToolReturnPart(BaseToolReturnPart):
662
729
  class BuiltinToolReturnPart(BaseToolReturnPart):
663
730
  """A tool return message from a built-in tool."""
664
731
 
732
+ _: KW_ONLY
733
+
665
734
  provider_name: str | None = None
666
735
  """The name of the provider that generated the response."""
667
736
 
@@ -695,6 +764,8 @@ class RetryPromptPart:
695
764
  error details.
696
765
  """
697
766
 
767
+ _: KW_ONLY
768
+
698
769
  tool_name: str | None = None
699
770
  """The name of the tool that was called, if any."""
700
771
 
@@ -753,7 +824,7 @@ class RetryPromptPart:
753
824
 
754
825
 
755
826
  ModelRequestPart = Annotated[
756
- Union[SystemPromptPart, UserPromptPart, ToolReturnPart, RetryPromptPart], pydantic.Discriminator('part_kind')
827
+ SystemPromptPart | UserPromptPart | ToolReturnPart | RetryPromptPart, pydantic.Discriminator('part_kind')
757
828
  ]
758
829
  """A message part sent by Pydantic AI to a model."""
759
830
 
@@ -762,9 +833,11 @@ ModelRequestPart = Annotated[
762
833
  class ModelRequest:
763
834
  """A request generated by Pydantic AI and sent to a model, e.g. a message from the Pydantic AI app to the model."""
764
835
 
765
- parts: list[ModelRequestPart]
836
+ parts: Sequence[ModelRequestPart]
766
837
  """The parts of the user message."""
767
838
 
839
+ _: KW_ONLY
840
+
768
841
  instructions: str | None = None
769
842
  """The instructions for the model."""
770
843
 
@@ -786,6 +859,8 @@ class TextPart:
786
859
  content: str
787
860
  """The text content of the response."""
788
861
 
862
+ _: KW_ONLY
863
+
789
864
  part_kind: Literal['text'] = 'text'
790
865
  """Part type identifier, this is available on all parts as a discriminator."""
791
866
 
@@ -803,6 +878,8 @@ class ThinkingPart:
803
878
  content: str
804
879
  """The thinking content of the response."""
805
880
 
881
+ _: KW_ONLY
882
+
806
883
  id: str | None = None
807
884
  """The identifier of the thinking part."""
808
885
 
@@ -881,6 +958,8 @@ class BaseToolCallPart:
881
958
  class ToolCallPart(BaseToolCallPart):
882
959
  """A tool call from a model."""
883
960
 
961
+ _: KW_ONLY
962
+
884
963
  part_kind: Literal['tool-call'] = 'tool-call'
885
964
  """Part type identifier, this is available on all parts as a discriminator."""
886
965
 
@@ -889,6 +968,8 @@ class ToolCallPart(BaseToolCallPart):
889
968
  class BuiltinToolCallPart(BaseToolCallPart):
890
969
  """A tool call to a built-in tool."""
891
970
 
971
+ _: KW_ONLY
972
+
892
973
  provider_name: str | None = None
893
974
  """The name of the provider that generated the response."""
894
975
 
@@ -897,7 +978,7 @@ class BuiltinToolCallPart(BaseToolCallPart):
897
978
 
898
979
 
899
980
  ModelResponsePart = Annotated[
900
- Union[TextPart, ToolCallPart, BuiltinToolCallPart, BuiltinToolReturnPart, ThinkingPart],
981
+ TextPart | ToolCallPart | BuiltinToolCallPart | BuiltinToolReturnPart | ThinkingPart,
901
982
  pydantic.Discriminator('part_kind'),
902
983
  ]
903
984
  """A message part returned by a model."""
@@ -907,9 +988,11 @@ ModelResponsePart = Annotated[
907
988
  class ModelResponse:
908
989
  """A response from a model, e.g. a message from the model to the Pydantic AI app."""
909
990
 
910
- parts: list[ModelResponsePart]
991
+ parts: Sequence[ModelResponsePart]
911
992
  """The parts of the model message."""
912
993
 
994
+ _: KW_ONLY
995
+
913
996
  usage: RequestUsage = field(default_factory=RequestUsage)
914
997
  """Usage information for the request.
915
998
 
@@ -931,18 +1014,30 @@ class ModelResponse:
931
1014
  provider_name: str | None = None
932
1015
  """The name of the LLM provider that generated the response."""
933
1016
 
934
- provider_details: dict[str, Any] | None = field(default=None)
1017
+ provider_details: Annotated[
1018
+ dict[str, Any] | None,
1019
+ # `vendor_details` is deprecated, but we still want to support deserializing model responses stored in a DB before the name was changed
1020
+ pydantic.Field(validation_alias=pydantic.AliasChoices('provider_details', 'vendor_details')),
1021
+ ] = None
935
1022
  """Additional provider-specific details in a serializable format.
936
1023
 
937
1024
  This allows storing selected vendor-specific data that isn't mapped to standard ModelResponse fields.
938
1025
  For OpenAI models, this may include 'logprobs', 'finish_reason', etc.
939
1026
  """
940
1027
 
941
- provider_response_id: str | None = None
1028
+ provider_response_id: Annotated[
1029
+ str | None,
1030
+ # `vendor_id` is deprecated, but we still want to support deserializing model responses stored in a DB before the name was changed
1031
+ pydantic.Field(validation_alias=pydantic.AliasChoices('provider_response_id', 'vendor_id')),
1032
+ ] = None
942
1033
  """request ID as specified by the model provider. This can be used to track the specific request to the model."""
943
1034
 
944
- def price(self) -> genai_types.PriceCalculation:
945
- """Calculate the price of the usage.
1035
+ @deprecated('`price` is deprecated, use `cost` instead')
1036
+ def price(self) -> genai_types.PriceCalculation: # pragma: no cover
1037
+ return self.cost()
1038
+
1039
+ def cost(self) -> genai_types.PriceCalculation:
1040
+ """Calculate the cost of the usage.
946
1041
 
947
1042
  Uses [`genai-prices`](https://github.com/pydantic/genai-prices).
948
1043
  """
@@ -970,14 +1065,14 @@ class ModelResponse:
970
1065
  body.setdefault('tool_calls', []).append(
971
1066
  {
972
1067
  'id': part.tool_call_id,
973
- 'type': 'function', # TODO https://github.com/pydantic/pydantic-ai/issues/888
1068
+ 'type': 'function',
974
1069
  'function': {
975
1070
  'name': part.tool_name,
976
1071
  **({'arguments': part.args} if settings.include_content else {}),
977
1072
  },
978
1073
  }
979
1074
  )
980
- elif isinstance(part, (TextPart, ThinkingPart)):
1075
+ elif isinstance(part, TextPart | ThinkingPart):
981
1076
  kind = part.part_kind
982
1077
  body.setdefault('content', []).append(
983
1078
  {'kind': kind, **({'text': part.content} if settings.include_content else {})}
@@ -1038,7 +1133,7 @@ class ModelResponse:
1038
1133
  __repr__ = _utils.dataclasses_no_defaults_repr
1039
1134
 
1040
1135
 
1041
- ModelMessage = Annotated[Union[ModelRequest, ModelResponse], pydantic.Discriminator('kind')]
1136
+ ModelMessage = Annotated[ModelRequest | ModelResponse, pydantic.Discriminator('kind')]
1042
1137
  """Any message sent to or returned by a model."""
1043
1138
 
1044
1139
  ModelMessagesTypeAdapter = pydantic.TypeAdapter(
@@ -1054,6 +1149,8 @@ class TextPartDelta:
1054
1149
  content_delta: str
1055
1150
  """The incremental text content to add to the existing `TextPart` content."""
1056
1151
 
1152
+ _: KW_ONLY
1153
+
1057
1154
  part_delta_kind: Literal['text'] = 'text'
1058
1155
  """Part delta type identifier, used as a discriminator."""
1059
1156
 
@@ -1076,7 +1173,7 @@ class TextPartDelta:
1076
1173
  __repr__ = _utils.dataclasses_no_defaults_repr
1077
1174
 
1078
1175
 
1079
- @dataclass(repr=False)
1176
+ @dataclass(repr=False, kw_only=True)
1080
1177
  class ThinkingPartDelta:
1081
1178
  """A partial update (delta) for a `ThinkingPart` to append new thinking content."""
1082
1179
 
@@ -1128,7 +1225,7 @@ class ThinkingPartDelta:
1128
1225
  __repr__ = _utils.dataclasses_no_defaults_repr
1129
1226
 
1130
1227
 
1131
- @dataclass(repr=False)
1228
+ @dataclass(repr=False, kw_only=True)
1132
1229
  class ToolCallPartDelta:
1133
1230
  """A partial update (delta) for a `ToolCallPart` to modify tool name, arguments, or tool call ID."""
1134
1231
 
@@ -1248,12 +1345,12 @@ class ToolCallPartDelta:
1248
1345
 
1249
1346
 
1250
1347
  ModelResponsePartDelta = Annotated[
1251
- Union[TextPartDelta, ThinkingPartDelta, ToolCallPartDelta], pydantic.Discriminator('part_delta_kind')
1348
+ TextPartDelta | ThinkingPartDelta | ToolCallPartDelta, pydantic.Discriminator('part_delta_kind')
1252
1349
  ]
1253
1350
  """A partial update (delta) for any model response part."""
1254
1351
 
1255
1352
 
1256
- @dataclass(repr=False)
1353
+ @dataclass(repr=False, kw_only=True)
1257
1354
  class PartStartEvent:
1258
1355
  """An event indicating that a new part has started.
1259
1356
 
@@ -1273,7 +1370,7 @@ class PartStartEvent:
1273
1370
  __repr__ = _utils.dataclasses_no_defaults_repr
1274
1371
 
1275
1372
 
1276
- @dataclass(repr=False)
1373
+ @dataclass(repr=False, kw_only=True)
1277
1374
  class PartDeltaEvent:
1278
1375
  """An event indicating a delta update for an existing part."""
1279
1376
 
@@ -1289,7 +1386,7 @@ class PartDeltaEvent:
1289
1386
  __repr__ = _utils.dataclasses_no_defaults_repr
1290
1387
 
1291
1388
 
1292
- @dataclass(repr=False)
1389
+ @dataclass(repr=False, kw_only=True)
1293
1390
  class FinalResultEvent:
1294
1391
  """An event indicating the response to the current model request matches the output schema and will produce a result."""
1295
1392
 
@@ -1304,7 +1401,7 @@ class FinalResultEvent:
1304
1401
 
1305
1402
 
1306
1403
  ModelResponseStreamEvent = Annotated[
1307
- Union[PartStartEvent, PartDeltaEvent, FinalResultEvent], pydantic.Discriminator('event_kind')
1404
+ PartStartEvent | PartDeltaEvent | FinalResultEvent, pydantic.Discriminator('event_kind')
1308
1405
  ]
1309
1406
  """An event in the model response stream, starting a new part, applying a delta to an existing one, or indicating the final result."""
1310
1407
 
@@ -1315,6 +1412,9 @@ class FunctionToolCallEvent:
1315
1412
 
1316
1413
  part: ToolCallPart
1317
1414
  """The (function) tool call to make."""
1415
+
1416
+ _: KW_ONLY
1417
+
1318
1418
  event_kind: Literal['function_tool_call'] = 'function_tool_call'
1319
1419
  """Event type identifier, used as a discriminator."""
1320
1420
 
@@ -1338,6 +1438,9 @@ class FunctionToolResultEvent:
1338
1438
 
1339
1439
  result: ToolReturnPart | RetryPromptPart
1340
1440
  """The result of the call to the function tool."""
1441
+
1442
+ _: KW_ONLY
1443
+
1341
1444
  event_kind: Literal['function_tool_result'] = 'function_tool_result'
1342
1445
  """Event type identifier, used as a discriminator."""
1343
1446
 
@@ -1356,6 +1459,8 @@ class BuiltinToolCallEvent:
1356
1459
  part: BuiltinToolCallPart
1357
1460
  """The built-in tool call to make."""
1358
1461
 
1462
+ _: KW_ONLY
1463
+
1359
1464
  event_kind: Literal['builtin_tool_call'] = 'builtin_tool_call'
1360
1465
  """Event type identifier, used as a discriminator."""
1361
1466
 
@@ -1367,15 +1472,17 @@ class BuiltinToolResultEvent:
1367
1472
  result: BuiltinToolReturnPart
1368
1473
  """The result of the call to the built-in tool."""
1369
1474
 
1475
+ _: KW_ONLY
1476
+
1370
1477
  event_kind: Literal['builtin_tool_result'] = 'builtin_tool_result'
1371
1478
  """Event type identifier, used as a discriminator."""
1372
1479
 
1373
1480
 
1374
1481
  HandleResponseEvent = Annotated[
1375
- Union[FunctionToolCallEvent, FunctionToolResultEvent, BuiltinToolCallEvent, BuiltinToolResultEvent],
1482
+ FunctionToolCallEvent | FunctionToolResultEvent | BuiltinToolCallEvent | BuiltinToolResultEvent,
1376
1483
  pydantic.Discriminator('event_kind'),
1377
1484
  ]
1378
1485
  """An event yielded when handling a model response, indicating tool calls and results."""
1379
1486
 
1380
- AgentStreamEvent = Annotated[Union[ModelResponseStreamEvent, HandleResponseEvent], pydantic.Discriminator('event_kind')]
1487
+ AgentStreamEvent = Annotated[ModelResponseStreamEvent | HandleResponseEvent, pydantic.Discriminator('event_kind')]
1381
1488
  """An event in the agent stream: model response stream events and response-handling events."""
@@ -14,10 +14,10 @@ from contextlib import asynccontextmanager, contextmanager
14
14
  from dataclasses import dataclass, field, replace
15
15
  from datetime import datetime
16
16
  from functools import cache, cached_property
17
- from typing import Any, Generic, TypeVar, overload
17
+ from typing import Any, Generic, Literal, TypeVar, overload
18
18
 
19
19
  import httpx
20
- from typing_extensions import Literal, TypeAliasType, TypedDict
20
+ from typing_extensions import TypeAliasType, TypedDict
21
21
 
22
22
  from .. import _utils
23
23
  from .._output import OutputObjectDefinition
@@ -367,7 +367,7 @@ KnownModelName = TypeAliasType(
367
367
  """
368
368
 
369
369
 
370
- @dataclass(repr=False)
370
+ @dataclass(repr=False, kw_only=True)
371
371
  class ModelRequestParameters:
372
372
  """Configuration for an agent's request to a model, specifically related to tools and output handling."""
373
373
 
@@ -552,6 +552,7 @@ class StreamedResponse(ABC):
552
552
  """Streamed response from an LLM when calling a tool."""
553
553
 
554
554
  model_request_parameters: ModelRequestParameters
555
+
555
556
  final_result_event: FinalResultEvent | None = field(default=None, init=False)
556
557
 
557
558
  _parts_manager: ModelResponsePartsManager = field(default_factory=ModelResponsePartsManager, init=False)
@@ -727,6 +728,7 @@ def infer_model(model: Model | KnownModelName | str) -> Model: # noqa: C901
727
728
  'openrouter',
728
729
  'together',
729
730
  'vercel',
731
+ 'litellm',
730
732
  ):
731
733
  from .openai import OpenAIChatModel
732
734
 
@@ -920,5 +922,5 @@ def _get_final_result_event(e: ModelResponseStreamEvent, params: ModelRequestPar
920
922
  elif isinstance(new_part, ToolCallPart) and (tool_def := params.tool_defs.get(new_part.tool_name)):
921
923
  if tool_def.kind == 'output':
922
924
  return FinalResultEvent(tool_name=new_part.tool_name, tool_call_id=new_part.tool_call_id)
923
- elif tool_def.kind == 'deferred':
925
+ elif tool_def.defer:
924
926
  return FinalResultEvent(tool_name=None, tool_call_id=None)