pydantic-ai-slim 0.0.48__tar.gz → 0.0.50__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 (51) hide show
  1. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/PKG-INFO +3 -3
  2. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_cli.py +82 -59
  3. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/__init__.py +2 -0
  4. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/anthropic.py +5 -41
  5. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/cohere.py +1 -1
  6. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/gemini.py +7 -5
  7. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/openai.py +65 -18
  8. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/.gitignore +0 -0
  9. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/README.md +0 -0
  10. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/__init__.py +0 -0
  11. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/__main__.py +0 -0
  12. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_agent_graph.py +0 -0
  13. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_griffe.py +0 -0
  14. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_parts_manager.py +0 -0
  15. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_pydantic.py +0 -0
  16. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_result.py +0 -0
  17. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_system_prompt.py +0 -0
  18. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/_utils.py +0 -0
  19. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/agent.py +0 -0
  20. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/common_tools/__init__.py +0 -0
  21. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  22. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/common_tools/tavily.py +0 -0
  23. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/exceptions.py +0 -0
  24. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/format_as_xml.py +0 -0
  25. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/mcp.py +0 -0
  26. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/messages.py +0 -0
  27. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/bedrock.py +0 -0
  28. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/fallback.py +0 -0
  29. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/function.py +0 -0
  30. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/groq.py +0 -0
  31. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/instrumented.py +0 -0
  32. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/mistral.py +0 -0
  33. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/test.py +0 -0
  34. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/models/wrapper.py +0 -0
  35. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/__init__.py +0 -0
  36. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/anthropic.py +0 -0
  37. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/azure.py +0 -0
  38. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/bedrock.py +0 -0
  39. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/cohere.py +0 -0
  40. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/deepseek.py +0 -0
  41. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/google_gla.py +0 -0
  42. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/google_vertex.py +0 -0
  43. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/groq.py +0 -0
  44. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/mistral.py +0 -0
  45. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/providers/openai.py +0 -0
  46. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/py.typed +0 -0
  47. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/result.py +0 -0
  48. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/settings.py +0 -0
  49. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/tools.py +0 -0
  50. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pydantic_ai/usage.py +0 -0
  51. {pydantic_ai_slim-0.0.48 → pydantic_ai_slim-0.0.50}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 0.0.48
3
+ Version: 0.0.50
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
5
5
  Author-email: Samuel Colvin <samuel@pydantic.dev>
6
6
  License-Expression: MIT
@@ -29,7 +29,7 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
29
29
  Requires-Dist: griffe>=1.3.2
30
30
  Requires-Dist: httpx>=0.27
31
31
  Requires-Dist: opentelemetry-api>=1.28.0
32
- Requires-Dist: pydantic-graph==0.0.48
32
+ Requires-Dist: pydantic-graph==0.0.50
33
33
  Requires-Dist: pydantic>=2.10
34
34
  Requires-Dist: typing-inspection>=0.4.0
35
35
  Provides-Extra: anthropic
@@ -45,7 +45,7 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
45
45
  Provides-Extra: duckduckgo
46
46
  Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
47
47
  Provides-Extra: evals
48
- Requires-Dist: pydantic-evals==0.0.48; extra == 'evals'
48
+ Requires-Dist: pydantic-evals==0.0.50; extra == 'evals'
49
49
  Provides-Extra: groq
50
50
  Requires-Dist: groq>=0.15.0; extra == 'groq'
51
51
  Provides-Extra: logfire
@@ -3,19 +3,20 @@ from __future__ import annotations as _annotations
3
3
  import argparse
4
4
  import asyncio
5
5
  import sys
6
+ from asyncio import CancelledError
6
7
  from collections.abc import Sequence
7
8
  from contextlib import ExitStack
8
9
  from datetime import datetime, timezone
9
10
  from importlib.metadata import version
10
11
  from pathlib import Path
11
- from typing import cast
12
+ from typing import Any, cast
12
13
 
13
14
  from typing_inspection.introspection import get_literal_values
14
15
 
15
16
  from pydantic_ai.agent import Agent
16
17
  from pydantic_ai.exceptions import UserError
17
18
  from pydantic_ai.messages import ModelMessage, PartDeltaEvent, TextPartDelta
18
- from pydantic_ai.models import KnownModelName
19
+ from pydantic_ai.models import KnownModelName, infer_model
19
20
 
