pydantic-ai-slim 0.7.0__tar.gz → 0.7.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 (115) hide show
  1. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/PKG-INFO +4 -4
  2. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_agent_graph.py +12 -5
  3. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/__init__.py +28 -0
  4. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/fallback.py +7 -2
  5. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/google.py +66 -6
  6. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/openai.py +28 -4
  7. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/usage.py +17 -1
  8. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pyproject.toml +1 -1
  9. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/.gitignore +0 -0
  10. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/LICENSE +0 -0
  11. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/README.md +0 -0
  12. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/__init__.py +0 -0
  13. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/__main__.py +0 -0
  14. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_a2a.py +0 -0
  15. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_cli.py +0 -0
  16. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_function_schema.py +0 -0
  17. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_griffe.py +0 -0
  18. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_mcp.py +0 -0
  19. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_output.py +0 -0
  20. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_parts_manager.py +0 -0
  21. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_run_context.py +0 -0
  22. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_system_prompt.py +0 -0
  23. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_thinking_part.py +0 -0
  24. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_tool_manager.py +0 -0
  25. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/_utils.py +0 -0
  26. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/ag_ui.py +0 -0
  27. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/agent/__init__.py +0 -0
  28. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/agent/abstract.py +0 -0
  29. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/agent/wrapper.py +0 -0
  30. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/builtin_tools.py +0 -0
  31. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/common_tools/__init__.py +0 -0
  32. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  33. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/common_tools/tavily.py +0 -0
  34. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/direct.py +0 -0
  35. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/__init__.py +0 -0
  36. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
  37. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
  38. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
  39. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  40. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  41. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_model.py +0 -0
  42. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_run_context.py +0 -0
  43. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  44. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/exceptions.py +0 -0
  45. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/ext/__init__.py +0 -0
  46. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/ext/aci.py +0 -0
  47. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/ext/langchain.py +0 -0
  48. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/format_prompt.py +0 -0
  49. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/mcp.py +0 -0
  50. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/messages.py +0 -0
  51. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/anthropic.py +0 -0
  52. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/bedrock.py +0 -0
  53. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/cohere.py +0 -0
  54. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/function.py +0 -0
  55. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/gemini.py +0 -0
  56. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/groq.py +0 -0
  57. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/huggingface.py +0 -0
  58. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/instrumented.py +0 -0
  59. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/mcp_sampling.py +0 -0
  60. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/mistral.py +0 -0
  61. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/test.py +0 -0
  62. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/models/wrapper.py +0 -0
  63. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/output.py +0 -0
  64. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/__init__.py +0 -0
  65. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/_json_schema.py +0 -0
  66. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/amazon.py +0 -0
  67. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/anthropic.py +0 -0
  68. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/cohere.py +0 -0
  69. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/deepseek.py +0 -0
  70. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/google.py +0 -0
  71. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/grok.py +0 -0
  72. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/groq.py +0 -0
  73. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/meta.py +0 -0
  74. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/mistral.py +0 -0
  75. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/moonshotai.py +0 -0
  76. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/openai.py +0 -0
  77. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/profiles/qwen.py +0 -0
  78. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/__init__.py +0 -0
  79. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/anthropic.py +0 -0
  80. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/azure.py +0 -0
  81. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/bedrock.py +0 -0
  82. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/cohere.py +0 -0
  83. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/deepseek.py +0 -0
  84. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/fireworks.py +0 -0
  85. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/github.py +0 -0
  86. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/google.py +0 -0
  87. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/google_gla.py +0 -0
  88. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/google_vertex.py +0 -0
  89. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/grok.py +0 -0
  90. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/groq.py +0 -0
  91. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/heroku.py +0 -0
  92. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/huggingface.py +0 -0
  93. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/mistral.py +0 -0
  94. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/moonshotai.py +0 -0
  95. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/openai.py +0 -0
  96. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/openrouter.py +0 -0
  97. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/together.py +0 -0
  98. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/providers/vercel.py +0 -0
  99. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/py.typed +0 -0
  100. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/result.py +0 -0
  101. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/retries.py +0 -0
  102. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/run.py +0 -0
  103. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/settings.py +0 -0
  104. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/tools.py +0 -0
  105. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/__init__.py +0 -0
  106. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/_dynamic.py +0 -0
  107. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/abstract.py +0 -0
  108. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/combined.py +0 -0
  109. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/deferred.py +0 -0
  110. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/filtered.py +0 -0
  111. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/function.py +0 -0
  112. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/prefixed.py +0 -0
  113. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/prepared.py +0 -0
  114. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/renamed.py +0 -0
  115. {pydantic_ai_slim-0.7.0 → pydantic_ai_slim-0.7.1}/pydantic_ai/toolsets/wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.7.0
