pydantic-ai-slim 0.8.0__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 +84 -17
  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 +70 -17
  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 +6 -3
  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 +156 -44
  31. pydantic_ai/models/__init__.py +20 -7
  32. pydantic_ai/models/anthropic.py +10 -17
  33. pydantic_ai/models/bedrock.py +55 -57
  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 +13 -14
  38. pydantic_ai/models/google.py +19 -5
  39. pydantic_ai/models/groq.py +127 -39
  40. pydantic_ai/models/huggingface.py +5 -5
  41. pydantic_ai/models/instrumented.py +49 -21
  42. pydantic_ai/models/mcp_sampling.py +3 -1
  43. pydantic_ai/models/mistral.py +8 -8
  44. pydantic_ai/models/openai.py +37 -42
  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 +173 -52
  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.0.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.0.dist-info/RECORD +0 -119
  73. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0.dist-info}/WHEEL +0 -0
  74. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0.dist-info}/entry_points.txt +0 -0
  75. {pydantic_ai_slim-0.8.0.dist-info → pydantic_ai_slim-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ from collections.abc import AsyncIterable, AsyncIterator, Sequence
6
6
  from contextlib import asynccontextmanager
7
7
  from dataclasses import dataclass, field
8
8
  from datetime import datetime
9
- from typing import Any, Literal, Union, cast, overload
9
+ from typing import Any, Literal, cast, overload
10
10
 
11
11
  from pydantic import ValidationError
12
12
  from typing_extensions import assert_never, deprecated
@@ -90,7 +90,7 @@ __all__ = (
90
90
  'OpenAIModelName',
91
91
  )
92
92
 
93
- OpenAIModelName = Union[str, AllModels]
93
+ OpenAIModelName = str | AllModels
94
94
  """
95
95
  Possible OpenAI model names.
96
96
 
@@ -225,6 +225,7 @@ class OpenAIChatModel(Model):
225
225
  'openrouter',
226
226
  'together',
227
227
  'vercel',
228
+ 'litellm',
228
229
  ]
229
230
  | Provider[AsyncOpenAI] = 'openai',
230
231
  profile: ModelProfileSpec | None = None,
@@ -252,6 +253,7 @@ class OpenAIChatModel(Model):
252
253
  'openrouter',
253
254
  'together',
254
255
  'vercel',
256
+ 'litellm',
255
257
  ]
256
258
  | Provider[AsyncOpenAI] = 'openai',
257
259
  profile: ModelProfileSpec | None = None,
@@ -278,6 +280,7 @@ class OpenAIChatModel(Model):
278
280
  'openrouter',
279
281
  'together',
280
282
  'vercel',
283
+ 'litellm',
281
284
  ]
282
285
  | Provider[AsyncOpenAI] = 'openai',
283
286
  profile: ModelProfileSpec | None = None,
@@ -409,13 +412,6 @@ class OpenAIChatModel(Model):
409
412
  for setting in unsupported_model_settings:
410
413
  model_settings.pop(setting, None)
411
414
 
412
- # TODO(Marcelo): Deprecate this in favor of `openai_unsupported_model_settings`.
413
- sampling_settings = (
414
- model_settings
415
- if OpenAIModelProfile.from_profile(self.profile).openai_supports_sampling_settings
416
- else OpenAIChatModelSettings()
417
- )
418
-
419
415
  try:
420
416
  extra_headers = model_settings.get('extra_headers', {})
421
417
  extra_headers.setdefault('User-Agent', get_user_agent())
@@ -437,13 +433,13 @@ class OpenAIChatModel(Model):
437
433
  web_search_options=web_search_options or NOT_GIVEN,
438
434
  service_tier=model_settings.get('openai_service_tier', NOT_GIVEN),
439
435
  prediction=model_settings.get('openai_prediction', NOT_GIVEN),
440
- temperature=sampling_settings.get('temperature', NOT_GIVEN),
441
- top_p=sampling_settings.get('top_p', NOT_GIVEN),
442
- presence_penalty=sampling_settings.get('presence_penalty', NOT_GIVEN),
443
- frequency_penalty=sampling_settings.get('frequency_penalty', NOT_GIVEN),
444
- logit_bias=sampling_settings.get('logit_bias', NOT_GIVEN),
445
- logprobs=sampling_settings.get('openai_logprobs', NOT_GIVEN),
446
- top_logprobs=sampling_settings.get('openai_top_logprobs', NOT_GIVEN),
436
+ temperature=model_settings.get('temperature', NOT_GIVEN),
437
+ top_p=model_settings.get('top_p', NOT_GIVEN),
438
+ presence_penalty=model_settings.get('presence_penalty', NOT_GIVEN),
439
+ frequency_penalty=model_settings.get('frequency_penalty', NOT_GIVEN),
440
+ logit_bias=model_settings.get('logit_bias', NOT_GIVEN),
441
+ logprobs=model_settings.get('openai_logprobs', NOT_GIVEN),
442
+ top_logprobs=model_settings.get('openai_top_logprobs', NOT_GIVEN),
447
443
  extra_headers=extra_headers,
448
444
  extra_body=model_settings.get('extra_body'),
449
445
  )
@@ -512,12 +508,12 @@ class OpenAIChatModel(Model):
512
508
  part.tool_call_id = _guard_tool_call_id(part)
513
509
  items.append(part)
514
510
  return ModelResponse(
515
- items,
511
+ parts=items,
516
512
  usage=_map_usage(response),
517
513
  model_name=response.model,
518
514
  timestamp=timestamp,
519
515
  provider_details=vendor_details,
520
- provider_request_id=response.id,
516
+ provider_response_id=response.id,
521
517
  provider_name=self._provider.name,
522
518
  )
523
519
 
@@ -582,7 +578,7 @@ class OpenAIChatModel(Model):
582
578
  elif isinstance(item, ToolCallPart):
583
579
  tool_calls.append(self._map_tool_call(item))
584
580
  # OpenAI doesn't return built-in tool calls
585
- elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
581
+ elif isinstance(item, BuiltinToolCallPart | BuiltinToolReturnPart): # pragma: no cover
586
582
  pass
587
583
  else:
588
584
  assert_never(item)
@@ -613,7 +609,7 @@ class OpenAIChatModel(Model):
613
609
  def _map_json_schema(self, o: OutputObjectDefinition) -> chat.completion_create_params.ResponseFormat:
614
610
  response_format_param: chat.completion_create_params.ResponseFormatJSONSchema = { # pyright: ignore[reportPrivateImportUsage]
615
611
  'type': 'json_schema',
616
- 'json_schema': {'name': o.name or DEFAULT_OUTPUT_TOOL_NAME, 'schema': o.json_schema, 'strict': True},
612
+ 'json_schema': {'name': o.name or DEFAULT_OUTPUT_TOOL_NAME, 'schema': o.json_schema},
617
613
  }
618
614
  if o.description:
619
615
  response_format_param['json_schema']['description'] = o.description
@@ -828,10 +824,10 @@ class OpenAIResponsesModel(Model):
828
824
  elif item.type == 'function_call':
829
825
  items.append(ToolCallPart(item.name, item.arguments, tool_call_id=item.call_id))
830
826
  return ModelResponse(
831
- items,
827
+ parts=items,
832
828
  usage=_map_usage(response),
833
829
  model_name=response.model,
834
- provider_request_id=response.id,
830
+ provider_response_id=response.id,
835
831
  timestamp=timestamp,
836
832
  provider_name=self._provider.name,
837
833
  )
@@ -918,11 +914,9 @@ class OpenAIResponsesModel(Model):
918
914
  text = text or {}
919
915
  text['verbosity'] = verbosity
920
916
 
921
- sampling_settings = (
922
- model_settings
923
- if OpenAIModelProfile.from_profile(self.profile).openai_supports_sampling_settings
924
- else OpenAIResponsesModelSettings()
925
- )
917
+ unsupported_model_settings = OpenAIModelProfile.from_profile(self.profile).openai_unsupported_model_settings
918
+ for setting in unsupported_model_settings:
919
+ model_settings.pop(setting, None)
926
920
 
927
921
  try:
928
922
  extra_headers = model_settings.get('extra_headers', {})
@@ -936,8 +930,8 @@ class OpenAIResponsesModel(Model):
936
930
  tool_choice=tool_choice or NOT_GIVEN,
937
931
  max_output_tokens=model_settings.get('max_tokens', NOT_GIVEN),
938
932
  stream=stream,
939
- temperature=sampling_settings.get('temperature', NOT_GIVEN),
940
- top_p=sampling_settings.get('top_p', NOT_GIVEN),
933
+ temperature=model_settings.get('temperature', NOT_GIVEN),
934
+ top_p=model_settings.get('top_p', NOT_GIVEN),
941
935
  truncation=model_settings.get('openai_truncation', NOT_GIVEN),
942
936
  timeout=model_settings.get('timeout', NOT_GIVEN),
943
937
  service_tier=model_settings.get('openai_service_tier', NOT_GIVEN),
@@ -1049,7 +1043,7 @@ class OpenAIResponsesModel(Model):
1049
1043
  elif isinstance(item, ToolCallPart):
1050
1044
  openai_messages.append(self._map_tool_call(item))
1051
1045
  # OpenAI doesn't return built-in tool calls
1052
- elif isinstance(item, (BuiltinToolCallPart, BuiltinToolReturnPart)):
1046
+ elif isinstance(item, BuiltinToolCallPart | BuiltinToolReturnPart):
1053
1047
  pass
1054
1048
  elif isinstance(item, ThinkingPart):
1055
1049
  # NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
@@ -1180,6 +1174,10 @@ class OpenAIStreamedResponse(StreamedResponse):
1180
1174
  except IndexError:
1181
1175
  continue
1182
1176
 
1177
+ # When using Azure OpenAI and an async content filter is enabled, the openai SDK can return None deltas.
1178
+ if choice.delta is None: # pyright: ignore[reportUnnecessaryComparison]
1179
+ continue
1180
+
1183
1181
  # Handle the text part of the response
1184
1182
  content = choice.delta.content
1185
1183
  if content is not None:
@@ -1279,12 +1277,7 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1279
1277
  tool_call_id=chunk.item.call_id,
1280
1278
  )
1281
1279
  elif isinstance(chunk.item, responses.ResponseReasoningItem):
1282
- content = chunk.item.summary[0].text if chunk.item.summary else ''
1283
- yield self._parts_manager.handle_thinking_delta(
1284
- vendor_part_id=chunk.item.id,
1285
- content=content,
1286
- signature=chunk.item.id,
1287
- )
1280
+ pass
1288
1281
  elif isinstance(chunk.item, responses.ResponseOutputMessage):
1289
1282
  pass
1290
1283
  elif isinstance(chunk.item, responses.ResponseFunctionWebSearch):
@@ -1300,7 +1293,11 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1300
1293
  pass
1301
1294
 
1302
1295
  elif isinstance(chunk, responses.ResponseReasoningSummaryPartAddedEvent):
1303
- pass # there's nothing we need to do here
1296
+ yield self._parts_manager.handle_thinking_delta(
1297
+ vendor_part_id=f'{chunk.item_id}-{chunk.summary_index}',
1298
+ content=chunk.part.text,
1299
+ id=chunk.item_id,
1300
+ )
1304
1301
 
1305
1302
  elif isinstance(chunk, responses.ResponseReasoningSummaryPartDoneEvent):
1306
1303
  pass # there's nothing we need to do here
@@ -1310,9 +1307,9 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1310
1307
 
1311
1308
  elif isinstance(chunk, responses.ResponseReasoningSummaryTextDeltaEvent):
1312
1309
  yield self._parts_manager.handle_thinking_delta(
1313
- vendor_part_id=chunk.item_id,
1310
+ vendor_part_id=f'{chunk.item_id}-{chunk.summary_index}',
1314
1311
  content=chunk.delta,
1315
- signature=chunk.item_id,
1312
+ id=chunk.item_id,
1316
1313
  )
1317
1314
 
1318
1315
  # TODO(Marcelo): We should support annotations in the future.
@@ -1320,9 +1317,7 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1320
1317
  pass # there's nothing we need to do here
1321
1318
 
1322
1319
  elif isinstance(chunk, responses.ResponseTextDeltaEvent):
1323
- maybe_event = self._parts_manager.handle_text_delta(
1324
- vendor_part_id=chunk.content_index, content=chunk.delta
1325
- )
1320
+ maybe_event = self._parts_manager.handle_text_delta(vendor_part_id=chunk.item_id, content=chunk.delta)
1326
1321
  if maybe_event is not None: # pragma: no branch
1327
1322
  yield maybe_event
1328
1323
 
@@ -195,7 +195,10 @@ class TestModel(Model):
195
195
  # if there are tools, the first thing we want to do is call all of them
196
196
  if tool_calls and not any(isinstance(m, ModelResponse) for m in messages):
197
197
  return ModelResponse(
198
- parts=[ToolCallPart(name, self.gen_tool_args(args)) for name, args in tool_calls],
198
+ parts=[
199
+ ToolCallPart(name, self.gen_tool_args(args), tool_call_id=f'pyd_ai_tool_call_id__{name}')
200
+ for name, args in tool_calls
201
+ ],
199
202
  model_name=self._model_name,
200
203
  )
201
204
 
@@ -220,6 +223,7 @@ class TestModel(Model):
220
223
  output_wrapper.value
221
224
  if isinstance(output_wrapper, _WrappedToolOutput) and output_wrapper.value is not None
222
225
  else self.gen_tool_args(tool),
226
+ tool_call_id=f'pyd_ai_tool_call_id__{tool.name}',
223
227
  )
224
228
  for tool in output_tools
225
229
  if tool.name in new_retry_names
@@ -250,11 +254,27 @@ class TestModel(Model):
250
254
  output_tool = output_tools[self.seed % len(output_tools)]
251
255
  if custom_output_args is not None:
252
256
  return ModelResponse(
253
- parts=[ToolCallPart(output_tool.name, custom_output_args)], model_name=self._model_name
257
+ parts=[
258
+ ToolCallPart(
259
+ output_tool.name,
260
+ custom_output_args,
261
+ tool_call_id=f'pyd_ai_tool_call_id__{output_tool.name}',
262
+ )
263
+ ],
264
+ model_name=self._model_name,
254
265
  )
255
266
  else:
256
267
  response_args = self.gen_tool_args(output_tool)
257
- return ModelResponse(parts=[ToolCallPart(output_tool.name, response_args)], model_name=self._model_name)
268
+ return ModelResponse(
269
+ parts=[
270
+ ToolCallPart(
271
+ output_tool.name,
272
+ response_args,
273
+ tool_call_id=f'pyd_ai_tool_call_id__{output_tool.name}',
274
+ )
275
+ ],
276
+ model_name=self._model_name,
277
+ )
258
278
 
259
279
 
260
280
  @dataclass
@@ -293,7 +313,7 @@ class TestStreamedResponse(StreamedResponse):
293
313
  yield self._parts_manager.handle_tool_call_part(
294
314
  vendor_part_id=i, tool_name=part.tool_name, args=part.args, tool_call_id=part.tool_call_id
295
315
  )
296
- elif isinstance(part, (BuiltinToolCallPart, BuiltinToolReturnPart)): # pragma: no cover
316
+ elif isinstance(part, BuiltinToolCallPart | BuiltinToolReturnPart): # pragma: no cover
297
317
  # NOTE: These parts are not generated by TestModel, but we need to handle them for type checking
298
318
  assert False, f'Unexpected part type in TestModel: {type(part).__name__}'
299
319
  elif isinstance(part, ThinkingPart): # pragma: no cover
pydantic_ai/output.py CHANGED
@@ -1,17 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Awaitable, Sequence
3
+ from collections.abc import Awaitable, Callable, Sequence
4
4
  from dataclasses import dataclass
5
- from typing import Any, Callable, Generic, Literal, Union
5
+ from typing import Any, Generic, Literal
6
6
 
7
7
  from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
8
8
  from pydantic.json_schema import JsonSchemaValue
9
9
  from pydantic_core import core_schema
10
- from typing_extensions import TypeAliasType, TypeVar
10
+ from typing_extensions import TypeAliasType, TypeVar, deprecated
11
11
 
12
12
  from . import _utils
13
13
  from .messages import ToolCallPart
14
- from .tools import RunContext, ToolDefinition
14
+ from .tools import DeferredToolRequests, RunContext, ToolDefinition
15
15
 
16
16
  __all__ = (
17
17
  # classes
@@ -42,7 +42,7 @@ StructuredOutputMode = Literal['tool', 'native', 'prompted']
42
42
 
43
43
 
44
44
  OutputTypeOrFunction = TypeAliasType(
45
- 'OutputTypeOrFunction', Union[type[T_co], Callable[..., Union[Awaitable[T_co], T_co]]], type_params=(T_co,)
45
+ 'OutputTypeOrFunction', type[T_co] | Callable[..., Awaitable[T_co] | T_co], type_params=(T_co,)
46
46
  )
47
47
  """Definition of an output type or function.
48
48
 
@@ -54,10 +54,7 @@ See [output docs](../output.md) for more information.
54
54
 
55
55
  TextOutputFunc = TypeAliasType(
56
56
  'TextOutputFunc',
57
- Union[
58
- Callable[[RunContext, str], Union[Awaitable[T_co], T_co]],
59
- Callable[[str], Union[Awaitable[T_co], T_co]],
60
- ],
57
+ Callable[[RunContext, str], Awaitable[T_co] | T_co] | Callable[[str], Awaitable[T_co] | T_co],
61
58
  type_params=(T_co,),
62
59
  )
63
60
  """Definition of a function that will be called to process the model's plain text output. The function must take a single string argument.
@@ -135,10 +132,9 @@ class NativeOutput(Generic[OutputDataT]):
135
132
 
136
133
  Example:
137
134
  ```python {title="native_output.py" requires="tool_output.py"}
138
- from tool_output import Fruit, Vehicle
139
-
140
135
  from pydantic_ai import Agent, NativeOutput
141
136
 
137
+ from tool_output import Fruit, Vehicle
142
138
 
143
139
  agent = Agent(
144
140
  'openai:gpt-4o',
@@ -184,10 +180,11 @@ class PromptedOutput(Generic[OutputDataT]):
184
180
  Example:
185
181
  ```python {title="prompted_output.py" requires="tool_output.py"}
186
182
  from pydantic import BaseModel
187
- from tool_output import Vehicle
188
183
 
189
184
  from pydantic_ai import Agent, PromptedOutput
190
185
 
186
+ from tool_output import Vehicle
187
+
191
188
 
192
189
  class Device(BaseModel):
193
190
  name: str
@@ -286,18 +283,17 @@ def StructuredDict(
286
283
  ```python {title="structured_dict.py"}
287
284
  from pydantic_ai import Agent, StructuredDict
288
285
 
289
-
290
286
  schema = {
291
- "type": "object",
292
- "properties": {
293
- "name": {"type": "string"},
294
- "age": {"type": "integer"}
287
+ 'type': 'object',
288
+ 'properties': {
289
+ 'name': {'type': 'string'},
290
+ 'age': {'type': 'integer'}
295
291
  },
296
- "required": ["name", "age"]
292
+ 'required': ['name', 'age']
297
293
  }
298
294
 
299
295
  agent = Agent('openai:gpt-4o', output_type=StructuredDict(schema))
300
- result = agent.run_sync("Create a person")
296
+ result = agent.run_sync('Create a person')
301
297
  print(result.output)
302
298
  #> {'name': 'John Doe', 'age': 30}
303
299
  ```
@@ -333,16 +329,13 @@ def StructuredDict(
333
329
 
334
330
  _OutputSpecItem = TypeAliasType(
335
331
  '_OutputSpecItem',
336
- Union[OutputTypeOrFunction[T_co], ToolOutput[T_co], NativeOutput[T_co], PromptedOutput[T_co], TextOutput[T_co]],
332
+ OutputTypeOrFunction[T_co] | ToolOutput[T_co] | NativeOutput[T_co] | PromptedOutput[T_co] | TextOutput[T_co],
337
333
  type_params=(T_co,),
338
334
  )
339
335
 
340
336
  OutputSpec = TypeAliasType(
341
337
  'OutputSpec',
342
- Union[
343
- _OutputSpecItem[T_co],
344
- Sequence['OutputSpec[T_co]'],
345
- ],
338
+ _OutputSpecItem[T_co] | Sequence['OutputSpec[T_co]'],
346
339
  type_params=(T_co,),
347
340
  )
348
341
  """Specification of the agent's output data.
@@ -359,12 +352,14 @@ See [output docs](../output.md) for more information.
359
352
  """
360
353
 
361
354
 
362
- @dataclass
363
- class DeferredToolCalls:
364
- """Container for calls of deferred tools. This can be used as an agent's `output_type` and will be used as the output of the agent run if the model called any deferred tools.
365
-
366
- See [deferred toolset docs](../toolsets.md#deferred-toolset) for more information.
367
- """
355
+ @deprecated('`DeferredToolCalls` is deprecated, use `DeferredToolRequests` instead')
356
+ class DeferredToolCalls(DeferredToolRequests): # pragma: no cover
357
+ @property
358
+ @deprecated('`DeferredToolCalls.tool_calls` is deprecated, use `DeferredToolRequests.calls` instead')
359
+ def tool_calls(self) -> list[ToolCallPart]:
360
+ return self.calls
368
361
 
369
- tool_calls: list[ToolCallPart]
370
- tool_defs: dict[str, ToolDefinition]
362
+ @property
363
+ @deprecated('`DeferredToolCalls.tool_defs` is deprecated')
364
+ def tool_defs(self) -> dict[str, ToolDefinition]:
365
+ return {}
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from dataclasses import dataclass, fields, replace
4
5
  from textwrap import dedent
5
- from typing import Callable, Union
6
6
 
7
7
  from typing_extensions import Self
8
8
 
@@ -18,7 +18,7 @@ __all__ = [
18
18
  ]
19
19
 
20
20
 
21
- @dataclass
21
+ @dataclass(kw_only=True)
22
22
  class ModelProfile:
23
23
  """Describes how requests to and responses from specific models or families of models need to be constructed and processed to get the best results, independent of the model and provider classes used."""
24
24
 
@@ -75,6 +75,6 @@ class ModelProfile:
75
75
  return replace(self, **non_default_attrs)
76
76
 
77
77
 
78
- ModelProfileSpec = Union[ModelProfile, Callable[[str], Union[ModelProfile, None]]]
78
+ ModelProfileSpec = ModelProfile | Callable[[str], ModelProfile | None]
79
79
 
80
80
  DEFAULT_PROFILE = ModelProfile()
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from . import ModelProfile
6
6
 
7
7
 
8
- @dataclass
8
+ @dataclass(kw_only=True)
9
9
  class GroqModelProfile(ModelProfile):
10
10
  """Profile for models used with GroqModel.
11
11
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import re
4
+ import warnings
4
5
  from collections.abc import Sequence
5
6
  from dataclasses import dataclass
6
7
  from typing import Any, Literal
@@ -11,7 +12,7 @@ from ._json_schema import JsonSchema, JsonSchemaTransformer
11
12
  OpenAISystemPromptRole = Literal['system', 'developer', 'user']
12
13
 
13
14
 
14
- @dataclass
15
+ @dataclass(kw_only=True)
15
16
  class OpenAIModelProfile(ModelProfile):
16
17
  """Profile for models used with `OpenAIChatModel`.
17
18
 
@@ -21,7 +22,6 @@ class OpenAIModelProfile(ModelProfile):
21
22
  openai_supports_strict_tool_definition: bool = True
22
23
  """This can be set by a provider or user if the OpenAI-"compatible" API doesn't support strict tool definitions."""
23
24
 
24
- # TODO(Marcelo): Deprecate this in favor of `openai_unsupported_model_settings`.
25
25
  openai_supports_sampling_settings: bool = True
26
26
  """Turn off to don't send sampling settings like `temperature` and `top_p` to models that don't support them, like OpenAI's o-series reasoning models."""
27
27
 
@@ -38,6 +38,14 @@ class OpenAIModelProfile(ModelProfile):
38
38
  openai_system_prompt_role: OpenAISystemPromptRole | None = None
39
39
  """The role to use for the system prompt message. If not provided, defaults to `'system'`."""
40
40
 
41
+ def __post_init__(self): # pragma: no cover
42
+ if not self.openai_supports_sampling_settings:
43
+ warnings.warn(
44
+ 'The `openai_supports_sampling_settings` has no effect, and it will be removed in future versions. '
45
+ 'Use `openai_unsupported_model_settings` instead.',
46
+ DeprecationWarning,
47
+ )
48
+
41
49
 
42
50
  def openai_model_profile(model_name: str) -> ModelProfile:
43
51
  """Get the model profile for an OpenAI model."""
@@ -46,6 +54,19 @@ def openai_model_profile(model_name: str) -> ModelProfile:
46
54
  # We leave it in here for all models because the `default_structured_output_mode` is `'tool'`, so `native` is only used
47
55
  # when the user specifically uses the `NativeOutput` marker, so an error from the API is acceptable.
48
56
 
57
+ if is_reasoning_model:
58
+ openai_unsupported_model_settings = (
59
+ 'temperature',
60
+ 'top_p',
61
+ 'presence_penalty',
62
+ 'frequency_penalty',
63
+ 'logit_bias',
64
+ 'logprobs',
65
+ 'top_logprobs',
66
+ )
67
+ else:
68
+ openai_unsupported_model_settings = ()
69
+
49
70
  # The o1-mini model doesn't support the `system` role, so we default to `user`.
50
71
  # See https://github.com/pydantic/pydantic-ai/issues/974 for more details.
51
72
  openai_system_prompt_role = 'user' if model_name.startswith('o1-mini') else None
@@ -54,7 +75,7 @@ def openai_model_profile(model_name: str) -> ModelProfile:
54
75
  json_schema_transformer=OpenAIJsonSchemaTransformer,
55
76
  supports_json_schema_output=True,
56
77
  supports_json_object_output=True,
57
- openai_supports_sampling_settings=not is_reasoning_model,
78
+ openai_unsupported_model_settings=openai_unsupported_model_settings,
58
79
  openai_system_prompt_role=openai_system_prompt_role,
59
80
  )
60
81
 
@@ -89,7 +110,7 @@ _STRICT_COMPATIBLE_STRING_FORMATS = [
89
110
  _sentinel = object()
90
111
 
91
112
 
92
- @dataclass
113
+ @dataclass(init=False)
93
114
  class OpenAIJsonSchemaTransformer(JsonSchemaTransformer):
94
115
  """Recursively handle the schema to make it compatible with OpenAI strict mode.
95
116
 
@@ -135,6 +135,10 @@ def infer_provider_class(provider: str) -> type[Provider[Any]]: # noqa: C901
135
135
  from .github import GitHubProvider
136
136
 
137
137
  return GitHubProvider
138
+ elif provider == 'litellm':
139
+ from .litellm import LiteLLMProvider
140
+
141
+ return LiteLLMProvider
138
142
  else: # pragma: no cover
139
143
  raise ValueError(f'Unknown provider: {provider}')
140
144
 
@@ -1,10 +1,9 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import os
4
- from typing import Union, overload
4
+ from typing import TypeAlias, overload
5
5
 
6
6
  import httpx
7
- from typing_extensions import TypeAlias
8
7
 
9
8
  from pydantic_ai.exceptions import UserError
10
9
  from pydantic_ai.models import cached_async_http_client
@@ -21,7 +20,7 @@ except ImportError as _import_error:
21
20
  ) from _import_error
22
21
 
23
22
 
24
- AsyncAnthropicClient: TypeAlias = Union[AsyncAnthropic, AsyncAnthropicBedrock]
23
+ AsyncAnthropicClient: TypeAlias = AsyncAnthropic | AsyncAnthropicBedrock
25
24
 
26
25
 
27
26
  class AnthropicProvider(Provider[AsyncAnthropicClient]):
@@ -2,8 +2,9 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import os
4
4
  import re
5
+ from collections.abc import Callable
5
6
  from dataclasses import dataclass
6
- from typing import Callable, Literal, overload
7
+ from typing import Literal, overload
7
8
 
8
9
  from pydantic_ai.exceptions import UserError
9
10
  from pydantic_ai.profiles import ModelProfile
@@ -27,7 +28,7 @@ except ImportError as _import_error:
27
28
  ) from _import_error
28
29
 
29
30
 
30
- @dataclass
31
+ @dataclass(kw_only=True)
31
32
  class BedrockModelProfile(ModelProfile):
32
33
  """Profile for models used with BedrockModel.
33
34
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import functools
4
+ from asyncio import Lock
4
5
  from collections.abc import AsyncGenerator, Mapping
5
6
  from pathlib import Path
6
7
  from typing import Literal, overload
@@ -118,7 +119,7 @@ class GoogleVertexProvider(Provider[httpx.AsyncClient]):
118
119
  class _VertexAIAuth(httpx.Auth):
119
120
  """Auth class for Vertex AI API."""
120
121
 
121
- _refresh_lock: anyio.Lock = anyio.Lock()
122
+ _refresh_lock: Lock = Lock()
122
123
 
123
124
  credentials: BaseCredentials | ServiceAccountCredentials | None
124
125
 
@@ -14,6 +14,7 @@ from pydantic_ai.profiles.groq import groq_model_profile
14
14
  from pydantic_ai.profiles.meta import meta_model_profile
15
15
  from pydantic_ai.profiles.mistral import mistral_model_profile
16
16
  from pydantic_ai.profiles.moonshotai import moonshotai_model_profile
17
+ from pydantic_ai.profiles.openai import openai_model_profile
17
18
  from pydantic_ai.profiles.qwen import qwen_model_profile
18
19
  from pydantic_ai.providers import Provider
19
20
 
@@ -26,6 +27,23 @@ except ImportError as _import_error: # pragma: no cover
26
27
  ) from _import_error
27
28
 
28
29
 
30
+ def groq_moonshotai_model_profile(model_name: str) -> ModelProfile | None:
31
+ """Get the model profile for an MoonshotAI model used with the Groq provider."""
32
+ return ModelProfile(supports_json_object_output=True, supports_json_schema_output=True).update(
33
+ moonshotai_model_profile(model_name)
34
+ )
35
+
36
+
37
+ def meta_groq_model_profile(model_name: str) -> ModelProfile | None:
38
+ """Get the model profile for a Meta model used with the Groq provider."""
39
+ if model_name in {'llama-4-maverick-17b-128e-instruct', 'llama-4-scout-17b-16e-instruct'}:
40
+ return ModelProfile(supports_json_object_output=True, supports_json_schema_output=True).update(
41
+ meta_model_profile(model_name)
42
+ )
43
+ else:
44
+ return meta_model_profile(model_name)
45
+
46
+
29
47
  class GroqProvider(Provider[AsyncGroq]):
30
48
  """Provider for Groq API."""
31
49
 
@@ -44,13 +62,14 @@ class GroqProvider(Provider[AsyncGroq]):
44
62
  def model_profile(self, model_name: str) -> ModelProfile | None:
45
63
  prefix_to_profile = {
46
64
  'llama': meta_model_profile,
47
- 'meta-llama/': meta_model_profile,
65
+ 'meta-llama/': meta_groq_model_profile,
48
66
  'gemma': google_model_profile,
49
67
  'qwen': qwen_model_profile,
50
68
  'deepseek': deepseek_model_profile,
51
69
  'mistral': mistral_model_profile,
52
- 'moonshotai/': moonshotai_model_profile,
70
+ 'moonshotai/': groq_moonshotai_model_profile,
53
71
  'compound-': groq_model_profile,
72
+ 'openai/': openai_model_profile,
54
73
  }
55
74
 
56
75
  for prefix, profile_func in prefix_to_profile.items():