pydantic-ai-slim 0.0.47__tar.gz → 0.0.48__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.47 → pydantic_ai_slim-0.0.48}/PKG-INFO +3 -3
  2. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_result.py +7 -3
  3. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/openai.py +409 -8
  4. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/tools.py +2 -2
  5. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pyproject.toml +3 -3
  6. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/.gitignore +0 -0
  7. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/README.md +0 -0
  8. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/__init__.py +0 -0
  9. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/__main__.py +0 -0
  10. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_agent_graph.py +0 -0
  11. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_cli.py +0 -0
  12. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_griffe.py +0 -0
  13. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_parts_manager.py +0 -0
  14. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_pydantic.py +0 -0
  15. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_system_prompt.py +0 -0
  16. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/_utils.py +0 -0
  17. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/agent.py +0 -0
  18. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/common_tools/__init__.py +0 -0
  19. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  20. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/common_tools/tavily.py +0 -0
  21. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/exceptions.py +0 -0
  22. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/format_as_xml.py +0 -0
  23. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/mcp.py +0 -0
  24. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/messages.py +0 -0
  25. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/__init__.py +0 -0
  26. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/anthropic.py +0 -0
  27. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/bedrock.py +0 -0
  28. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/cohere.py +0 -0
  29. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/fallback.py +0 -0
  30. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/function.py +0 -0
  31. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/gemini.py +0 -0
  32. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/groq.py +0 -0
  33. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/instrumented.py +0 -0
  34. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/mistral.py +0 -0
  35. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/test.py +0 -0
  36. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/models/wrapper.py +0 -0
  37. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/__init__.py +0 -0
  38. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/anthropic.py +0 -0
  39. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/azure.py +0 -0
  40. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/bedrock.py +0 -0
  41. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/cohere.py +0 -0
  42. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/deepseek.py +0 -0
  43. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/google_gla.py +0 -0
  44. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/google_vertex.py +0 -0
  45. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/groq.py +0 -0
  46. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/mistral.py +0 -0
  47. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/providers/openai.py +0 -0
  48. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/py.typed +0 -0
  49. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/result.py +0 -0
  50. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/pydantic_ai/settings.py +0 -0
  51. {pydantic_ai_slim-0.0.47 → pydantic_ai_slim-0.0.48}/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.0.47
3
+ Version: 0.0.48
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.47
32
+ Requires-Dist: pydantic-graph==0.0.48
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.41; extra == 'evals'
48
+ Requires-Dist: pydantic-evals==0.0.48; extra == 'evals'
49
49
  Provides-Extra: groq
50
50
  Requires-Dist: groq>=0.15.0; extra == 'groq'
51
51
  Provides-Extra: logfire
@@ -13,7 +13,7 @@ from typing_inspection.introspection import is_union_origin
13
13
  from . import _utils, messages as _messages
14
14
  from .exceptions import ModelRetry
15
15
  from .result import ResultDataT, ResultDataT_inv, ResultValidatorFunc
16
- from .tools import AgentDepsT, RunContext, ToolDefinition
16
+ from .tools import AgentDepsT, GenerateToolJsonSchema, RunContext, ToolDefinition
17
17
 
18
18
  T = TypeVar('T')
19
19
  """An invariant TypeVar."""
@@ -159,7 +159,9 @@ class ResultTool(Generic[ResultDataT]):
159
159
  self.type_adapter = TypeAdapter(response_type)
160
160
  outer_typed_dict_key: str | None = None
161
161
  # noinspection PyArgumentList
162
- parameters_json_schema = _utils.check_object_json_schema(self.type_adapter.json_schema())
162
+ parameters_json_schema = _utils.check_object_json_schema(
163
+ self.type_adapter.json_schema(schema_generator=GenerateToolJsonSchema)
164
+ )
163
165
  else:
164
166
  response_data_typed_dict = TypedDict( # noqa: UP013
165
167
  'response_data_typed_dict',
@@ -168,7 +170,9 @@ class ResultTool(Generic[ResultDataT]):
168
170
  self.type_adapter = TypeAdapter(response_data_typed_dict)
169
171
  outer_typed_dict_key = 'response'
170
172
  # noinspection PyArgumentList
171
- parameters_json_schema = _utils.check_object_json_schema(self.type_adapter.json_schema())
173
+ parameters_json_schema = _utils.check_object_json_schema(
174
+ self.type_adapter.json_schema(schema_generator=GenerateToolJsonSchema)
175
+ )
172
176
  # including `response_data_typed_dict` as a title here doesn't add anything and could confuse the LLM
173
177
  parameters_json_schema.pop('title')
174
178
 
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import base64
4
+ import warnings
4
5
  from collections.abc import AsyncIterable, AsyncIterator
5
6
  from contextlib import asynccontextmanager
6
7
  from dataclasses import dataclass, field
7
8
  from datetime import datetime, timezone
8
9
  from typing import Literal, Union, cast, overload
9
10
 
11
+ from openai import NotGiven
12
+ from openai.types import Reasoning
10
13
  from typing_extensions import assert_never
11
14
 
12
15
  from pydantic_ai.providers import Provider, infer_provider
@@ -42,7 +45,7 @@ from . import (
42
45
 
43
46
  try:
44
47
  from openai import NOT_GIVEN, APIStatusError, AsyncOpenAI, AsyncStream
45
- from openai.types import ChatModel, chat
48
+ from openai.types import ChatModel, chat, responses
46
49
  from openai.types.chat import (
47
50
  ChatCompletionChunk,
48
51
  ChatCompletionContentPartImageParam,
@@ -52,6 +55,9 @@ try:
52
55
  )
53
56
  from openai.types.chat.chat_completion_content_part_image_param import ImageURL
54
57
  from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio
58
+ from openai.types.responses.response_input_param import FunctionCallOutput, Message
59
+ from openai.types.shared import ReasoningEffort
60
+ from openai.types.shared_params import Reasoning
55
61
  except ImportError as _import_error:
56
62
  raise ImportError(
57
63
  'Please install `openai` to use the OpenAI model, '
@@ -79,9 +85,10 @@ class OpenAIModelSettings(ModelSettings, total=False):
79
85
  ALL FIELDS MUST BE `openai_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
80
86
  """
81
87
 
82
- openai_reasoning_effort: chat.ChatCompletionReasoningEffort
88
+ openai_reasoning_effort: ReasoningEffort
83
89
  """
84
90
  Constrains effort on reasoning for [reasoning models](https://platform.openai.com/docs/guides/reasoning).
91
+
85
92
  Currently supported values are `low`, `medium`, and `high`. Reducing reasoning effort can
86
93
  result in faster responses and fewer tokens used on reasoning in a response.
87
94
  """
@@ -178,8 +185,7 @@ class OpenAIModel(Model):
178
185
  stream: Literal[True],
179
186
  model_settings: OpenAIModelSettings,
180
187
  model_request_parameters: ModelRequestParameters,
181
- ) -> AsyncStream[ChatCompletionChunk]:
182
- pass
188
+ ) -> AsyncStream[ChatCompletionChunk]: ...
183
189
 
184
190
  @overload
185
191
  async def _completions_create(
@@ -188,8 +194,7 @@ class OpenAIModel(Model):
188
194
  stream: Literal[False],
189
195
  model_settings: OpenAIModelSettings,
190
196
  model_request_parameters: ModelRequestParameters,
191
- ) -> chat.ChatCompletion:
192
- pass
197
+ ) -> chat.ChatCompletion: ...
193
198
 
194
199
  async def _completions_create(
195
200
  self,
@@ -248,7 +253,7 @@ class OpenAIModel(Model):
248
253
  items.append(TextPart(choice.message.content))
249
254
  if choice.message.tool_calls is not None:
250
255
  for c in choice.message.tool_calls:
251
- items.append(ToolCallPart(c.function.name, c.function.arguments, c.id))
256
+ items.append(ToolCallPart(c.function.name, c.function.arguments, tool_call_id=c.id))
252
257
  return ModelResponse(items, model_name=response.model, timestamp=timestamp)
253
258
 
254
259
  async def _process_streamed_response(self, response: AsyncStream[ChatCompletionChunk]) -> OpenAIStreamedResponse:
@@ -399,6 +404,311 @@ class OpenAIModel(Model):
399
404
  return chat.ChatCompletionUserMessageParam(role='user', content=content)
400
405
 
401
406
 
407
+ @dataclass(init=False)
408
+ class OpenAIResponsesModel(Model):
409
+ """A model that uses the OpenAI Responses API.
410
+
411
+ The [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses) is the
412
+ new API for OpenAI models.
413
+
414
+ The Responses API has built-in tools, that you can use instead of building your own:
415
+
416
+ - [Web search](https://platform.openai.com/docs/guides/tools-web-search)
417
+ - [File search](https://platform.openai.com/docs/guides/tools-file-search)
418
+ - [Computer use](https://platform.openai.com/docs/guides/tools-computer-use)
419
+
420
+ If you are interested in the differences between the Responses API and the Chat Completions API,
421
+ see the [OpenAI API docs](https://platform.openai.com/docs/guides/responses-vs-chat-completions).
422
+ """
423
+
424
+ client: AsyncOpenAI = field(repr=False)
425
+ system_prompt_role: OpenAISystemPromptRole | None = field(default=None)
426
+
427
+ _model_name: OpenAIModelName = field(repr=False)
428
+ _system: str = field(default='openai', repr=False)
429
+
430
+ def __init__(
431
+ self,
432
+ model_name: OpenAIModelName,
433
+ *,
434
+ provider: Literal['openai', 'deepseek', 'azure'] | Provider[AsyncOpenAI] = 'openai',
435
+ ):
436
+ """Initialize an OpenAI Responses model.
437
+
438
+ Args:
439
+ model_name: The name of the OpenAI model to use.
440
+ provider: The provider to use. Defaults to `'openai'`.
441
+ """
442
+ self._model_name = model_name
443
+ if isinstance(provider, str):
444
+ provider = infer_provider(provider)
445
+ self.client = provider.client
446
+
447
+ @property
448
+ def model_name(self) -> OpenAIModelName:
449
+ """The model name."""
450
+ return self._model_name
451
+
452
+ @property
453
+ def system(self) -> str:
454
+ """The system / model provider."""
455
+ return self._system
456
+
457
+ async def request(
458
+ self,
459
+ messages: list[ModelRequest | ModelResponse],
460
+ model_settings: ModelSettings | None,
461
+ model_request_parameters: ModelRequestParameters,
462
+ ) -> tuple[ModelResponse, usage.Usage]:
463
+ check_allow_model_requests()
464
+ response = await self._responses_create(
465
+ messages, False, cast(OpenAIModelSettings, model_settings or {}), model_request_parameters
466
+ )
467
+ return self._process_response(response), _map_usage(response)
468
+
469
+ @asynccontextmanager
470
+ async def request_stream(
471
+ self,
472
+ messages: list[ModelMessage],
473
+ model_settings: ModelSettings | None,
474
+ model_request_parameters: ModelRequestParameters,
475
+ ) -> AsyncIterator[StreamedResponse]:
476
+ check_allow_model_requests()
477
+ response = await self._responses_create(
478
+ messages, True, cast(OpenAIModelSettings, model_settings or {}), model_request_parameters
479
+ )
480
+ async with response:
481
+ yield await self._process_streamed_response(response)
482
+
483
+ def _process_response(self, response: responses.Response) -> ModelResponse:
484
+ """Process a non-streamed response, and prepare a message to return."""
485
+ timestamp = datetime.fromtimestamp(response.created_at, tz=timezone.utc)
486
+ items: list[ModelResponsePart] = []
487
+ items.append(TextPart(response.output_text))
488
+ for item in response.output:
489
+ if item.type == 'function_call':
490
+ items.append(ToolCallPart(item.name, item.arguments, tool_call_id=item.call_id))
491
+ return ModelResponse(items, model_name=response.model, timestamp=timestamp)
492
+
493
+ async def _process_streamed_response(
494
+ self, response: AsyncStream[responses.ResponseStreamEvent]
495
+ ) -> OpenAIResponsesStreamedResponse:
496
+ """Process a streamed response, and prepare a streaming response to return."""
497
+ peekable_response = _utils.PeekableAsyncStream(response)
498
+ first_chunk = await peekable_response.peek()
499
+ if isinstance(first_chunk, _utils.Unset): # pragma: no cover
500
+ raise UnexpectedModelBehavior('Streamed response ended without content or tool calls')
501
+
502
+ assert isinstance(first_chunk, responses.ResponseCreatedEvent)
503
+ return OpenAIResponsesStreamedResponse(
504
+ _model_name=self._model_name,
505
+ _response=peekable_response,
506
+ _timestamp=datetime.fromtimestamp(first_chunk.response.created_at, tz=timezone.utc),
507
+ )
508
+
509
+ @overload
510
+ async def _responses_create(
511
+ self,
512
+ messages: list[ModelRequest | ModelResponse],
513
+ stream: Literal[False],
514
+ model_settings: OpenAIModelSettings,
515
+ model_request_parameters: ModelRequestParameters,
516
+ ) -> responses.Response: ...
517
+
518
+ @overload
519
+ async def _responses_create(
520
+ self,
521
+ messages: list[ModelRequest | ModelResponse],
522
+ stream: Literal[True],
523
+ model_settings: OpenAIModelSettings,
524
+ model_request_parameters: ModelRequestParameters,
525
+ ) -> AsyncStream[responses.ResponseStreamEvent]: ...
526
+
527
+ async def _responses_create(
528
+ self,
529
+ messages: list[ModelRequest | ModelResponse],
530
+ stream: bool,
531
+ model_settings: OpenAIModelSettings,
532
+ model_request_parameters: ModelRequestParameters,
533
+ ) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]:
534
+ tools = self._get_tools(model_request_parameters)
535
+
536
+ # standalone function to make it easier to override
537
+ if not tools:
538
+ tool_choice: Literal['none', 'required', 'auto'] | None = None
539
+ elif not model_request_parameters.allow_text_result:
540
+ tool_choice = 'required'
541
+ else:
542
+ tool_choice = 'auto'
543
+
544
+ 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
551
+
552
+ try:
553
+ return await self.client.responses.create(
554
+ input=openai_messages,
555
+ model=self._model_name,
556
+ instructions=system_prompt,
557
+ parallel_tool_calls=model_settings.get('parallel_tool_calls', NOT_GIVEN),
558
+ tools=tools or NOT_GIVEN,
559
+ tool_choice=tool_choice or NOT_GIVEN,
560
+ max_output_tokens=model_settings.get('max_tokens', NOT_GIVEN),
561
+ stream=stream,
562
+ temperature=model_settings.get('temperature', NOT_GIVEN),
563
+ top_p=model_settings.get('top_p', NOT_GIVEN),
564
+ timeout=model_settings.get('timeout', NOT_GIVEN),
565
+ reasoning=reasoning,
566
+ user=model_settings.get('user', NOT_GIVEN),
567
+ )
568
+ except APIStatusError as e:
569
+ if (status_code := e.status_code) >= 400:
570
+ raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
571
+ raise
572
+
573
+ def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.FunctionToolParam]:
574
+ tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
575
+ if model_request_parameters.result_tools:
576
+ tools += [self._map_tool_definition(r) for r in model_request_parameters.result_tools]
577
+ return tools
578
+
579
+ @staticmethod
580
+ def _map_tool_definition(f: ToolDefinition) -> responses.FunctionToolParam:
581
+ return {
582
+ 'name': f.name,
583
+ 'parameters': f.parameters_json_schema,
584
+ 'type': 'function',
585
+ 'description': f.description,
586
+ 'strict': True,
587
+ }
588
+
589
+ async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[responses.ResponseInputItemParam]]:
590
+ """Just maps a `pydantic_ai.Message` to a `openai.types.responses.ResponseInputParam`."""
591
+ system_prompt: str = ''
592
+ openai_messages: list[responses.ResponseInputItemParam] = []
593
+ for message in messages:
594
+ if isinstance(message, ModelRequest):
595
+ for part in message.parts:
596
+ if isinstance(part, SystemPromptPart):
597
+ system_prompt += part.content
598
+ elif isinstance(part, UserPromptPart):
599
+ openai_messages.append(await self._map_user_prompt(part))
600
+ elif isinstance(part, ToolReturnPart):
601
+ openai_messages.append(
602
+ FunctionCallOutput(
603
+ type='function_call_output',
604
+ call_id=_guard_tool_call_id(t=part),
605
+ output=part.model_response_str(),
606
+ )
607
+ )
608
+ elif isinstance(part, RetryPromptPart):
609
+ # TODO(Marcelo): How do we test this conditional branch?
610
+ if part.tool_name is None: # pragma: no cover
611
+ openai_messages.append(
612
+ Message(role='user', content=[{'type': 'input_text', 'text': part.model_response()}])
613
+ )
614
+ else:
615
+ openai_messages.append(
616
+ FunctionCallOutput(
617
+ type='function_call_output',
618
+ call_id=_guard_tool_call_id(t=part),
619
+ output=part.model_response(),
620
+ )
621
+ )
622
+ else:
623
+ assert_never(part)
624
+ elif isinstance(message, ModelResponse):
625
+ for item in message.parts:
626
+ if isinstance(item, TextPart):
627
+ openai_messages.append(responses.EasyInputMessageParam(role='assistant', content=item.content))
628
+ elif isinstance(item, ToolCallPart):
629
+ openai_messages.append(self._map_tool_call(item))
630
+ else:
631
+ assert_never(item)
632
+ else:
633
+ assert_never(message)
634
+ return system_prompt, openai_messages
635
+
636
+ @staticmethod
637
+ def _map_tool_call(t: ToolCallPart) -> responses.ResponseFunctionToolCallParam:
638
+ return responses.ResponseFunctionToolCallParam(
639
+ arguments=t.args_as_json_str(),
640
+ call_id=_guard_tool_call_id(t=t),
641
+ name=t.tool_name,
642
+ type='function_call',
643
+ )
644
+
645
+ @staticmethod
646
+ async def _map_user_prompt(part: UserPromptPart) -> responses.EasyInputMessageParam:
647
+ content: str | list[responses.ResponseInputContentParam]
648
+ if isinstance(part.content, str):
649
+ content = part.content
650
+ else:
651
+ content = []
652
+ for item in part.content:
653
+ if isinstance(item, str):
654
+ content.append(responses.ResponseInputTextParam(text=item, type='input_text'))
655
+ elif isinstance(item, BinaryContent):
656
+ base64_encoded = base64.b64encode(item.data).decode('utf-8')
657
+ if item.is_image:
658
+ content.append(
659
+ responses.ResponseInputImageParam(
660
+ image_url=f'data:{item.media_type};base64,{base64_encoded}',
661
+ type='input_image',
662
+ detail='auto',
663
+ )
664
+ )
665
+ elif item.is_document:
666
+ content.append(
667
+ responses.ResponseInputFileParam(
668
+ type='input_file',
669
+ file_data=f'data:{item.media_type};base64,{base64_encoded}',
670
+ # NOTE: Type wise it's not necessary to include the filename, but it's required by the
671
+ # API itself. If we add empty string, the server sends a 500 error - which OpenAI needs
672
+ # to fix. In any case, we add a placeholder name.
673
+ filename=f'filename.{item.format}',
674
+ )
675
+ )
676
+ elif item.is_audio:
677
+ raise NotImplementedError('Audio as binary content is not supported for OpenAI Responses API.')
678
+ else: # pragma: no cover
679
+ raise RuntimeError(f'Unsupported binary content type: {item.media_type}')
680
+ elif isinstance(item, ImageUrl):
681
+ content.append(
682
+ responses.ResponseInputImageParam(image_url=item.url, type='input_image', detail='auto')
683
+ )
684
+ elif isinstance(item, AudioUrl): # pragma: no cover
685
+ client = cached_async_http_client()
686
+ response = await client.get(item.url)
687
+ response.raise_for_status()
688
+ base64_encoded = base64.b64encode(response.content).decode('utf-8')
689
+ content.append(
690
+ responses.ResponseInputFileParam(
691
+ type='input_file',
692
+ file_data=f'data:{item.media_type};base64,{base64_encoded}',
693
+ )
694
+ )
695
+ elif isinstance(item, DocumentUrl): # pragma: no cover
696
+ client = cached_async_http_client()
697
+ response = await client.get(item.url)
698
+ response.raise_for_status()
699
+ base64_encoded = base64.b64encode(response.content).decode('utf-8')
700
+ content.append(
701
+ responses.ResponseInputFileParam(
702
+ type='input_file',
703
+ file_data=f'data:{item.media_type};base64,{base64_encoded}',
704
+ filename=f'filename.{item.format}',
705
+ )
706
+ )
707
+ else:
708
+ assert_never(item)
709
+ return responses.EasyInputMessageParam(role='user', content=content)
710
+
711
+
402
712
  @dataclass
403
713
  class OpenAIStreamedResponse(StreamedResponse):
404
714
  """Implementation of `StreamedResponse` for OpenAI models."""
@@ -442,10 +752,101 @@ class OpenAIStreamedResponse(StreamedResponse):
442
752
  return self._timestamp
443
753
 
444
754
 
445
- def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk) -> usage.Usage:
755
+ @dataclass
756
+ class OpenAIResponsesStreamedResponse(StreamedResponse):
757
+ """Implementation of `StreamedResponse` for OpenAI Responses API."""
758
+
759
+ _model_name: OpenAIModelName
760
+ _response: AsyncIterable[responses.ResponseStreamEvent]
761
+ _timestamp: datetime
762
+
763
+ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: # noqa: C901
764
+ async for chunk in self._response:
765
+ if isinstance(chunk, responses.ResponseCompletedEvent):
766
+ self._usage += _map_usage(chunk.response)
767
+
768
+ elif isinstance(chunk, responses.ResponseContentPartAddedEvent):
769
+ pass # there's nothing we need to do here
770
+
771
+ elif isinstance(chunk, responses.ResponseContentPartDoneEvent):
772
+ pass # there's nothing we need to do here
773
+
774
+ elif isinstance(chunk, responses.ResponseCreatedEvent):
775
+ pass # there's nothing we need to do here
776
+
777
+ elif isinstance(chunk, responses.ResponseFailedEvent): # pragma: no cover
778
+ self._usage += _map_usage(chunk.response)
779
+
780
+ elif isinstance(chunk, responses.ResponseFunctionCallArgumentsDeltaEvent):
781
+ maybe_event = self._parts_manager.handle_tool_call_delta(
782
+ vendor_part_id=chunk.item_id,
783
+ tool_name=None,
784
+ args=chunk.delta,
785
+ tool_call_id=chunk.item_id,
786
+ )
787
+ if maybe_event is not None:
788
+ yield maybe_event
789
+
790
+ elif isinstance(chunk, responses.ResponseFunctionCallArgumentsDoneEvent):
791
+ pass # there's nothing we need to do here
792
+
793
+ elif isinstance(chunk, responses.ResponseIncompleteEvent): # pragma: no cover
794
+ self._usage += _map_usage(chunk.response)
795
+
796
+ elif isinstance(chunk, responses.ResponseInProgressEvent):
797
+ self._usage += _map_usage(chunk.response)
798
+
799
+ elif isinstance(chunk, responses.ResponseOutputItemAddedEvent):
800
+ if isinstance(chunk.item, responses.ResponseFunctionToolCall):
801
+ yield self._parts_manager.handle_tool_call_part(
802
+ vendor_part_id=chunk.item.id,
803
+ tool_name=chunk.item.name,
804
+ args=chunk.item.arguments,
805
+ tool_call_id=chunk.item.id,
806
+ )
807
+
808
+ elif isinstance(chunk, responses.ResponseOutputItemDoneEvent):
809
+ # NOTE: We only need this if the tool call deltas don't include the final info.
810
+ pass
811
+
812
+ elif isinstance(chunk, responses.ResponseTextDeltaEvent):
813
+ yield self._parts_manager.handle_text_delta(vendor_part_id=chunk.content_index, content=chunk.delta)
814
+
815
+ elif isinstance(chunk, responses.ResponseTextDoneEvent):
816
+ pass # there's nothing we need to do here
817
+
818
+ else: # pragma: no cover
819
+ warnings.warn(
820
+ f'Handling of this event type is not yet implemented. Please report on our GitHub: {chunk}',
821
+ UserWarning,
822
+ )
823
+
824
+ @property
825
+ def model_name(self) -> OpenAIModelName:
826
+ """Get the model name of the response."""
827
+ return self._model_name
828
+
829
+ @property
830
+ def timestamp(self) -> datetime:
831
+ """Get the timestamp of the response."""
832
+ return self._timestamp
833
+
834
+
835
+ def _map_usage(response: chat.ChatCompletion | ChatCompletionChunk | responses.Response) -> usage.Usage:
446
836
  response_usage = response.usage
447
837
  if response_usage is None:
448
838
  return usage.Usage()
839
+ elif isinstance(response_usage, responses.ResponseUsage):
840
+ details: dict[str, int] = {}
841
+ return usage.Usage(
842
+ request_tokens=response_usage.input_tokens,
843
+ response_tokens=response_usage.output_tokens,
844
+ total_tokens=response_usage.total_tokens,
845
+ details={
846
+ 'reasoning_tokens': response_usage.output_tokens_details.reasoning_tokens,
847
+ 'cached_tokens': response_usage.input_tokens_details.cached_tokens,
848
+ },
849
+ )
449
850
  else:
450
851
  details: dict[str, int] = {}
451
852
  if response_usage.completion_tokens_details is not None:
@@ -149,8 +149,8 @@ class GenerateToolJsonSchema(GenerateJsonSchema):
149
149
  def typed_dict_schema(self, schema: core_schema.TypedDictSchema) -> JsonSchemaValue:
150
150
  s = super().typed_dict_schema(schema)
151
151
  total = schema.get('total')
152
- if total is not None:
153
- s['additionalProperties'] = not total
152
+ if 'additionalProperties' not in s and (total is True or total is None):
153
+ s['additionalProperties'] = False
154
154
  return s
155
155
 
156
156
  def _named_required_fields_schema(self, named_required_fields: Sequence[tuple[str, bool, Any]]) -> JsonSchemaValue:
@@ -12,7 +12,7 @@ bump = true
12
12
 
13
13
  [project]
14
14
  name = "pydantic-ai-slim"
15
- dynamic = ["version", "dependencies"]
15
+ dynamic = ["version", "dependencies", "optional-dependencies"]
16
16
  description = "Agent Framework / shim to use Pydantic with LLMs, slim package"
17
17
  authors = [{ name = "Samuel Colvin", email = "samuel@pydantic.dev" }]
18
18
  license = "MIT"
@@ -52,7 +52,7 @@ dependencies = [
52
52
  "typing-inspection>=0.4.0",
53
53
  ]
54
54
 
55
- [project.optional-dependencies]
55
+ [tool.hatch.metadata.hooks.uv-dynamic-versioning.optional-dependencies]
56
56
  # WARNING if you add optional groups, please update docs/install.md
57
57
  logfire = ["logfire>=3.11.0"]
58
58
  # Models
@@ -71,7 +71,7 @@ cli = ["rich>=13", "prompt-toolkit>=3", "argcomplete>=3.5.0"]
71
71
  # MCP
72
72
  mcp = ["mcp>=1.4.1; python_version >= '3.10'"]
73
73
  # Evals
74
- evals = ["pydantic-evals==0.0.41"]
74
+ evals = ["pydantic-evals=={{ version }}"]
75
75
 
76
76
  [dependency-groups]
77
77
  dev = [