pydantic-ai-slim 0.7.2__py3-none-any.whl → 0.7.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. pydantic_ai/_agent_graph.py +2 -2
  2. pydantic_ai/_cli.py +18 -3
  3. pydantic_ai/_run_context.py +2 -2
  4. pydantic_ai/ag_ui.py +4 -4
  5. pydantic_ai/agent/__init__.py +7 -9
  6. pydantic_ai/agent/abstract.py +16 -18
  7. pydantic_ai/agent/wrapper.py +4 -6
  8. pydantic_ai/direct.py +4 -4
  9. pydantic_ai/durable_exec/temporal/_agent.py +13 -15
  10. pydantic_ai/durable_exec/temporal/_model.py +2 -2
  11. pydantic_ai/messages.py +16 -6
  12. pydantic_ai/models/__init__.py +5 -5
  13. pydantic_ai/models/anthropic.py +27 -26
  14. pydantic_ai/models/bedrock.py +24 -26
  15. pydantic_ai/models/cohere.py +20 -25
  16. pydantic_ai/models/fallback.py +15 -15
  17. pydantic_ai/models/function.py +7 -9
  18. pydantic_ai/models/gemini.py +43 -39
  19. pydantic_ai/models/google.py +59 -40
  20. pydantic_ai/models/groq.py +22 -19
  21. pydantic_ai/models/huggingface.py +18 -21
  22. pydantic_ai/models/instrumented.py +4 -4
  23. pydantic_ai/models/mcp_sampling.py +1 -2
  24. pydantic_ai/models/mistral.py +24 -22
  25. pydantic_ai/models/openai.py +98 -44
  26. pydantic_ai/models/test.py +4 -5
  27. pydantic_ai/profiles/openai.py +13 -3
  28. pydantic_ai/providers/openai.py +1 -1
  29. pydantic_ai/result.py +5 -5
  30. pydantic_ai/run.py +4 -11
  31. pydantic_ai/usage.py +229 -67
  32. {pydantic_ai_slim-0.7.2.dist-info → pydantic_ai_slim-0.7.3.dist-info}/METADATA +10 -4
  33. {pydantic_ai_slim-0.7.2.dist-info → pydantic_ai_slim-0.7.3.dist-info}/RECORD +36 -36
  34. {pydantic_ai_slim-0.7.2.dist-info → pydantic_ai_slim-0.7.3.dist-info}/WHEEL +0 -0
  35. {pydantic_ai_slim-0.7.2.dist-info → pydantic_ai_slim-0.7.3.dist-info}/entry_points.txt +0 -0
  36. {pydantic_ai_slim-0.7.2.dist-info → pydantic_ai_slim-0.7.3.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ from datetime import datetime
9
9
  from typing import Any, Literal, Union, cast, overload
10
10
 
11
11
  from pydantic import ValidationError
12
- from typing_extensions import assert_never
12
+ from typing_extensions import assert_never, deprecated
13
13
 
14
14
  from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
15
15
  from .._output import DEFAULT_OUTPUT_TOOL_NAME, OutputObjectDefinition
@@ -40,7 +40,7 @@ from ..messages import (
40
40
  VideoUrl,
41
41
  )
42
42
  from ..profiles import ModelProfile, ModelProfileSpec
43
- from ..profiles.openai import OpenAIModelProfile
43
+ from ..profiles.openai import OpenAIModelProfile, OpenAISystemPromptRole
44
44
  from ..providers import Provider, infer_provider
45
45
  from ..settings import ModelSettings
46
46
  from ..tools import ToolDefinition
@@ -100,8 +100,6 @@ Using this more broad type for the model name instead of the ChatModel definitio
100
100
  allows this model to be used more easily with other model types (ie, Ollama, Deepseek).
101
101
  """
102
102
 
103
- OpenAISystemPromptRole = Literal['system', 'developer', 'user']
104
-
105
103
 
106
104
  class OpenAIModelSettings(ModelSettings, total=False):
107
105
  """Settings used for an OpenAI model request."""
@@ -196,10 +194,59 @@ class OpenAIModel(Model):
196
194
  """
197
195
 
198
196
  client: AsyncOpenAI = field(repr=False)
199
- system_prompt_role: OpenAISystemPromptRole | None = field(default=None, repr=False)
200
197
 
201
198
  _model_name: OpenAIModelName = field(repr=False)
202
- _system: str = field(default='openai', repr=False)
199
+ _provider: Provider[AsyncOpenAI] = field(repr=False)
200
+
201
+ @overload
202
+ def __init__(
203
+ self,
204
+ model_name: OpenAIModelName,
205
+ *,
206
+ provider: Literal[
207
+ 'openai',
208
+ 'deepseek',
209
+ 'azure',
210
+ 'openrouter',
211
+ 'moonshotai',
212
+ 'vercel',
213
+ 'grok',
214
+ 'fireworks',
215
+ 'together',
216
+ 'heroku',
217
+ 'github',
218
+ 'ollama',
219
+ ]
220
+ | Provider[AsyncOpenAI] = 'openai',
221
+ profile: ModelProfileSpec | None = None,
222
+ settings: ModelSettings | None = None,
223
+ ) -> None: ...
224
+
225
+ @deprecated('Set the `system_prompt_role` in the `OpenAIModelProfile` instead.')
226
+ @overload
227
+ def __init__(
228
+ self,
229
+ model_name: OpenAIModelName,
230
+ *,
231
+ provider: Literal[
232
+ 'openai',
233
+ 'deepseek',
234
+ 'azure',
235
+ 'openrouter',
236
+ 'moonshotai',
237
+ 'vercel',
238
+ 'grok',
239
+ 'fireworks',
240
+ 'together',
241
+ 'heroku',
242
+ 'github',
243
+ 'ollama',
244
+ ]
245
+ | Provider[AsyncOpenAI] = 'openai',
246
+ profile: ModelProfileSpec | None = None,
247
+ system_prompt_role: OpenAISystemPromptRole | None = None,
248
+ settings: ModelSettings | None = None,
249
+ ) -> None: ...
203
250
 
204
251
  def __init__(
205
252
  self,
@@ -240,16 +287,33 @@ class OpenAIModel(Model):
240
287
 
241
288
  if isinstance(provider, str):
242
289
  provider = infer_provider(provider)
290
+ self._provider = provider
243
291
  self.client = provider.client
244
292
 
245
- self.system_prompt_role = system_prompt_role
246
-
247
293
  super().__init__(settings=settings, profile=profile or provider.model_profile)
248
294
 
295
+ if system_prompt_role is not None:
296
+ self.profile = OpenAIModelProfile(openai_system_prompt_role=system_prompt_role).update(self.profile)
297
+
249
298
  @property
250
299
  def base_url(self) -> str:
251
300
  return str(self.client.base_url)
252
301
 
302
+ @property
303
+ def model_name(self) -> OpenAIModelName:
304
+ """The model name."""
305
+ return self._model_name
306
+
307
+ @property
308
+ def system(self) -> str:
309
+ """The model provider."""
310
+ return self._provider.name
311
+
312
+ @property
313
+ @deprecated('Set the `system_prompt_role` in the `OpenAIModelProfile` instead.')
314
+ def system_prompt_role(self) -> OpenAISystemPromptRole | None:
315
+ return OpenAIModelProfile.from_profile(self.profile).openai_system_prompt_role
316
+
253
317
  async def request(
254
318
  self,
255
319
  messages: list[ModelMessage],
@@ -261,7 +325,6 @@ class OpenAIModel(Model):
261
325
  messages, False, cast(OpenAIModelSettings, model_settings or {}), model_request_parameters
262
326
  )
263
327
  model_response = self._process_response(response)
264
- model_response.usage.requests = 1
265
328
  return model_response
266
329
 
267
330
  @asynccontextmanager
@@ -279,16 +342,6 @@ class OpenAIModel(Model):
279
342
  async with response:
280
343
  yield await self._process_streamed_response(response, model_request_parameters)
281
344
 
282
- @property
283
- def model_name(self) -> OpenAIModelName:
284
- """The model name."""
285
- return self._model_name
286
-
287
- @property
288
- def system(self) -> str:
289
- """The system / model provider."""
290
- return self._system
291
-
292
345
  @overload
293
346
  async def _completions_create(
294
347
  self,
@@ -445,8 +498,8 @@ class OpenAIModel(Model):
445
498
  usage=_map_usage(response),
446
499
  model_name=response.model,
447
500
  timestamp=timestamp,
448
- vendor_details=vendor_details,
449
- vendor_id=response.id,
501
+ provider_details=vendor_details,
502
+ provider_request_id=response.id,
450
503
  )
451
504
 
452
505
  async def _process_streamed_response(
@@ -562,9 +615,10 @@ class OpenAIModel(Model):
562
615
  async def _map_user_message(self, message: ModelRequest) -> AsyncIterable[chat.ChatCompletionMessageParam]:
563
616
  for part in message.parts:
564
617
  if isinstance(part, SystemPromptPart):
565
- if self.system_prompt_role == 'developer':
618
+ system_prompt_role = OpenAIModelProfile.from_profile(self.profile).openai_system_prompt_role
619
+ if system_prompt_role == 'developer':
566
620
  yield chat.ChatCompletionDeveloperMessageParam(role='developer', content=part.content)
567
- elif self.system_prompt_role == 'user':
621
+ elif system_prompt_role == 'user':
568
622
  yield chat.ChatCompletionUserMessageParam(role='user', content=part.content)
569
623
  else:
570
624
  yield chat.ChatCompletionSystemMessageParam(role='system', content=part.content)
@@ -660,10 +714,9 @@ class OpenAIResponsesModel(Model):
660
714
  """
661
715
 
662
716
  client: AsyncOpenAI = field(repr=False)
663
- system_prompt_role: OpenAISystemPromptRole | None = field(default=None)
664
717
 
665
718
  _model_name: OpenAIModelName = field(repr=False)
666
- _system: str = field(default='openai', repr=False)
719
+ _provider: Provider[AsyncOpenAI] = field(repr=False)
667
720
 
668
721
  def __init__(
669
722
  self,
@@ -686,6 +739,7 @@ class OpenAIResponsesModel(Model):
686
739
 
687
740
  if isinstance(provider, str):
688
741
  provider = infer_provider(provider)
742
+ self._provider = provider
689
743
  self.client = provider.client
690
744
 
691
745
  super().__init__(settings=settings, profile=profile or provider.model_profile)
@@ -697,8 +751,8 @@ class OpenAIResponsesModel(Model):
697
751
 
698
752
  @property
699
753
  def system(self) -> str:
700
- """The system / model provider."""
701
- return self._system
754
+ """The model provider."""
755
+ return self._provider.name
702
756
 
703
757
  async def request(
704
758
  self,
@@ -747,7 +801,7 @@ class OpenAIResponsesModel(Model):
747
801
  items,
748
802
  usage=_map_usage(response),
749
803
  model_name=response.model,
750
- vendor_id=response.id,
804
+ provider_request_id=response.id,
751
805
  timestamp=timestamp,
752
806
  )
753
807
 
@@ -1265,10 +1319,10 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
1265
1319
  return self._timestamp
1266
1320
 
1267
1321
 
1268
- def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.Response) -> usage.Usage:
1322
+ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.Response) -> usage.RequestUsage:
1269
1323
  response_usage = response.usage
1270
1324
  if response_usage is None:
1271
- return usage.Usage()
1325
+ return usage.RequestUsage()
1272
1326
  elif isinstance(response_usage, responses.ResponseUsage):
1273
1327
  details: dict[str, int] = {
1274
1328
  key: value
@@ -1278,29 +1332,29 @@ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.R
1278
1332
  if isinstance(value, int)
1279
1333
  }
1280
1334
  details['reasoning_tokens'] = response_usage.output_tokens_details.reasoning_tokens
1281
- details['cached_tokens'] = response_usage.input_tokens_details.cached_tokens
1282
- return usage.Usage(
1283
- request_tokens=response_usage.input_tokens,
1284
- response_tokens=response_usage.output_tokens,
1285
- total_tokens=response_usage.total_tokens,
1335
+ return usage.RequestUsage(
1336
+ input_tokens=response_usage.input_tokens,
1337
+ output_tokens=response_usage.output_tokens,
1338
+ cache_read_tokens=response_usage.input_tokens_details.cached_tokens,
1286
1339
  details=details,
1287
1340
  )
1288
1341
  else:
1289
1342
  details = {
1290
1343
  key: value
1291
1344
  for key, value in response_usage.model_dump(
1292
- exclude={'prompt_tokens', 'completion_tokens', 'total_tokens'}
1345
+ exclude_none=True, exclude={'prompt_tokens', 'completion_tokens', 'total_tokens'}
1293
1346
  ).items()
1294
1347
  if isinstance(value, int)
1295
1348
  }
1349
+ u = usage.RequestUsage(
1350
+ input_tokens=response_usage.prompt_tokens,
1351
+ output_tokens=response_usage.completion_tokens,
1352
+ details=details,
1353
+ )
1296
1354
  if response_usage.completion_tokens_details is not None:
1297
1355
  details.update(response_usage.completion_tokens_details.model_dump(exclude_none=True))
1356
+ u.output_audio_tokens = response_usage.completion_tokens_details.audio_tokens or 0
1298
1357
  if response_usage.prompt_tokens_details is not None:
1299
- details.update(response_usage.prompt_tokens_details.model_dump(exclude_none=True))
1300
- return usage.Usage(
1301
- requests=1,
1302
- request_tokens=response_usage.prompt_tokens,
1303
- response_tokens=response_usage.completion_tokens,
1304
- total_tokens=response_usage.total_tokens,
1305
- details=details,
1306
- )
1358
+ u.input_audio_tokens = response_usage.prompt_tokens_details.audio_tokens or 0
1359
+ u.cache_read_tokens = response_usage.prompt_tokens_details.cached_tokens or 0
1360
+ return u
@@ -31,7 +31,7 @@ from ..messages import (
31
31
  from ..profiles import ModelProfileSpec
32
32
  from ..settings import ModelSettings
33
33
  from ..tools import ToolDefinition
34
- from ..usage import Usage
34
+ from ..usage import RequestUsage
35
35
  from . import Model, ModelRequestParameters, StreamedResponse
36
36
  from .function import _estimate_string_tokens, _estimate_usage # pyright: ignore[reportPrivateUsage]
37
37
 
@@ -113,7 +113,6 @@ class TestModel(Model):
113
113
  self.last_model_request_parameters = model_request_parameters
114
114
  model_response = self._request(messages, model_settings, model_request_parameters)
115
115
  model_response.usage = _estimate_usage([*messages, model_response])
116
- model_response.usage.requests = 1
117
116
  return model_response
118
117
 
119
118
  @asynccontextmanager
@@ -141,7 +140,7 @@ class TestModel(Model):
141
140
 
142
141
  @property
143
142
  def system(self) -> str:
144
- """The system / model provider."""
143
+ """The model provider."""
145
144
  return self._system
146
145
 
147
146
  def gen_tool_args(self, tool_def: ToolDefinition) -> Any:
@@ -468,6 +467,6 @@ class _JsonSchemaTestData:
468
467
  return s
469
468
 
470
469
 
471
- def _get_string_usage(text: str) -> Usage:
470
+ def _get_string_usage(text: str) -> RequestUsage:
472
471
  response_tokens = _estimate_string_tokens(text)
473
- return Usage(response_tokens=response_tokens, total_tokens=response_tokens)
472
+ return RequestUsage(output_tokens=response_tokens)
@@ -2,11 +2,13 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import re
4
4
  from dataclasses import dataclass
5
- from typing import Any
5
+ from typing import Any, Literal
6
6
 
7
7
  from . import ModelProfile
8
8
  from ._json_schema import JsonSchema, JsonSchemaTransformer
9
9
 
10
+ OpenAISystemPromptRole = Literal['system', 'developer', 'user']
11
+
10
12
 
11
13
  @dataclass
12
14
  class OpenAIModelProfile(ModelProfile):
@@ -26,8 +28,10 @@ class OpenAIModelProfile(ModelProfile):
26
28
  # safe to pass that value along. Default is `True` to preserve existing
27
29
  # behaviour for OpenAI itself and most providers.
28
30
  openai_supports_tool_choice_required: bool = True
29
- """Whether the provider accepts the value ``tool_choice='required'`` in the
30
- request payload."""
31
+ """Whether the provider accepts the value ``tool_choice='required'`` in the request payload."""
32
+
33
+ openai_system_prompt_role: OpenAISystemPromptRole | None = None
34
+ """The role to use for the system prompt message. If not provided, defaults to `'system'`."""
31
35
 
32
36
 
33
37
  def openai_model_profile(model_name: str) -> ModelProfile:
@@ -36,11 +40,17 @@ def openai_model_profile(model_name: str) -> ModelProfile:
36
40
  # Structured Outputs (output mode 'native') is only supported with the gpt-4o-mini, gpt-4o-mini-2024-07-18, and gpt-4o-2024-08-06 model snapshots and later.
37
41
  # We leave it in here for all models because the `default_structured_output_mode` is `'tool'`, so `native` is only used
38
42
  # when the user specifically uses the `NativeOutput` marker, so an error from the API is acceptable.
43
+
44
+ # The o1-mini model doesn't support the `system` role, so we default to `user`.
45
+ # See https://github.com/pydantic/pydantic-ai/issues/974 for more details.
46
+ openai_system_prompt_role = 'user' if model_name.startswith('o1-mini') else None
47
+
39
48
  return OpenAIModelProfile(
40
49
  json_schema_transformer=OpenAIJsonSchemaTransformer,
41
50
  supports_json_schema_output=True,
42
51
  supports_json_object_output=True,
43
52
  openai_supports_sampling_settings=not is_reasoning_model,
53
+ openai_system_prompt_role=openai_system_prompt_role,
44
54
  )
45
55
 
46
56
 
@@ -23,7 +23,7 @@ class OpenAIProvider(Provider[AsyncOpenAI]):
23
23
 
24
24
  @property
25
25
  def name(self) -> str:
26
- return 'openai' # pragma: no cover
26
+ return 'openai'
27
27
 
28
28
  @property
29
29
  def base_url(self) -> str:
pydantic_ai/result.py CHANGED
@@ -27,7 +27,7 @@ from .output import (
27
27
  OutputDataT,
28
28
  ToolOutput,
29
29
  )
30
- from .usage import Usage, UsageLimits
30
+ from .usage import RunUsage, UsageLimits
31
31
 
32
32
  __all__ = (
33
33
  'OutputDataT',
@@ -52,7 +52,7 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
52
52
  _tool_manager: ToolManager[AgentDepsT]
53
53
 
54
54
  _agent_stream_iterator: AsyncIterator[AgentStreamEvent] | None = field(default=None, init=False)
55
- _initial_run_ctx_usage: Usage = field(init=False)
55
+ _initial_run_ctx_usage: RunUsage = field(init=False)
56
56
 
57
57
  def __post_init__(self):
58
58
  self._initial_run_ctx_usage = copy(self._run_ctx.usage)
@@ -110,7 +110,7 @@ class AgentStream(Generic[AgentDepsT, OutputDataT]):
110
110
  """Get the current state of the response."""
111
111
  return self._raw_stream_response.get()
112
112
 
113
- def usage(self) -> Usage:
113
+ def usage(self) -> RunUsage:
114
114
  """Return the usage of the whole run.
115
115
 
116
116
  !!! note
@@ -382,7 +382,7 @@ class StreamedRunResult(Generic[AgentDepsT, OutputDataT]):
382
382
  await self._marked_completed(self._stream_response.get())
383
383
  return output
384
384
 
385
- def usage(self) -> Usage:
385
+ def usage(self) -> RunUsage:
386
386
  """Return the usage of the whole run.
387
387
 
388
388
  !!! note
@@ -425,7 +425,7 @@ class FinalResult(Generic[OutputDataT]):
425
425
  def _get_usage_checking_stream_response(
426
426
  stream_response: models.StreamedResponse,
427
427
  limits: UsageLimits | None,
428
- get_usage: Callable[[], Usage],
428
+ get_usage: Callable[[], RunUsage],
429
429
  ) -> AsyncIterator[AgentStreamEvent]:
430
430
  if limits is not None and limits.has_token_limits():
431
431
 
pydantic_ai/run.py CHANGED
@@ -66,9 +66,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
66
66
  CallToolsNode(
67
67
  model_response=ModelResponse(
68
68
  parts=[TextPart(content='The capital of France is Paris.')],
69
- usage=Usage(
70
- requests=1, request_tokens=56, response_tokens=7, total_tokens=63
71
- ),
69
+ usage=RequestUsage(input_tokens=56, output_tokens=7),
72
70
  model_name='gpt-4o',
73
71
  timestamp=datetime.datetime(...),
74
72
  )
@@ -203,12 +201,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
203
201
  CallToolsNode(
204
202
  model_response=ModelResponse(
205
203
  parts=[TextPart(content='The capital of France is Paris.')],
206
- usage=Usage(
207
- requests=1,
208
- request_tokens=56,
209
- response_tokens=7,
210
- total_tokens=63,
211
- ),
204
+ usage=RequestUsage(input_tokens=56, output_tokens=7),
212
205
  model_name='gpt-4o',
213
206
  timestamp=datetime.datetime(...),
214
207
  )
@@ -235,7 +228,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
235
228
  assert isinstance(next_node, End), f'Unexpected node type: {type(next_node)}'
236
229
  return next_node
237
230
 
238
- def usage(self) -> _usage.Usage:
231
+ def usage(self) -> _usage.RunUsage:
239
232
  """Get usage statistics for the run so far, including token usage, model requests, and so on."""
240
233
  return self._graph_run.state.usage
241
234
 
@@ -352,6 +345,6 @@ class AgentRunResult(Generic[OutputDataT]):
352
345
  self.new_messages(output_tool_return_content=output_tool_return_content)
353
346
  )
354
347
 
355
- def usage(self) -> _usage.Usage:
348
+ def usage(self) -> _usage.RunUsage:
356
349
  """Return the usage of the whole run."""
357
350
  return self._state.usage