20
21
  try:
21
22
  import argcomplete
@@ -47,7 +48,7 @@ class SimpleCodeBlock(CodeBlock):
47
48
  This avoids a background color which messes up copy-pasting and sets the language name as dim prefix and suffix.
48
49
  """
49
50
 
50
- def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: # pragma: no cover
51
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
51
52
  code = str(self.text).rstrip()
52
53
  yield Text(self.lexer_name, style='dim')
53
54
  yield Syntax(code, self.lexer_name, theme=self.theme, background_color='default', word_wrap=True)
@@ -57,7 +58,7 @@ class SimpleCodeBlock(CodeBlock):
57
58
  class LeftHeading(Heading):
58
59
  """Customised headings in markdown to stop centering and prepend markdown style hashes."""
59
60
 
60
- def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: # pragma: no cover
61
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
61
62
  # note we use `Style(bold=True)` not `self.style_name` here to disable underlining which is ugly IMHO
62
63
  yield Text(f'{"#" * int(self.tag[1:])} {self.text.plain}', style=Style(bold=True))
63
64
 
@@ -68,7 +69,21 @@ Markdown.elements.update(
68
69
  )
69
70
 
70
71
 
71
- def cli(args_list: Sequence[str] | None = None) -> int: # noqa: C901 # pragma: no cover
72
+ cli_agent = Agent()
73
+
74
+
75
+ @cli_agent.system_prompt
76
+ def cli_system_prompt() -> str:
77
+ now_utc = datetime.now(timezone.utc)
78
+ tzinfo = now_utc.astimezone().tzinfo
79
+ tzname = tzinfo.tzname(now_utc) if tzinfo else ''
80
+ return f"""\
81
+ Help the user by responding to their request, the output should be concise and always written in markdown.
82
+ The current date and time is {datetime.now()} {tzname}.
83
+ The user is running {sys.platform}."""
84
+
85
+
86
+ def cli(args_list: Sequence[str] | None = None) -> int:
72
87
  parser = argparse.ArgumentParser(
73
88
  prog='pai',
74
89
  description=f"""\
@@ -124,18 +139,10 @@ Special prompt:
124
139
  console.print(f' {model}', highlight=False)
125
140
  return 0
126
141
 
127
- now_utc = datetime.now(timezone.utc)
128
- tzname = now_utc.astimezone().tzinfo.tzname(now_utc) # type: ignore
129
142
  try:
130
- agent = Agent(
131
- model=args.model,
132
- system_prompt=f"""\
133
- Help the user by responding to their request, the output should be concise and always written in markdown.
134
- The current date and time is {datetime.now()} {tzname}.
135
- The user is running {sys.platform}.""",
136
- )
137
- except UserError:
138
- console.print(f'[red]Invalid model "{args.model}"[/red]')
143
+ cli_agent.model = infer_model(args.model)
144
+ except UserError as e:
145
+ console.print(f'Error initializing [magenta]{args.model}[/magenta]:\n[red]{e}[/red]')
139
146
  return 1
140
147
 
141
148
  stream = not args.no_stream
@@ -148,67 +155,44 @@ Special prompt:
148
155
 
149
156
  if prompt := cast(str, args.prompt):
150
157
  try:
151
- asyncio.run(ask_agent(agent, prompt, stream, console, code_theme))
158
+ asyncio.run(ask_agent(cli_agent, prompt, stream, console, code_theme))
152
159
  except KeyboardInterrupt:
153
160
  pass
154
161
  return 0
155
162
 
156
163
  history = Path.home() / '.pai-prompt-history.txt'
157
- session = PromptSession(history=FileHistory(str(history))) # type: ignore
164
+ # doing this instead of `PromptSession[Any](history=` allows mocking of PromptSession in tests
165
+ session: PromptSession[Any] = PromptSession(history=FileHistory(str(history)))
166
+ try:
167
+ return asyncio.run(run_chat(session, stream, cli_agent, console, code_theme))
168
+ except KeyboardInterrupt: # pragma: no cover
169
+ return 0
170
+
171
+
172
+ async def run_chat(session: PromptSession[Any], stream: bool, agent: Agent, console: Console, code_theme: str) -> int:
158
173
  multiline = False
159
174
  messages: list[ModelMessage] = []
160
175
 
161
176
  while True:
162
177
  try:
