pydantic-ai-slim 0.4.11__tar.gz → 0.5.1__tar.gz

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 (101) hide show
  1. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/PKG-INFO +4 -4
  2. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_agent_graph.py +1 -1
  3. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_function_schema.py +7 -4
  4. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/exceptions.py +2 -2
  5. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/messages.py +37 -10
  6. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/anthropic.py +1 -1
  7. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/bedrock.py +1 -1
  8. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/cohere.py +1 -1
  9. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/gemini.py +5 -4
  10. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/google.py +4 -2
  11. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/groq.py +1 -1
  12. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/instrumented.py +2 -6
  13. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/mistral.py +1 -1
  14. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/openai.py +2 -2
  15. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/_json_schema.py +4 -3
  16. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/openai.py +22 -12
  17. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/google.py +9 -6
  18. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/tools.py +13 -5
  19. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pyproject.toml +4 -3
  20. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/.gitignore +0 -0
  21. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/LICENSE +0 -0
  22. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/README.md +0 -0
  23. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/__init__.py +0 -0
  24. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/__main__.py +0 -0
  25. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_a2a.py +0 -0
  26. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_cli.py +0 -0
  27. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_griffe.py +0 -0
  28. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_mcp.py +0 -0
  29. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_output.py +0 -0
  30. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_parts_manager.py +0 -0
  31. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_run_context.py +0 -0
  32. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_system_prompt.py +0 -0
  33. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_thinking_part.py +0 -0
  34. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_tool_manager.py +0 -0
  35. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/_utils.py +0 -0
  36. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/ag_ui.py +0 -0
  37. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/agent.py +0 -0
  38. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/common_tools/__init__.py +0 -0
  39. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  40. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/common_tools/tavily.py +0 -0
  41. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/direct.py +0 -0
  42. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/ext/__init__.py +0 -0
  43. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/ext/aci.py +0 -0
  44. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/ext/langchain.py +0 -0
  45. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/format_as_xml.py +0 -0
  46. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/format_prompt.py +0 -0
  47. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/mcp.py +0 -0
  48. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/__init__.py +0 -0
  49. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/fallback.py +0 -0
  50. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/function.py +0 -0
  51. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/huggingface.py +0 -0
  52. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/mcp_sampling.py +0 -0
  53. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/test.py +0 -0
  54. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/models/wrapper.py +0 -0
  55. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/output.py +0 -0
  56. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/__init__.py +0 -0
  57. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/amazon.py +0 -0
  58. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/anthropic.py +0 -0
  59. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/cohere.py +0 -0
  60. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/deepseek.py +0 -0
  61. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/google.py +0 -0
  62. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/grok.py +0 -0
  63. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/meta.py +0 -0
  64. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/mistral.py +0 -0
  65. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/moonshotai.py +0 -0
  66. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/profiles/qwen.py +0 -0
  67. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/__init__.py +0 -0
  68. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/anthropic.py +0 -0
  69. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/azure.py +0 -0
  70. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/bedrock.py +0 -0
  71. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/cohere.py +0 -0
  72. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/deepseek.py +0 -0
  73. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/fireworks.py +0 -0
  74. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/github.py +0 -0
  75. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/google_gla.py +0 -0
  76. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/google_vertex.py +0 -0
  77. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/grok.py +0 -0
  78. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/groq.py +0 -0
  79. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/heroku.py +0 -0
  80. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/huggingface.py +0 -0
  81. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/mistral.py +0 -0
  82. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/moonshotai.py +0 -0
  83. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/openai.py +0 -0
  84. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/openrouter.py +0 -0
  85. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/together.py +0 -0
  86. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/providers/vercel.py +0 -0
  87. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/py.typed +0 -0
  88. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/result.py +0 -0
  89. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/retries.py +0 -0
  90. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/settings.py +0 -0
  91. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/__init__.py +0 -0
  92. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/abstract.py +0 -0
  93. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/combined.py +0 -0
  94. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/deferred.py +0 -0
  95. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/filtered.py +0 -0
  96. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/function.py +0 -0
  97. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/prefixed.py +0 -0
  98. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/prepared.py +0 -0
  99. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/renamed.py +0 -0
  100. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/toolsets/wrapper.py +0 -0
  101. {pydantic_ai_slim-0.4.11 → pydantic_ai_slim-0.5.1}/pydantic_ai/usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.4.11
3
+ Version: 0.5.1
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
5
5
  Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>, Douwe Maan <douwe@pydantic.dev>
6
6
  License-Expression: MIT
@@ -30,7 +30,7 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
30
30
  Requires-Dist: griffe>=1.3.2
31
31
  Requires-Dist: httpx>=0.27
32
32
  Requires-Dist: opentelemetry-api>=1.28.0
33
- Requires-Dist: pydantic-graph==0.4.11
33
+ Requires-Dist: pydantic-graph==0.5.1
34
34
  Requires-Dist: pydantic>=2.10
35
35
  Requires-Dist: typing-inspection>=0.4.0
36
36
  Provides-Extra: a2a
@@ -51,9 +51,9 @@ Requires-Dist: cohere>=5.16.0; (platform_system != 'Emscripten') and extra == 'c
51
51
  Provides-Extra: duckduckgo
52
52
  Requires-Dist: ddgs>=9.0.0; extra == 'duckduckgo'
53
53
  Provides-Extra: evals
54
- Requires-Dist: pydantic-evals==0.4.11; extra == 'evals'
54
+ Requires-Dist: pydantic-evals==0.5.1; extra == 'evals'
55
55
  Provides-Extra: google
56
- Requires-Dist: google-genai>=1.24.0; extra == 'google'
56
+ Requires-Dist: google-genai>=1.28.0; extra == 'google'
57
57
  Provides-Extra: groq
58
58
  Requires-Dist: groq>=0.19.0; extra == 'groq'
59
59
  Provides-Extra: huggingface
@@ -620,7 +620,7 @@ async def process_function_tools( # noqa: C901
620
620
  result_data = await tool_manager.handle_call(call)
621
621
  except exceptions.UnexpectedModelBehavior as e:
622
622
  ctx.state.increment_retries(ctx.deps.max_result_retries, e)
623
- raise e # pragma: no cover
623
+ raise e # pragma: lax no cover
624
624
  except ToolRetryError as e:
625
625
  ctx.state.increment_retries(ctx.deps.max_result_retries, e)
626
626
  yield _messages.FunctionToolCallEvent(call)
@@ -154,9 +154,13 @@ def function_schema( # noqa: C901
154
154
  if p.kind == Parameter.VAR_POSITIONAL:
155
155
  annotation = list[annotation]
156
156
 
157
- # FieldInfo.from_annotation expects a type, `annotation` is Any
157
+ required = p.default is Parameter.empty
158
+ # FieldInfo.from_annotated_attribute expects a type, `annotation` is Any
158
159
  annotation = cast(type[Any], annotation)
159
- field_info = FieldInfo.from_annotation(annotation)
160
+ if required:
161
+ field_info = FieldInfo.from_annotation(annotation)
162
+ else:
163
+ field_info = FieldInfo.from_annotated_attribute(annotation, p.default)
160
164
  if field_info.description is None:
161
165
  field_info.description = field_descriptions.get(field_name)
162
166
 
@@ -164,7 +168,7 @@ def function_schema( # noqa: C901
164
168
  field_name,
165
169
  field_info,
166
170
  decorators,
167
- required=p.default is Parameter.empty,
171
+ required=required,
168
172
  )
169
173
  # noinspection PyTypeChecker
170
174
  td_schema.setdefault('metadata', {})['is_model_like'] = is_model_like(annotation)
@@ -281,7 +285,6 @@ def _build_schema(
281
285
  td_schema = core_schema.typed_dict_schema(
282
286
  fields,
283
287
  config=core_config,
284
- total=var_kwargs_schema is None,
285
288
  extras_schema=gen_schema.generate_schema(var_kwargs_schema) if var_kwargs_schema else None,
286
289
  )
287
290
  return td_schema, None
@@ -5,9 +5,9 @@ import sys
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  if sys.version_info < (3, 11):
8
- from exceptiongroup import ExceptionGroup
8
+ from exceptiongroup import ExceptionGroup as ExceptionGroup # pragma: lax no cover
9
9
  else:
10
- ExceptionGroup = ExceptionGroup
10
+ ExceptionGroup = ExceptionGroup # pragma: lax no cover
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from .messages import RetryPromptPart
@@ -106,7 +106,7 @@ class FileUrl(ABC):
106
106
  - `GoogleModel`: `VideoUrl.vendor_metadata` is used as `video_metadata`: https://ai.google.dev/gemini-api/docs/video-understanding#customize-video-processing
107
107
  """
108
108
 
109
- _media_type: str | None = field(init=False, repr=False)
109
+ _media_type: str | None = field(init=False, repr=False, compare=False)
110
110
 
111
111
  def __init__(
112
112
  self,
@@ -120,19 +120,21 @@ class FileUrl(ABC):
120
120
  self.force_download = force_download
121
121
  self._media_type = media_type
122
122
 
123
- @abstractmethod
124
- def _infer_media_type(self) -> str:
125
- """Return the media type of the file, based on the url."""
126
-
127
123
  @property
128
124
  def media_type(self) -> str:
129
- """Return the media type of the file, based on the url or the provided `_media_type`."""
125
+ """Return the media type of the file, based on the URL or the provided `media_type`."""
130
126
  return self._media_type or self._infer_media_type()
131
127
 
128
+ @abstractmethod
129
+ def _infer_media_type(self) -> str:
130
+ """Infer the media type of the file based on the URL."""
131
+ raise NotImplementedError
132
+
132
133
  @property
133
134
  @abstractmethod
134
135
  def format(self) -> str:
135
136
  """The file format."""
137
+ raise NotImplementedError
136
138
 
137
139
  __repr__ = _utils.dataclasses_no_defaults_repr
138
140
 
@@ -182,7 +184,9 @@ class VideoUrl(FileUrl):
182
184
  elif self.is_youtube:
183
185
  return 'video/mp4'
184
186
  else:
185
- raise ValueError(f'Unknown video file extension: {self.url}')
187
+ raise ValueError(
188
+ f'Could not infer media type from video URL: {self.url}. Explicitly provide a `media_type` instead.'
189
+ )
186
190
 
187
191
  @property
188
192
  def is_youtube(self) -> bool:
@@ -238,7 +242,9 @@ class AudioUrl(FileUrl):
238
242
  if self.url.endswith('.aac'):
239
243
  return 'audio/aac'
240
244
 
241
- raise ValueError(f'Unknown audio file extension: {self.url}')
245
+ raise ValueError(
246
+ f'Could not infer media type from audio URL: {self.url}. Explicitly provide a `media_type` instead.'
247
+ )
242
248
 
243
249
  @property
244
250
  def format(self) -> AudioFormat:
@@ -278,7 +284,9 @@ class ImageUrl(FileUrl):
278
284
  elif self.url.endswith('.webp'):
279
285
  return 'image/webp'
280
286
  else:
281
- raise ValueError(f'Unknown image file extension: {self.url}')
287
+ raise ValueError(
288
+ f'Could not infer media type from image URL: {self.url}. Explicitly provide a `media_type` instead.'
289
+ )
282
290
 
283
291
  @property
284
292
  def format(self) -> ImageFormat:
@@ -312,9 +320,28 @@ class DocumentUrl(FileUrl):
312
320
 
313
321
  def _infer_media_type(self) -> str:
314
322
  """Return the media type of the document, based on the url."""
323
+ # Common document types are hardcoded here as mime-type support for these
324
+ # extensions varies across operating systems.
325
+ if self.url.endswith(('.md', '.mdx', '.markdown')):
326
+ return 'text/markdown'
327
+ elif self.url.endswith('.asciidoc'):
328
+ return 'text/x-asciidoc'
329
+ elif self.url.endswith('.txt'):
330
+ return 'text/plain'
331
+ elif self.url.endswith('.pdf'):
332
+ return 'application/pdf'
333
+ elif self.url.endswith('.rtf'):
334
+ return 'application/rtf'
335
+ elif self.url.endswith('.docx'):
336
+ return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
337
+ elif self.url.endswith('.xlsx'):
338
+ return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
339
+
315
340
  type_, _ = guess_type(self.url)
316
341
  if type_ is None:
317
- raise ValueError(f'Unknown document file extension: {self.url}')
342
+ raise ValueError(
343
+ f'Could not infer media type from document URL: {self.url}. Explicitly provide a `media_type` instead.'
344
+ )
318
345
  return type_
319
346
 
320
347
  @property
@@ -256,7 +256,7 @@ class AnthropicModel(Model):
256
256
  except APIStatusError as e:
257
257
  if (status_code := e.status_code) >= 400:
258
258
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
259
- raise # pragma: no cover
259
+ raise # pragma: lax no cover
260
260
 
261
261
  def _process_response(self, response: BetaMessage) -> ModelResponse:
262
262
  """Process a non-streamed response, and prepare a message to return."""
@@ -665,4 +665,4 @@ class _AsyncIteratorWrapper(Generic[T]):
665
665
  if type(e.__cause__) is StopIteration:
666
666
  raise StopAsyncIteration
667
667
  else:
668
- raise e # pragma: no cover
668
+ raise e # pragma: lax no cover
@@ -183,7 +183,7 @@ class CohereModel(Model):
183
183
  except ApiError as e:
184
184
  if (status_code := e.status_code) and status_code >= 400:
185
185
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
186
- raise # pragma: no cover
186
+ raise # pragma: lax no cover
187
187
 
188
188
  def _process_response(self, response: V2ChatResponse) -> ModelResponse:
189
189
  """Process a non-streamed response, and prepare a message to return."""
@@ -11,7 +11,7 @@ from uuid import uuid4
11
11
  import httpx
12
12
  import pydantic
13
13
  from httpx import USE_CLIENT_DEFAULT, Response as HTTPResponse
14
- from typing_extensions import NotRequired, TypedDict, assert_never
14
+ from typing_extensions import NotRequired, TypedDict, assert_never, deprecated
15
15
 
16
16
  from pydantic_ai.providers import Provider, infer_provider
17
17
 
@@ -92,6 +92,7 @@ class GeminiModelSettings(ModelSettings, total=False):
92
92
  """
93
93
 
94
94
 
95
+ @deprecated('Use `GoogleModel` instead. See <https://ai.pydantic.dev/models/google/> for more details.')
95
96
  @dataclass(init=False)
96
97
  class GeminiModel(Model):
97
98
  """A model that uses Gemini via `generativelanguage.googleapis.com` API.
@@ -236,7 +237,7 @@ class GeminiModel(Model):
236
237
 
237
238
  if gemini_labels := model_settings.get('gemini_labels'):
238
239
  if self._system == 'google-vertex':
239
- request_data['labels'] = gemini_labels
240
+ request_data['labels'] = gemini_labels # pragma: lax no cover
240
241
 
241
242
  headers = {'Content-Type': 'application/json', 'User-Agent': get_user_agent()}
242
243
  url = f'/{self._model_name}:{"streamGenerateContent" if streamed else "generateContent"}'
@@ -366,11 +367,11 @@ class GeminiModel(Model):
366
367
  inline_data={'data': downloaded_item['data'], 'mime_type': downloaded_item['data_type']}
367
368
  )
368
369
  content.append(inline_data)
369
- else:
370
+ else: # pragma: lax no cover
370
371
  file_data = _GeminiFileDataPart(file_data={'file_uri': item.url, 'mime_type': item.media_type})
371
372
  content.append(file_data)
372
373
  else:
373
- assert_never(item)
374
+ assert_never(item) # pragma: lax no cover
374
375
  return content
375
376
 
376
377
  def _map_response_schema(self, o: OutputObjectDefinition) -> dict[str, Any]:
@@ -407,7 +407,7 @@ class GoogleModel(Model):
407
407
  content.append(inline_data_dict) # type: ignore
408
408
  elif isinstance(item, VideoUrl) and item.is_youtube:
409
409
  file_data_dict = {'file_data': {'file_uri': item.url, 'mime_type': item.media_type}}
410
- if item.vendor_metadata:
410
+ if item.vendor_metadata: # pragma: no branch
411
411
  file_data_dict['video_metadata'] = item.vendor_metadata
412
412
  content.append(file_data_dict) # type: ignore
413
413
  elif isinstance(item, FileUrl):
@@ -421,7 +421,9 @@ class GoogleModel(Model):
421
421
  inline_data = {'data': downloaded_item['data'], 'mime_type': downloaded_item['data_type']}
422
422
  content.append({'inline_data': inline_data}) # type: ignore
423
423
  else:
424
- content.append({'file_data': {'file_uri': item.url, 'mime_type': item.media_type}})
424
+ content.append(
425
+ {'file_data': {'file_uri': item.url, 'mime_type': item.media_type}}
426
+ ) # pragma: lax no cover
425
427
  else:
426
428
  assert_never(item)
427
429
  return content
@@ -249,7 +249,7 @@ class GroqModel(Model):
249
249
  except APIStatusError as e:
250
250
  if (status_code := e.status_code) >= 400:
251
251
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
252
- raise # pragma: no cover
252
+ raise # pragma: lax no cover
253
253
 
254
254
  def _process_response(self, response: chat.ChatCompletion) -> ModelResponse:
255
255
  """Process a non-streamed response, and prepare a message to return."""
@@ -18,11 +18,7 @@ from opentelemetry.trace import Span, Tracer, TracerProvider, get_tracer_provide
18
18
  from opentelemetry.util.types import AttributeValue
19
19
  from pydantic import TypeAdapter
20
20
 
21
- from ..messages import (
22
- ModelMessage,
23
- ModelRequest,
24
- ModelResponse,
25
- )
21
+ from ..messages import ModelMessage, ModelRequest, ModelResponse
26
22
  from ..settings import ModelSettings
27
23
  from . import KnownModelName, Model, ModelRequestParameters, StreamedResponse
28
24
  from .wrapper import WrapperModel
@@ -138,7 +134,7 @@ class InstrumentationSettings:
138
134
  **tokens_histogram_kwargs,
139
135
  explicit_bucket_boundaries_advisory=TOKEN_HISTOGRAM_BOUNDARIES,
140
136
  )
141
- except TypeError:
137
+ except TypeError: # pragma: lax no cover
142
138
  # Older OTel/logfire versions don't support explicit_bucket_boundaries_advisory
143
139
  self.tokens_histogram = self.meter.create_histogram(
144
140
  **tokens_histogram_kwargs, # pyright: ignore
@@ -218,7 +218,7 @@ class MistralModel(Model):
218
218
  except SDKError as e:
219
219
  if (status_code := e.status_code) >= 400:
220
220
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
221
- raise # pragma: no cover
221
+ raise # pragma: lax no cover
222
222
 
223
223
  assert response, 'A unexpected empty response from Mistral.'
224
224
  return response
@@ -359,7 +359,7 @@ class OpenAIModel(Model):
359
359
  except APIStatusError as e:
360
360
  if (status_code := e.status_code) >= 400:
361
361
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
362
- raise # pragma: no cover
362
+ raise # pragma: lax no cover
363
363
 
364
364
  def _process_response(self, response: chat.ChatCompletion | str) -> ModelResponse:
365
365
  """Process a non-streamed response, and prepare a message to return."""
@@ -814,7 +814,7 @@ class OpenAIResponsesModel(Model):
814
814
  except APIStatusError as e:
815
815
  if (status_code := e.status_code) >= 400:
816
816
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
817
- raise # pragma: no cover
817
+ raise # pragma: lax no cover
818
818
 
819
819
  def _get_reasoning(self, model_settings: OpenAIResponsesModelSettings) -> Reasoning | NotGiven:
820
820
  reasoning_effort = model_settings.get('openai_reasoning_effort', None)
@@ -137,8 +137,9 @@ class JsonSchemaTransformer(ABC):
137
137
  return schema
138
138
 
139
139
  def _handle_union(self, schema: JsonSchema, union_kind: Literal['anyOf', 'oneOf']) -> JsonSchema:
140
- members = schema.get(union_kind)
141
- if not members:
140
+ try:
141
+ members = schema.pop(union_kind)
142
+ except KeyError:
142
143
  return schema
143
144
 
144
145
  handled = [self._handle(member) for member in members]
@@ -149,7 +150,7 @@ class JsonSchemaTransformer(ABC):
149
150
 
150
151
  if len(handled) == 1:
151
152
  # In this case, no need to retain the union
152
- return handled[0]
153
+ return handled[0] | schema
153
154
 
154
155
  # If we have keys besides the union kind (such as title or discriminator), keep them without modifications
155
156
  schema = schema.copy()
@@ -47,11 +47,6 @@ def openai_model_profile(model_name: str) -> ModelProfile:
47
47
  _STRICT_INCOMPATIBLE_KEYS = [
48
48
  'minLength',
49
49
  'maxLength',
50
- 'pattern',
51
- 'format',
52
- 'minimum',
53
- 'maximum',
54
- 'multipleOf',
55
50
  'patternProperties',
56
51
  'unevaluatedProperties',
57
52
  'propertyNames',
@@ -61,11 +56,21 @@ _STRICT_INCOMPATIBLE_KEYS = [
61
56
  'contains',
62
57
  'minContains',
63
58
  'maxContains',
64
- 'minItems',
65
- 'maxItems',
66
59
  'uniqueItems',
67
60
  ]
68
61
 
62
+ _STRICT_COMPATIBLE_STRING_FORMATS = [
63
+ 'date-time',
64
+ 'time',
65
+ 'date',
66
+ 'duration',
67
+ 'email',
68
+ 'hostname',
69
+ 'ipv4',
70
+ 'ipv6',
71
+ 'uuid',
72
+ ]
73
+
69
74
  _sentinel = object()
70
75
 
71
76
 
@@ -127,6 +132,9 @@ class OpenAIJsonSchemaTransformer(JsonSchemaTransformer):
127
132
  value = schema.get(key, _sentinel)
128
133
  if value is not _sentinel:
129
134
  incompatible_values[key] = value
135
+ if format := schema.get('format'):
136
+ if format not in _STRICT_COMPATIBLE_STRING_FORMATS:
137
+ incompatible_values['format'] = format
130
138
  description = schema.get('description')
131
139
  if incompatible_values:
132
140
  if self.strict is True:
@@ -158,11 +166,13 @@ class OpenAIJsonSchemaTransformer(JsonSchemaTransformer):
158
166
  schema['required'] = list(schema['properties'].keys())
159
167
 
160
168
  elif self.strict is None:
161
- if (
162
- schema.get('additionalProperties') is not False
163
- or 'properties' not in schema
164
- or 'required' not in schema
165
- ):
169
+ if schema.get('additionalProperties', None) not in (None, False):
170
+ self.is_strict_compatible = False
171
+ else:
172
+ # additional properties are disallowed by default
173
+ schema['additionalProperties'] = False
174
+
175
+ if 'properties' not in schema or 'required' not in schema:
166
176
  self.is_strict_compatible = False
167
177
  else:
168
178
  required = schema['required']
@@ -3,6 +3,8 @@ from __future__ import annotations as _annotations
3
3
  import os
4
4
  from typing import Literal, overload
5
5
 
6
+ import httpx
7
+
6
8
  from pydantic_ai.exceptions import UserError
7
9
  from pydantic_ai.models import get_user_agent
8
10
  from pydantic_ai.profiles import ModelProfile
@@ -12,6 +14,7 @@ from pydantic_ai.providers import Provider
12
14
  try:
13
15
  from google import genai
14
16
  from google.auth.credentials import Credentials
17
+ from google.genai.types import HttpOptionsDict
15
18
  except ImportError as _import_error:
16
19
  raise ImportError(
17
20
  'Please install the `google-genai` package to use the Google provider, '
@@ -89,17 +92,17 @@ class GoogleProvider(Provider[genai.Client]):
89
92
  if vertexai is None:
90
93
  vertexai = bool(location or project or credentials)
91
94
 
95
+ http_options: HttpOptionsDict = {
96
+ 'headers': {'User-Agent': get_user_agent()},
97
+ 'async_client_args': {'transport': httpx.AsyncHTTPTransport()},
98
+ }
92
99
  if not vertexai:
93
100
  if api_key is None:
94
101
  raise UserError( # pragma: no cover
95
102
  'Set the `GOOGLE_API_KEY` environment variable or pass it via `GoogleProvider(api_key=...)`'
96
103
  'to use the Google Generative Language API.'
97
104
  )
98
- self._client = genai.Client(
99
- vertexai=vertexai,
100
- api_key=api_key,
101
- http_options={'headers': {'User-Agent': get_user_agent()}},
102
- )
105
+ self._client = genai.Client(vertexai=vertexai, api_key=api_key, http_options=http_options)
103
106
  else:
104
107
  self._client = genai.Client(
105
108
  vertexai=vertexai,
@@ -111,7 +114,7 @@ class GoogleProvider(Provider[genai.Client]):
111
114
  # For more details, check: https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions
112
115
  location=location or os.environ.get('GOOGLE_CLOUD_LOCATION') or 'us-central1',
113
116
  credentials=credentials,
114
- http_options={'headers': {'User-Agent': get_user_agent()}},
117
+ http_options=http_options,
115
118
  )
116
119
  else:
117
120
  self._client = client
@@ -133,11 +133,19 @@ A = TypeVar('A')
133
133
 
134
134
  class GenerateToolJsonSchema(GenerateJsonSchema):
135
135
  def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue:
136
- s = super().typed_dict_schema(schema)
137
- total = schema.get('total')
138
- if 'additionalProperties' not in s and (total is True or total is None):
139
- s['additionalProperties'] = False
140
- return s
136
+ json_schema = super().typed_dict_schema(schema)
137
+ # Workaround for https://github.com/pydantic/pydantic/issues/12123
138
+ if 'additionalProperties' not in json_schema: # pragma: no branch
139
+ extra = schema.get('extra_behavior') or schema.get('config', {}).get('extra_fields_behavior')
140
+ if extra == 'allow':
141
+ extras_schema = schema.get('extras_schema', None)
142
+ if extras_schema is not None:
143
+ json_schema['additionalProperties'] = self.generate_inner(extras_schema) or True
144
+ else:
145
+ json_schema['additionalProperties'] = True # pragma: no cover
146
+ elif extra == 'forbid':
147
+ json_schema['additionalProperties'] = False
148
+ return json_schema
141
149
 
142
150
  def _named_required_fields_schema(self, named_required_fields: Sequence[tuple[str, bool, Any]]) -> JsonSchemaValue:
143
151
  # Remove largely-useless property titles
@@ -65,7 +65,7 @@ logfire = ["logfire>=3.11.0"]
65
65
  openai = ["openai>=1.92.0"]
66
66
  cohere = ["cohere>=5.16.0; platform_system != 'Emscripten'"]
67
67
  vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"]
68
- google = ["google-genai>=1.24.0"]
68
+ google = ["google-genai>=1.28.0"]
69
69
  anthropic = ["anthropic>=0.52.0"]
70
70
  groq = ["groq>=0.19.0"]
71
71
  mistral = ["mistralai>=1.9.2"]
@@ -92,7 +92,7 @@ dev = [
92
92
  "anyio>=4.5.0",
93
93
  "asgi-lifespan>=2.1.0",
94
94
  "devtools>=0.12.2",
95
- "coverage[toml]>=7.6.2",
95
+ "coverage[toml]>=7.10.2",
96
96
  "dirty-equals>=0.9.0",
97
97
  "duckduckgo-search>=7.0.0",
98
98
  "inline-snapshot>=0.19.3",
@@ -103,8 +103,9 @@ dev = [
103
103
  "pytest-recording>=0.13.2",
104
104
  "diff-cover>=9.2.0",
105
105
  "boto3-stubs[bedrock-runtime]",
106
- "strict-no-cover>=0.1.1",
106
+ "strict-no-cover @ git+https://github.com/pydantic/strict-no-cover.git@7fc59da2c4dff919db2095a0f0e47101b657131d",
107
107
  "pytest-xdist>=3.6.1",
108
+ "coverage-enable-subprocess>=0.1.0",
108
109
  ]
109
110
 
110
111
  [tool.hatch.metadata]