3
+ Version: 0.7.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.7.0
33
+ Requires-Dist: pydantic-graph==0.7.1
34
34
  Requires-Dist: pydantic>=2.10
35
35
  Requires-Dist: typing-inspection>=0.4.0
36
36
  Provides-Extra: a2a
@@ -51,7 +51,7 @@ 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.7.0; extra == 'evals'
54
+ Requires-Dist: pydantic-evals==0.7.1; extra == 'evals'
55
55
  Provides-Extra: google
56
56
  Requires-Dist: google-genai>=1.28.0; extra == 'google'
57
57
  Provides-Extra: groq
@@ -65,7 +65,7 @@ Requires-Dist: mcp>=1.10.0; (python_version >= '3.10') and extra == 'mcp'
65
65
  Provides-Extra: mistral
66
66
  Requires-Dist: mistralai>=1.9.2; extra == 'mistral'
67
67
  Provides-Extra: openai
68
- Requires-Dist: openai>=1.92.0; extra == 'openai'
68
+ Requires-Dist: openai>=1.99.9; extra == 'openai'
69
69
  Provides-Extra: retries
70
70
  Requires-Dist: tenacity>=8.2.3; extra == 'retries'
71
71
  Provides-Extra: tavily
@@ -351,11 +351,6 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
351
351
  ) -> tuple[ModelSettings | None, models.ModelRequestParameters, list[_messages.ModelMessage], RunContext[DepsT]]:
352
352
  ctx.state.message_history.append(self.request)
353
353
 
354
- # Check usage
355
- if ctx.deps.usage_limits: # pragma: no branch
356
- ctx.deps.usage_limits.check_before_request(ctx.state.usage)
357
-
358
- # Increment run_step
359
354
  ctx.state.run_step += 1
360
355
 
361
356
  run_context = build_run_context(ctx)
@@ -367,6 +362,18 @@ class ModelRequestNode(AgentNode[DepsT, NodeRunEndT]):
367
362
 
368
363
  message_history = await _process_message_history(ctx.state, ctx.deps.history_processors, run_context)
369
364
 
365
+ usage = ctx.state.usage
366
+ if ctx.deps.usage_limits.count_tokens_before_request:
367
+ # Copy to avoid modifying the original usage object with the counted usage
368
+ usage = dataclasses.replace(usage)
369
+
370
+ counted_usage = await ctx.deps.model.count_tokens(
371
+ message_history, ctx.deps.model_settings, model_request_parameters
372
+ )
373
+ usage.incr(counted_usage)
374
+
375
+ ctx.deps.usage_limits.check_before_request(usage)
376
+
370
377
  return model_settings, model_request_parameters, message_history, run_context
371
378
 
372
379
  def _finish_handling(
@@ -194,6 +194,13 @@ KnownModelName = TypeAliasType(
194
194
  'gpt-4o-mini-search-preview-2025-03-11',
195
195
  'gpt-4o-search-preview',
196
196
  'gpt-4o-search-preview-2025-03-11',
197
+ 'gpt-5',
198
+ 'gpt-5-2025-08-07',
199
+ 'gpt-5-chat-latest',
200
+ 'gpt-5-mini',
201
+ 'gpt-5-mini-2025-08-07',
202
+ 'gpt-5-nano',
203
+ 'gpt-5-nano-2025-08-07',
197
204
  'grok:grok-4',
198
205
  'grok:grok-4-0709',
199
206
  'grok:grok-3',
@@ -313,11 +320,18 @@ KnownModelName = TypeAliasType(
313
320
  'openai:gpt-4o-mini-search-preview-2025-03-11',
314
321
  'openai:gpt-4o-search-preview',
315
322
  'openai:gpt-4o-search-preview-2025-03-11',
323
+ 'openai:gpt-5',
324
+ 'openai:gpt-5-2025-08-07',
316
325
  'openai:o1',
326
+ 'openai:gpt-5-chat-latest',
317
327
  'openai:o1-2024-12-17',
328
+ 'openai:gpt-5-mini',
318
329
  'openai:o1-mini',
330
+ 'openai:gpt-5-mini-2025-08-07',
319
331
  'openai:o1-mini-2024-09-12',
332
+ 'openai:gpt-5-nano',
320
333
  'openai:o1-preview',
334
+ 'openai:gpt-5-nano-2025-08-07',
321
335
  'openai:o1-preview-2024-09-12',
322
336
  'openai:o1-pro',
323
337
  'openai:o1-pro-2025-03-19',
@@ -399,6 +413,16 @@ class Model(ABC):
399
413
  """Make a request to the model."""
400
414
  raise NotImplementedError()
401
415
 
416
+ async def count_tokens(
417
+ self,
418
+ messages: list[ModelMessage],
419
+ model_settings: ModelSettings | None,
420
+ model_request_parameters: ModelRequestParameters,
421
+ ) -> Usage:
422
+ """Make a request to the model for counting tokens."""
423
+ # This method is not required, but you need to implement it if you want to support `UsageLimits.count_tokens_before_request`.
424
+ raise NotImplementedError(f'Token counting ahead of the request is not supported by {self.__class__.__name__}')
425
+
402
426
  @asynccontextmanager
403
427
  async def request_stream(
404
428
  self,
@@ -679,6 +703,10 @@ def infer_model(model: Model | KnownModelName | str) -> Model: # noqa: C901
679
703
  from .openai import OpenAIModel
680
704
 
681
705
  return OpenAIModel(model_name, provider=provider)
706
+ elif provider == 'openai-responses':
707
+ from .openai import OpenAIResponsesModel
708
+
709
+ return OpenAIResponsesModel(model_name, provider='openai')
682
710
  elif provider in ('google-gla', 'google-vertex'):
683
711
  from .google import GoogleModel
684
712
 
@@ -11,6 +11,7 @@ from pydantic_ai._run_context import RunContext
11
11
  from pydantic_ai.models.instrumented import InstrumentedModel
12
12
 
13
13
  from ..exceptions import FallbackExceptionGroup, ModelHTTPError
14
+ from ..settings import merge_model_settings
14
15
  from . import KnownModelName, Model, ModelRequestParameters, StreamedResponse, infer_model
15
16
 
16
17
  if TYPE_CHECKING:
@@ -65,8 +66,9 @@ class FallbackModel(Model):
65
66
 
66
67
  for model in self.models:
67
68
  customized_model_request_parameters = model.customize_request_parameters(model_request_parameters)
69
+ merged_settings = merge_model_settings(model.settings, model_settings)
68
70
  try:
69
- response = await model.request(messages, model_settings, customized_model_request_parameters)
71
+ response = await model.request(messages, merged_settings, customized_model_request_parameters)
70
72
  except Exception as exc:
71
73
  if self._fallback_on(exc):
72
74
  exceptions.append(exc)
@@ -91,10 +93,13 @@ class FallbackModel(Model):
91
93
 
92
94
  for model in self.models:
93
95
  customized_model_request_parameters = model.customize_request_parameters(model_request_parameters)
96
+ merged_settings = merge_model_settings(model.settings, model_settings)
94
97
  async with AsyncExitStack() as stack:
95
98
  try:
96
99
  response = await stack.enter_async_context(
97
- model.request_stream(messages, model_settings, customized_model_request_parameters, run_context)
100
+ model.request_stream(
101
+ messages, merged_settings, customized_model_request_parameters, run_context
102
+ )
98
103
  )
99
104
  except Exception as exc:
100
105
  if self._fallback_on(exc):
@@ -52,6 +52,7 @@ try:
52
52
  from google.genai.types import (
53
53
  ContentDict,
54
54
  ContentUnionDict,
55
+ CountTokensConfigDict,
55
56
  ExecutableCodeDict,
56
57
  FunctionCallDict,
57
58
  FunctionCallingConfigDict,
@@ -59,6 +60,7 @@ try:
59
60
  FunctionDeclarationDict,
60
61
  GenerateContentConfigDict,
61
62
  GenerateContentResponse,
63
+ GenerationConfigDict,
62
64
  GoogleSearchDict,
63
65
  HttpOptionsDict,
64
66
  MediaResolution,
@@ -188,6 +190,59 @@ class GoogleModel(Model):
188
190
  response = await self._generate_content(messages, False, model_settings, model_request_parameters)
189
191
  return self._process_response(response)
190
192
 
193
+ async def count_tokens(
194
+ self,
195
+ messages: list[ModelMessage],
196
+ model_settings: ModelSettings | None,
197
+ model_request_parameters: ModelRequestParameters,
198
+ ) -> usage.Usage:
199
+ check_allow_model_requests()
200
+ model_settings = cast(GoogleModelSettings, model_settings or {})
201
+ contents, generation_config = await self._build_content_and_config(
202
+ messages, model_settings, model_request_parameters
203
+ )
204
+
205
+ # Annoyingly, the type of `GenerateContentConfigDict.get` is "partially `Unknown`" because `response_schema` includes `typing._UnionGenericAlias`,
206
+ # so without this we'd need `pyright: ignore[reportUnknownMemberType]` on every line and wouldn't get type checking anyway.
207
+ generation_config = cast(dict[str, Any], generation_config)
208
+
209
+ config = CountTokensConfigDict(
210
+ http_options=generation_config.get('http_options'),
211
+ )
212
+ if self.system != 'google-gla':
213
+ # The fields are not supported by the Gemini API per https://github.com/googleapis/python-genai/blob/7e4ec284dc6e521949626f3ed54028163ef9121d/google/genai/models.py#L1195-L1214
214
+ config.update(
215
+ system_instruction=generation_config.get('system_instruction'),
216
+ tools=cast(list[ToolDict], generation_config.get('tools')),
217
+ # Annoyingly, GenerationConfigDict has fewer fields than GenerateContentConfigDict, and no extra fields are allowed.
218
+ generation_config=GenerationConfigDict(
219
+ temperature=generation_config.get('temperature'),
220
+ top_p=generation_config.get('top_p'),
221
+ max_output_tokens=generation_config.get('max_output_tokens'),
222
+ stop_sequences=generation_config.get('stop_sequences'),
223
+ presence_penalty=generation_config.get('presence_penalty'),
224
+ frequency_penalty=generation_config.get('frequency_penalty'),
225
+ thinking_config=generation_config.get('thinking_config'),
226
+ media_resolution=generation_config.get('media_resolution'),
227
+ response_mime_type=generation_config.get('response_mime_type'),
228
+ response_schema=generation_config.get('response_schema'),
229
+ ),
230
+ )
231
+
232
+ response = await self.client.aio.models.count_tokens(
233
+ model=self._model_name,
234
+ contents=contents,
235
+ config=config,
236
+ )
237
+ if response.total_tokens is None:
238
+ raise UnexpectedModelBehavior( # pragma: no cover
239
+ 'Total tokens missing from Gemini response', str(response)
240
+ )
241
+ return usage.Usage(
242
+ request_tokens=response.total_tokens,
243
+ total_tokens=response.total_tokens,
244
+ )
245
+
191
246
  @asynccontextmanager
192
247
  async def request_stream(
193
248
  self,
@@ -265,16 +320,23 @@ class GoogleModel(Model):
265
320
  model_settings: GoogleModelSettings,
266
321
  model_request_parameters: ModelRequestParameters,
267
322
  ) -> GenerateContentResponse | Awaitable[AsyncIterator[GenerateContentResponse]]:
268
- tools = self._get_tools(model_request_parameters)
323
+ contents, config = await self._build_content_and_config(messages, model_settings, model_request_parameters)
324
+ func = self.client.aio.models.generate_content_stream if stream else self.client.aio.models.generate_content
325
+ return await func(model=self._model_name, contents=contents, config=config) # type: ignore
269
326
 
327
+ async def _build_content_and_config(
328
+ self,
329
+ messages: list[ModelMessage],
330
+ model_settings: GoogleModelSettings,
331
+ model_request_parameters: ModelRequestParameters,
332
+ ) -> tuple[list[ContentUnionDict], GenerateContentConfigDict]:
333
+ tools = self._get_tools(model_request_parameters)
270
334
  response_mime_type = None
271
335
  response_schema = None
272
336
  if model_request_parameters.output_mode == 'native':
273
337
  if tools:
274
338
  raise UserError('Gemini does not support structured output and tools at the same time.')
275
-
276
339
  response_mime_type = 'application/json'
277
-
278
340
  output_object = model_request_parameters.output_object
279
341
  assert output_object is not None
280
342
  response_schema = self._map_response_schema(output_object)
@@ -311,9 +373,7 @@ class GoogleModel(Model):
311
373
  response_mime_type=response_mime_type,
312
374
  response_schema=response_schema,
313
375
  )
314
-
315
- func = self.client.aio.models.generate_content_stream if stream else self.client.aio.models.generate_content
316
- return await func(model=self._model_name, contents=contents, config=config) # type: ignore
376
+ return contents, config
317
377
 
318
378
  def _process_response(self, response: GenerateContentResponse) -> ModelResponse:
319
379
  if not response.candidates or len(response.candidates) != 1:
@@ -59,6 +59,11 @@ try:
59
59
  from openai.types.chat.chat_completion_content_part_image_param import ImageURL
60
60
  from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio
61
61
  from openai.types.chat.chat_completion_content_part_param import File, FileFile
62
+ from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall
63
+ from openai.types.chat.chat_completion_message_function_tool_call import ChatCompletionMessageFunctionToolCall
64
+ from openai.types.chat.chat_completion_message_function_tool_call_param import (
65
+ ChatCompletionMessageFunctionToolCallParam,
66
+ )
62
67
  from openai.types.chat.chat_completion_prediction_content_param import ChatCompletionPredictionContentParam
63
68
  from openai.types.chat.completion_create_params import (
64
69
  WebSearchOptions,
@@ -172,6 +177,14 @@ class OpenAIResponsesModelSettings(OpenAIModelSettings, total=False):
172
177
  middle of the conversation.
173
178
  """
174
179
 
180
+ openai_text_verbosity: Literal['low', 'medium', 'high']
181
+ """Constrains the verbosity of the model's text response.
182
+
183
+ Lower values will result in more concise responses, while higher values will
184
+ result in more verbose responses. Currently supported values are `low`,
185
+ `medium`, and `high`.
186
+ """
187
+
175
188
 
176
189
  @dataclass(init=False)
177
190
  class OpenAIModel(Model):
@@ -416,7 +429,14 @@ class OpenAIModel(Model):
416
429
  items.extend(split_content_into_text_and_thinking(choice.message.content, self.profile.thinking_tags))
417
430
  if choice.message.tool_calls is not None:
418
431
  for c in choice.message.tool_calls:
419
- part = ToolCallPart(c.function.name, c.function.arguments, tool_call_id=c.id)
432
+ if isinstance(c, ChatCompletionMessageFunctionToolCall):
433
+ part = ToolCallPart(c.function.name, c.function.arguments, tool_call_id=c.id)
434
+ elif isinstance(c, ChatCompletionMessageCustomToolCall): # pragma: no cover
435
+ # NOTE: Custom tool calls are not supported.
436
+ # See <https://github.com/pydantic/pydantic-ai/issues/2513> for more details.
437
+ raise RuntimeError('Custom tool calls are not supported')
438
+ else:
439
+ assert_never(c)
420
440
  part.tool_call_id = _guard_tool_call_id(part)
421
441
  items.append(part)
422
442
  return ModelResponse(
@@ -476,7 +496,7 @@ class OpenAIModel(Model):
476
496
  openai_messages.append(item)
477
497
  elif isinstance(message, ModelResponse):
478
498
  texts: list[str] = []
479
- tool_calls: list[chat.ChatCompletionMessageToolCallParam] = []
499
+ tool_calls: list[ChatCompletionMessageFunctionToolCallParam] = []
480
500
  for item in message.parts:
481
501
  if isinstance(item, TextPart):
482
502
  texts.append(item.content)
@@ -507,8 +527,8 @@ class OpenAIModel(Model):
507
527
  return openai_messages
508
528
 
509
529
  @staticmethod
510
- def _map_tool_call(t: ToolCallPart) -> chat.ChatCompletionMessageToolCallParam:
511
- return chat.ChatCompletionMessageToolCallParam(
530
+ def _map_tool_call(t: ToolCallPart) -> ChatCompletionMessageFunctionToolCallParam:
531
+ return ChatCompletionMessageFunctionToolCallParam(
512
532
  id=_guard_tool_call_id(t=t),
513
533
  type='function',
514
534
  function={'name': t.tool_name, 'arguments': t.args_as_json_str()},
@@ -807,6 +827,10 @@ class OpenAIResponsesModel(Model):
807
827
  openai_messages.insert(0, responses.EasyInputMessageParam(role='system', content=instructions))
808
828
  instructions = NOT_GIVEN
809
829
 
830
+ if verbosity := model_settings.get('openai_text_verbosity'):
831
+ text = text or {}
832
+ text['verbosity'] = verbosity
833
+
810
834
  sampling_settings = (
811
835
  model_settings
812
836
  if OpenAIModelProfile.from_profile(self.profile).openai_supports_sampling_settings
@@ -96,6 +96,10 @@ class UsageLimits:
96
96
  """The maximum number of tokens allowed in responses from the model."""
97
97
  total_tokens_limit: int | None = None
98
98
  """The maximum number of tokens allowed in requests and responses combined."""
99
+ count_tokens_before_request: bool = False
100
+ """If True, perform a token counting pass before sending the request to the model,
101
+ to enforce `request_tokens_limit` ahead of time. This may incur additional overhead
102
+ (from calling the model's `count_tokens` API before making the actual request) and is disabled by default."""
99
103
 
100
104
  def has_token_limits(self) -> bool:
101
105
  """Returns `True` if this instance places any limits on token counts.
@@ -111,11 +115,23 @@ class UsageLimits:
111
115
  )
112
116
 
113
117
  def check_before_request(self, usage: Usage) -> None:
114
- """Raises a `UsageLimitExceeded` exception if the next request would exceed the request_limit."""
118
+ """Raises a `UsageLimitExceeded` exception if the next request would exceed any of the limits."""
115
119
  request_limit = self.request_limit
116
120
  if request_limit is not None and usage.requests >= request_limit:
117
121
  raise UsageLimitExceeded(f'The next request would exceed the request_limit of {request_limit}')
118
122
 
123
+ request_tokens = usage.request_tokens or 0
124
+ if self.request_tokens_limit is not None and request_tokens > self.request_tokens_limit:
125
+ raise UsageLimitExceeded(
126
+ f'The next request would exceed the request_tokens_limit of {self.request_tokens_limit} ({request_tokens=})'
127
+ )
128
+
129
+ total_tokens = usage.total_tokens or 0
130
+ if self.total_tokens_limit is not None and total_tokens > self.total_tokens_limit:
131
+ raise UsageLimitExceeded(
132
+ f'The next request would exceed the total_tokens_limit of {self.total_tokens_limit} ({total_tokens=})'
133
+ )
134
+
119
135
  def check_tokens(self, usage: Usage) -> None:
120
136
  """Raises a `UsageLimitExceeded` exception if the usage exceeds any of the token limits."""
121
137
  request_tokens = usage.request_tokens or 0
@@ -62,7 +62,7 @@ dependencies = [
62
62
  # WARNING if you add optional groups, please update docs/install.md
63
63
  logfire = ["logfire>=3.14.1"]
64
64
  # Models
65
- openai = ["openai>=1.92.0"]
65
+ openai = ["openai>=1.99.9"]
66
66
  cohere = ["cohere>=5.16.0; platform_system != 'Emscripten'"]
67
67
  vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"]
68
68
  google = ["google-genai>=1.28.0"]