163
178
  auto_suggest = CustomAutoSuggest(['/markdown', '/multiline', '/exit'])
164
- text = cast(str, session.prompt('pai ➤ ', auto_suggest=auto_suggest, multiline=multiline))
165
- except (KeyboardInterrupt, EOFError):
179
+ text = await session.prompt_async('pai ➤ ', auto_suggest=auto_suggest, multiline=multiline)
180
+ except (KeyboardInterrupt, EOFError): # pragma: no cover
166
181
  return 0
167
182
 
168
183
  if not text.strip():
169
184
  continue
170
185
 
171
- ident_prompt = text.lower().strip(' ').replace(' ', '-').lstrip(' ')
186
+ ident_prompt = text.lower().strip().replace(' ', '-')
172
187
  if ident_prompt.startswith('/'):
173
- if ident_prompt == '/markdown':
174
- try:
175
- parts = messages[-1].parts
176
- except IndexError:
177
- console.print('[dim]No markdown output available.[/dim]')
178
- continue
179
- console.print('[dim]Markdown output of last question:[/dim]\n')
180
- for part in parts:
181
- if part.part_kind == 'text':
182
- console.print(
183
- Syntax(
184
- part.content,
185
- lexer='markdown',
186
- theme=code_theme,
187
- word_wrap=True,
188
- background_color='default',
189
- )
190
- )
191
-
192
- elif ident_prompt == '/multiline':
193
- multiline = not multiline
194
- if multiline:
195
- console.print(
196
- 'Enabling multiline mode. '
197
- '[dim]Press [Meta+Enter] or [Esc] followed by [Enter] to accept input.[/dim]'
198
- )
199
- else:
200
- console.print('Disabling multiline mode.')
201
- elif ident_prompt == '/exit':
202
- console.print('[dim]Exiting…[/dim]')
203
- return 0
204
- else:
205
- console.print(f'[red]Unknown command[/red] [magenta]`{ident_prompt}`[/magenta]')
188
+ exit_value, multiline = handle_slash_command(ident_prompt, messages, multiline, console, code_theme)
189
+ if exit_value is not None:
190
+ return exit_value
206
191
  else:
207
192
  try:
208
- messages = asyncio.run(ask_agent(agent, text, stream, console, code_theme, messages))
209
- except KeyboardInterrupt:
193
+ messages = await ask_agent(agent, text, stream, console, code_theme, messages)
194
+ except CancelledError: # pragma: no cover
210
195
  console.print('[dim]Interrupted[/dim]')
211
- messages = []
212
196
 
213
197
 
214
198
  async def ask_agent(
@@ -218,7 +202,7 @@ async def ask_agent(
218
202
  console: Console,
219
203
  code_theme: str,
220
204
  messages: list[ModelMessage] | None = None,
221
- ) -> list[ModelMessage]: # pragma: no cover
205
+ ) -> list[ModelMessage]:
222
206
  status = Status('[dim]Working on it…[/dim]', console=console)
223
207
 
224
208
  if not stream:
@@ -248,7 +232,7 @@ async def ask_agent(
248
232
 
249
233
 
250
234
  class CustomAutoSuggest(AutoSuggestFromHistory):
251
- def __init__(self, special_suggestions: list[str] | None = None): # pragma: no cover
235
+ def __init__(self, special_suggestions: list[str] | None = None):
252
236
  super().__init__()
253
237
  self.special_suggestions = special_suggestions or []
254
238
 
@@ -264,5 +248,44 @@ class CustomAutoSuggest(AutoSuggestFromHistory):
264
248
  return suggestion
265
249
 
266
250
 
251
+ def handle_slash_command(
252
+ ident_prompt: str, messages: list[ModelMessage], multiline: bool, console: Console, code_theme: str
253
+ ) -> tuple[int | None, bool]:
254
+ if ident_prompt == '/markdown':
255
+ try:
256
+ parts = messages[-1].parts
257
+ except IndexError:
258
+ console.print('[dim]No markdown output available.[/dim]')
259
+ else:
260
+ console.print('[dim]Markdown output of last question:[/dim]\n')
261
+ for part in parts:
262
+ if part.part_kind == 'text':
263
+ console.print(
264
+ Syntax(
265
+ part.content,
266
+ lexer='markdown',
267
+ theme=code_theme,
268
+ word_wrap=True,
269
+ background_color='default',
270
+ )
271
+ )
272
+
273
+ elif ident_prompt == '/multiline':
274
+ multiline = not multiline
275
+ if multiline:
276
+ console.print(
277
+ 'Enabling multiline mode. [dim]Press [Meta+Enter] or [Esc] followed by [Enter] to accept input.[/dim]'
278
+ )
279
+ else:
280
+ console.print('Disabling multiline mode.')
281
+ return None, multiline
282
+ elif ident_prompt == '/exit':
283
+ console.print('[dim]Exiting…[/dim]')
284
+ return 0, multiline
285
+ else:
286
+ console.print(f'[red]Unknown command[/red] [magenta]`{ident_prompt}`[/magenta]')
287
+ return None, multiline
288
+
289
+
267
290
  def app(): # pragma: no cover
268
291
  sys.exit(cli())
@@ -106,6 +106,7 @@ KnownModelName = TypeAliasType(
106
106
  'google-gla:gemini-2.0-flash',
107
107
  'google-gla:gemini-2.0-flash-lite-preview-02-05',
108
108
  'google-gla:gemini-2.0-pro-exp-02-05',
109
+ 'google-gla:gemini-2.5-pro-exp-03-25',
109
110
  'google-vertex:gemini-1.0-pro',
110
111
  'google-vertex:gemini-1.5-flash',
111
112
  'google-vertex:gemini-1.5-flash-8b',
@@ -116,6 +117,7 @@ KnownModelName = TypeAliasType(
116
117
  'google-vertex:gemini-2.0-flash',
117
118
  'google-vertex:gemini-2.0-flash-lite-preview-02-05',
118
119
  'google-vertex:gemini-2.0-pro-exp-02-05',
120
+ 'google-vertex:gemini-2.5-pro-exp-03-25',
119
121
  'gpt-3.5-turbo',
120
122
  'gpt-3.5-turbo-0125',
121
123
  'gpt-3.5-turbo-0301',
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- import base64
4
3
  import io
5
4
  from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator
6
5
  from contextlib import asynccontextmanager
@@ -9,7 +8,6 @@ from datetime import datetime, timezone
9
8
  from json import JSONDecodeError, loads as json_loads
10
9
  from typing import Any, Literal, Union, cast, overload
11
10
 
12
- from anthropic.types import DocumentBlockParam
13
11
  from typing_extensions import assert_never
14
12
 
15
13
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
@@ -40,6 +38,7 @@ try:
40
38
  from anthropic.types import (
41
39
  Base64PDFSourceParam,
42
40
  ContentBlock,
41
+ DocumentBlockParam,
43
42
  ImageBlockParam,
44
43
  Message as AnthropicMessage,
45
44
  MessageParam,
@@ -354,48 +353,13 @@ class AnthropicModel(Model):
354
353
  else:
355
354
  raise RuntimeError('Only images and PDFs are supported for binary content')
356
355
  elif isinstance(item, ImageUrl):
357
- try:
358
- response = await cached_async_http_client().get(item.url)
359
- response.raise_for_status()
360
- yield ImageBlockParam(
361
- source={
362
- 'data': io.BytesIO(response.content),
363
- 'media_type': item.media_type,
364
- 'type': 'base64',
365
- },
366
- type='image',
367
- )
368
- except ValueError:
369
- # Download the file if can't find the mime type.
370
- client = cached_async_http_client()
371
- response = await client.get(item.url, follow_redirects=True)
372
- response.raise_for_status()
373
- base64_encoded = base64.b64encode(response.content).decode('utf-8')
374
- if (mime_type := response.headers['Content-Type']) in (
375
- 'image/jpeg',
376
- 'image/png',
377
- 'image/gif',
378
- 'image/webp',
379
- ):
380
- yield ImageBlockParam(
381
- source={'data': base64_encoded, 'media_type': mime_type, 'type': 'base64'},
382
- type='image',
383
- )
384
- else: # pragma: no cover
385
- raise RuntimeError(f'Unsupported image type: {mime_type}')
356
+ yield ImageBlockParam(source={'type': 'url', 'url': item.url}, type='image')
386
357
  elif isinstance(item, DocumentUrl):
387
- response = await cached_async_http_client().get(item.url)
388
- response.raise_for_status()
389
358
  if item.media_type == 'application/pdf':
390
- yield DocumentBlockParam(
391
- source=Base64PDFSourceParam(
392
- data=io.BytesIO(response.content),
393
- media_type=item.media_type,
394
- type='base64',
395
- ),
396
- type='document',
397
- )
359
+ yield DocumentBlockParam(source={'url': item.url, 'type': 'url'}, type='document')
398
360
  elif item.media_type == 'text/plain':
361
+ response = await cached_async_http_client().get(item.url)
362
+ response.raise_for_status()
399
363
  yield DocumentBlockParam(
400
364
  source=PlainTextSourceParam(data=response.text, media_type=item.media_type, type='text'),
401
365
  type='document',
@@ -5,7 +5,6 @@ from dataclasses import dataclass, field
5
5
  from itertools import chain
6
6
  from typing import Literal, Union, cast
7
7
 
8
- from cohere import TextAssistantMessageContentItem
9
8
  from typing_extensions import assert_never
10
9
 
11
10
  from .. import ModelHTTPError, result
@@ -38,6 +37,7 @@ try:
38
37
  ChatMessageV2,
39
38
  ChatResponse,
40
39
  SystemChatMessageV2,
40
+ TextAssistantMessageContentItem,
41
41
  ToolCallV2,
42
42
  ToolCallV2Function,
43
43
  ToolChatMessageV2,
@@ -57,6 +57,7 @@ LatestGeminiModelNames = Literal[
57
57
  'gemini-2.0-flash',
58
58
  'gemini-2.0-flash-lite-preview-02-05',
59
59
  'gemini-2.0-pro-exp-02-05',
60
+ 'gemini-2.5-pro-exp-03-25',
60
61
  ]
61
62
  """Latest Gemini models."""
62
63
 
@@ -134,7 +135,8 @@ class GeminiModel(Model):
134
135
  async with self._make_request(
135
136
  messages, False, cast(GeminiModelSettings, model_settings or {}), model_request_parameters
136
137
  ) as http_response:
137
- response = _gemini_response_ta.validate_json(await http_response.aread())
138
+ data = await http_response.aread()
139
+ response = _gemini_response_ta.validate_json(data)
138
140
  return self._process_response(response), _metadata_as_usage(response)
139
141
 
140
142
  @asynccontextmanager
@@ -639,10 +641,7 @@ class _GeminiFunction(TypedDict):
639
641
 
640
642
  def _function_from_abstract_tool(tool: ToolDefinition) -> _GeminiFunction:
641
643
  json_schema = _GeminiJsonSchema(tool.parameters_json_schema).simplify()
642
- f = _GeminiFunction(
643
- name=tool.name,
644
- description=tool.description,
645
- )
644
+ f = _GeminiFunction(name=tool.name, description=tool.description)
646
645
  if json_schema.get('properties'):
647
646
  f['parameters'] = json_schema
648
647
  return f
@@ -769,6 +768,9 @@ class _GeminiJsonSchema:
769
768
  def _simplify(self, schema: dict[str, Any], refs_stack: tuple[str, ...]) -> None:
770
769
  schema.pop('title', None)
771
770
  schema.pop('default', None)
771
+ schema.pop('$schema', None)
772
+ schema.pop('exclusiveMaximum', None)
773
+ schema.pop('exclusiveMinimum', None)
772
774
  if ref := schema.pop('$ref', None):
773
775
  # noinspection PyTypeChecker
774
776
  key = re.sub(r'^#/\$defs/', '', ref)
@@ -2,14 +2,12 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import base64
4
4
  import warnings
5
- from collections.abc import AsyncIterable, AsyncIterator
5
+ 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, timezone
9
9
  from typing import Literal, Union, cast, overload
10
10
 
11
- from openai import NotGiven
12
- from openai.types import Reasoning
13
11
  from typing_extensions import assert_never
14
12
 
15
13
  from pydantic_ai.providers import Provider, infer_provider
@@ -44,7 +42,7 @@ from . import (
44
42
  )
45
43
 
46
44
  try:
47
- from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream
45
+ from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream, NotGiven
48
46
  from openai.types import ChatModel, chat, responses
49
47
  from openai.types.chat import (
50
48
  ChatCompletionChunk,
@@ -55,6 +53,7 @@ try:
55
53
  )
56
54
  from openai.types.chat.chat_completion_content_part_image_param import ImageURL
57
55
  from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio
56
+ from openai.types.responses import ComputerToolParam, FileSearchToolParam, WebSearchToolParam
58
57
  from openai.types.responses.response_input_param import FunctionCallOutput, Message
59
58
  from openai.types.shared import ReasoningEffort
60
59
  from openai.types.shared_params import Reasoning
@@ -64,6 +63,14 @@ except ImportError as _import_error:
64
63
  'you can use the `openai` optional group — `pip install "pydantic-ai-slim[openai]"`'
65
64
  ) from _import_error
66
65
 
66
+ __all__ = (
67
+ 'OpenAIModel',
68
+ 'OpenAIResponsesModel',
69
+ 'OpenAIModelSettings',
70
+ 'OpenAIResponsesModelSettings',
71
+ 'OpenAIModelName',
72
+ )
73
+
67
74
  OpenAIModelName = Union[str, ChatModel]
68
75
  """
69
76
  Possible OpenAI model names.
@@ -86,8 +93,7 @@ class OpenAIModelSettings(ModelSettings, total=False):
86
93
  """
87
94
 
88
95
  openai_reasoning_effort: ReasoningEffort
89
- """
90
- Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning).
96
+ """Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning).
91
97
 
92
98
  Currently supported values are `low`, `medium`, and `high`. Reducing reasoning effort can
93
99
  result in faster responses and fewer tokens used on reasoning in a response.
@@ -100,6 +106,40 @@ class OpenAIModelSettings(ModelSettings, total=False):
100
106
  """
101
107
 
102
108
 
109
+ class OpenAIResponsesModelSettings(OpenAIModelSettings, total=False):
110
+ """Settings used for an OpenAI Responses model request.
111
+
112
+ ALL FIELDS MUST BE `openai_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
113
+ """
114
+
115
+ openai_builtin_tools: Sequence[FileSearchToolParam | WebSearchToolParam | ComputerToolParam]
116
+ """The provided OpenAI built-in tools to use.
117
+
118
+ See [OpenAI's built-in tools](https://platform.openai.com/docs/guides/tools?api-mode=responses) for more details.
119
+ """
120
+
121
+ openai_reasoning_generate_summary: Literal['detailed', 'concise']
122
+ """A summary of the reasoning performed by the model.
123
+
124
+ This can be useful for debugging and understanding the model's reasoning process.
125
+ One of `concise` or `detailed`.
126
+
127
+ Check the [OpenAI Computer use documentation](https://platform.openai.com/docs/guides/tools-computer-use#1-send-a-request-to-the-model)
128
+ for more details.
129
+ """
130
+
131
+ openai_truncation: Literal['disabled', 'auto']
132
+ """The truncation strategy to use for the model response.
133
+
134
+ It can be either:
135
+ - `disabled` (default): If a model response will exceed the context window size for a model, the
136
+ request will fail with a 400 error.
137
+ - `auto`: If the context of this response and previous ones exceeds the model's context window size,
138
+ the model will truncate the response to fit the context window by dropping input items in the
139
+ middle of the conversation.
140
+ """
141
+
142
+
103
143
  @dataclass(init=False)
104
144
  class OpenAIModel(Model):
105
145
  """A model that uses the OpenAI API.
@@ -417,6 +457,8 @@ class OpenAIResponsesModel(Model):
417
457
  - [File search](https://platform.openai.com/docs/guides/tools-file-search)
418
458
  - [Computer use](https://platform.openai.com/docs/guides/tools-computer-use)
419
459
 
460
+ Use the `openai_builtin_tools` setting to add these tools to your model.
461
+
420
462
  If you are interested in the differences between the Responses API and the Chat Completions API,
421
463
  see the [OpenAI API docs](https://platform.openai.com/docs/guides/responses-vs-chat-completions).
422
464
  """
@@ -462,7 +504,7 @@ class OpenAIResponsesModel(Model):
462
504
  ) -> tuple[ModelResponse, usage.Usage]:
463
505
  check_allow_model_requests()
464
506
  response = await self._responses_create(
465
- messages, False, cast(OpenAIModelSettings, model_settings or {}), model_request_parameters
507
+ messages, False, cast(OpenAIResponsesModelSettings, model_settings or {}), model_request_parameters
466
508
  )
467
509
  return self._process_response(response), _map_usage(response)
468
510
 
@@ -475,7 +517,7 @@ class OpenAIResponsesModel(Model):
475
517
  ) -> AsyncIterator[StreamedResponse]:
476
518
  check_allow_model_requests()
477
519
  response = await self._responses_create(
478
- messages, True, cast(OpenAIModelSettings, model_settings or {}), model_request_parameters
520
+ messages, True, cast(OpenAIResponsesModelSettings, model_settings or {}), model_request_parameters
479
521
  )
480
522
  async with response:
481
523
  yield await self._process_streamed_response(response)
@@ -511,7 +553,7 @@ class OpenAIResponsesModel(Model):
511
553
  self,
512
554
  messages: list[ModelRequest | ModelResponse],
513
555
  stream: Literal[False],
514
- model_settings: OpenAIModelSettings,
556
+ model_settings: OpenAIResponsesModelSettings,
515
557
  model_request_parameters: ModelRequestParameters,
516
558
  ) -> responses.Response: ...
517
559
 
@@ -520,7 +562,7 @@ class OpenAIResponsesModel(Model):
520
562
  self,
521
563
  messages: list[ModelRequest | ModelResponse],
522
564
  stream: Literal[True],
523
- model_settings: OpenAIModelSettings,
565
+ model_settings: OpenAIResponsesModelSettings,
524
566
  model_request_parameters: ModelRequestParameters,
525
567
  ) -> AsyncStream[responses.ResponseStreamEvent]: ...
526
568
 
@@ -528,10 +570,11 @@ class OpenAIResponsesModel(Model):
528
570
  self,
529
571
  messages: list[ModelRequest | ModelResponse],
530
572
  stream: bool,
531
- model_settings: OpenAIModelSettings,
573
+ model_settings: OpenAIResponsesModelSettings,
532
574
  model_request_parameters: ModelRequestParameters,
533
575
  ) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]:
534
576
  tools = self._get_tools(model_request_parameters)
577
+ tools = list(model_settings.get('openai_builtin_tools', [])) + tools
535
578
 
536
579
  # standalone function to make it easier to override
537
580
  if not tools:
@@ -542,12 +585,7 @@ class OpenAIResponsesModel(Model):
542
585
  tool_choice = 'auto'
543
586
 
544
587
  system_prompt, openai_messages = await self._map_message(messages)
545
-
546
- reasoning_effort = model_settings.get('openai_reasoning_effort', NOT_GIVEN)
547
- if not isinstance(reasoning_effort, NotGiven):
548
- reasoning = Reasoning(effort=reasoning_effort)
549
- else:
550
- reasoning = NOT_GIVEN
588
+ reasoning = self._get_reasoning(model_settings)
551
589
 
552
590
  try:
553
591
  return await self.client.responses.create(
@@ -561,6 +599,7 @@ class OpenAIResponsesModel(Model):
561
599
  stream=stream,
562
600
  temperature=model_settings.get('temperature', NOT_GIVEN),
563
601
  top_p=model_settings.get('top_p', NOT_GIVEN),
602
+ truncation=model_settings.get('openai_truncation', NOT_GIVEN),
564
603
  timeout=model_settings.get('timeout', NOT_GIVEN),
565
604
  reasoning=reasoning,
566
605
  user=model_settings.get('user', NOT_GIVEN),
@@ -570,6 +609,14 @@ class OpenAIResponsesModel(Model):
570
609
  raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
571
610
  raise
572
611
 
612
+ def _get_reasoning(self, model_settings: OpenAIResponsesModelSettings) -> Reasoning | NotGiven:
613
+ reasoning_effort = model_settings.get('openai_reasoning_effort', None)
614
+ reasoning_generate_summary = model_settings.get('openai_reasoning_generate_summary', None)
615
+
616
+ if reasoning_effort is None and reasoning_generate_summary is None:
617
+ return NOT_GIVEN
618
+ return Reasoning(effort=reasoning_effort, generate_summary=reasoning_generate_summary)
619
+
573
620
  def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.FunctionToolParam]:
574
621
  tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
575
622
  if model_request_parameters.result_tools:
@@ -848,7 +895,7 @@ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.R
848
895
  },
849
896
  )
850
897
  else:
851
- details: dict[str, int] = {}
898
+ details = {}
852
899
  if response_usage.completion_tokens_details is not None:
853
900
  details.update(response_usage.completion_tokens_details.model_dump(exclude_none=True))
854
901
  if response_usage.prompt_tokens_details is not None: