pydantic-ai-slim 1.0.13__py3-none-any.whl → 1.0.15__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.

Potentially problematic release.


This version of pydantic-ai-slim might be problematic. Click here for more details.

Files changed (38) hide show
  1. pydantic_ai/__init__.py +19 -1
  2. pydantic_ai/_agent_graph.py +118 -97
  3. pydantic_ai/_cli.py +4 -7
  4. pydantic_ai/_output.py +236 -192
  5. pydantic_ai/_parts_manager.py +8 -42
  6. pydantic_ai/_tool_manager.py +9 -16
  7. pydantic_ai/agent/abstract.py +169 -1
  8. pydantic_ai/builtin_tools.py +82 -0
  9. pydantic_ai/direct.py +7 -0
  10. pydantic_ai/durable_exec/dbos/_agent.py +106 -3
  11. pydantic_ai/durable_exec/temporal/_agent.py +123 -6
  12. pydantic_ai/durable_exec/temporal/_model.py +8 -0
  13. pydantic_ai/format_prompt.py +4 -3
  14. pydantic_ai/mcp.py +20 -10
  15. pydantic_ai/messages.py +149 -3
  16. pydantic_ai/models/__init__.py +15 -1
  17. pydantic_ai/models/anthropic.py +7 -3
  18. pydantic_ai/models/cohere.py +4 -0
  19. pydantic_ai/models/function.py +7 -4
  20. pydantic_ai/models/gemini.py +8 -0
  21. pydantic_ai/models/google.py +56 -23
  22. pydantic_ai/models/groq.py +11 -5
  23. pydantic_ai/models/huggingface.py +5 -3
  24. pydantic_ai/models/mistral.py +6 -8
  25. pydantic_ai/models/openai.py +197 -58
  26. pydantic_ai/models/test.py +4 -0
  27. pydantic_ai/output.py +5 -2
  28. pydantic_ai/profiles/__init__.py +2 -0
  29. pydantic_ai/profiles/google.py +5 -2
  30. pydantic_ai/profiles/openai.py +2 -1
  31. pydantic_ai/result.py +46 -30
  32. pydantic_ai/run.py +35 -7
  33. pydantic_ai/usage.py +5 -4
  34. {pydantic_ai_slim-1.0.13.dist-info → pydantic_ai_slim-1.0.15.dist-info}/METADATA +3 -3
  35. {pydantic_ai_slim-1.0.13.dist-info → pydantic_ai_slim-1.0.15.dist-info}/RECORD +38 -38
  36. {pydantic_ai_slim-1.0.13.dist-info → pydantic_ai_slim-1.0.15.dist-info}/WHEEL +0 -0
  37. {pydantic_ai_slim-1.0.13.dist-info → pydantic_ai_slim-1.0.15.dist-info}/entry_points.txt +0 -0
  38. {pydantic_ai_slim-1.0.13.dist-info → pydantic_ai_slim-1.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -17,6 +17,7 @@ from typing_extensions import Never
17
17
 
18
18
  from pydantic_ai import (
19
19
  AbstractToolset,
20
+ AgentRunResultEvent,
20
21
  _utils,
21
22
  messages as _messages,
22
23
  models,
@@ -558,9 +559,8 @@ class TemporalAgent(WrapperAgent[AgentDepsT, OutputDataT]):
558
559
  """
559
560
  if workflow.in_workflow():
560
561
  raise UserError(
561
- '`agent.run_stream()` cannot currently be used inside a Temporal workflow. '
562
- 'Set an `event_stream_handler` on the agent and use `agent.run()` instead. '
563
- 'Please file an issue if this is not sufficient for your use case.'
562
+ '`agent.run_stream()` cannot be used inside a Temporal workflow. '
563
+ 'Set an `event_stream_handler` on the agent and use `agent.run()` instead.'
564
564
  )
565
565
 
566
566
  async with super().run_stream(
@@ -580,6 +580,124 @@ class TemporalAgent(WrapperAgent[AgentDepsT, OutputDataT]):
580
580
  ) as result:
581
581
  yield result
582
582
 
583
+ @overload
584
+ def run_stream_events(
585
+ self,
586
+ user_prompt: str | Sequence[_messages.UserContent] | None = None,
587
+ *,
588
+ output_type: None = None,
589
+ message_history: list[_messages.ModelMessage] | None = None,
590
+ deferred_tool_results: DeferredToolResults | None = None,
591
+ model: models.Model | models.KnownModelName | str | None = None,
592
+ deps: AgentDepsT = None,
593
+ model_settings: ModelSettings | None = None,
594
+ usage_limits: _usage.UsageLimits | None = None,
595
+ usage: _usage.RunUsage | None = None,
596
+ infer_name: bool = True,
597
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
598
+ ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[OutputDataT]]: ...
599
+
600
+ @overload
601
+ def run_stream_events(
602
+ self,
603
+ user_prompt: str | Sequence[_messages.UserContent] | None = None,
604
+ *,
605
+ output_type: OutputSpec[RunOutputDataT],
606
+ message_history: list[_messages.ModelMessage] | None = None,
607
+ deferred_tool_results: DeferredToolResults | None = None,
608
+ model: models.Model | models.KnownModelName | str | None = None,
609
+ deps: AgentDepsT = None,
610
+ model_settings: ModelSettings | None = None,
611
+ usage_limits: _usage.UsageLimits | None = None,
612
+ usage: _usage.RunUsage | None = None,
613
+ infer_name: bool = True,
614
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
615
+ ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[RunOutputDataT]]: ...
616
+
617
+ def run_stream_events(
618
+ self,
619
+ user_prompt: str | Sequence[_messages.UserContent] | None = None,
620
+ *,
621
+ output_type: OutputSpec[RunOutputDataT] | None = None,
622
+ message_history: list[_messages.ModelMessage] | None = None,
623
+ deferred_tool_results: DeferredToolResults | None = None,
624
+ model: models.Model | models.KnownModelName | str | None = None,
625
+ deps: AgentDepsT = None,
626
+ model_settings: ModelSettings | None = None,
627
+ usage_limits: _usage.UsageLimits | None = None,
628
+ usage: _usage.RunUsage | None = None,
629
+ infer_name: bool = True,
630
+ toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
631
+ ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]:
632
+ """Run the agent with a user prompt in async mode and stream events from the run.
633
+
634
+ This is a convenience method that wraps [`self.run`][pydantic_ai.agent.AbstractAgent.run] and
635
+ uses the `event_stream_handler` kwarg to get a stream of events from the run.
636
+
637
+ Example:
638
+ ```python
639
+ from pydantic_ai import Agent, AgentRunResultEvent, AgentStreamEvent
640
+
641
+ agent = Agent('openai:gpt-4o')
642
+
643
+ async def main():
644
+ events: list[AgentStreamEvent | AgentRunResultEvent] = []
645
+ async for event in agent.run_stream_events('What is the capital of France?'):
646
+ events.append(event)
647
+ print(events)
648
+ '''
649
+ [
650
+ PartStartEvent(index=0, part=TextPart(content='The capital of ')),
651
+ FinalResultEvent(tool_name=None, tool_call_id=None),
652
+ PartDeltaEvent(index=0, delta=TextPartDelta(content_delta='France is Paris. ')),
653
+ AgentRunResultEvent(
654
+ result=AgentRunResult(output='The capital of France is Paris. ')
655
+ ),
656
+ ]
657
+ '''
658
+ ```
659
+
660
+ Arguments are the same as for [`self.run`][pydantic_ai.agent.AbstractAgent.run],
661
+ except that `event_stream_handler` is now allowed.
662
+
663
+ Args:
664
+ user_prompt: User input to start/continue the conversation.
665
+ output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
666
+ output validators since output validators would expect an argument that matches the agent's output type.
667
+ message_history: History of the conversation so far.
668
+ deferred_tool_results: Optional results for deferred tool calls in the message history.
669
+ model: Optional model to use for this run, required if `model` was not set when creating the agent.
670
+ deps: Optional dependencies to use for this run.
671
+ model_settings: Optional settings to use for this model's request.
672
+ usage_limits: Optional limits on model request count or token usage.
673
+ usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
674
+ infer_name: Whether to try to infer the agent name from the call frame if it's not set.
675
+ toolsets: Optional additional toolsets for this run.
676
+
677
+ Returns:
678
+ An async iterable of stream events `AgentStreamEvent` and finally a `AgentRunResultEvent` with the final
679
+ run result.
680
+ """
681
+ if workflow.in_workflow():
682
+ raise UserError(
683
+ '`agent.run_stream_events()` cannot be used inside a Temporal workflow. '
684
+ 'Set an `event_stream_handler` on the agent and use `agent.run()` instead.'
685
+ )
686
+
687
+ return super().run_stream_events(
688
+ user_prompt,
689
+ output_type=output_type,
690
+ message_history=message_history,
691
+ deferred_tool_results=deferred_tool_results,
692
+ model=model,
693
+ deps=deps,
694
+ model_settings=model_settings,
695
+ usage_limits=usage_limits,
696
+ usage=usage,
697
+ infer_name=infer_name,
698
+ toolsets=toolsets,
699
+ )
700
+
583
701
  @overload
584
702
  def iter(
585
703
  self,
@@ -711,9 +829,8 @@ class TemporalAgent(WrapperAgent[AgentDepsT, OutputDataT]):
711
829
  if workflow.in_workflow():
712
830
  if not self._temporal_overrides_active.get():
713
831
  raise UserError(
714
- '`agent.iter()` cannot currently be used inside a Temporal workflow. '
715
- 'Set an `event_stream_handler` on the agent and use `agent.run()` instead. '
716
- 'Please file an issue if this is not sufficient for your use case.'
832
+ '`agent.iter()` cannot be used inside a Temporal workflow. '
833
+ 'Set an `event_stream_handler` on the agent and use `agent.run()` instead.'
717
834
  )
718
835
 
719
836
  if model is not None:
@@ -128,6 +128,8 @@ class TemporalModel(WrapperModel):
128
128
  if not workflow.in_workflow():
129
129
  return await super().request(messages, model_settings, model_request_parameters)
130
130
 
131
+ self._validate_model_request_parameters(model_request_parameters)
132
+
131
133
  return await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
132
134
  activity=self.request_activity,
133
135
  arg=_RequestParams(
@@ -163,6 +165,8 @@ class TemporalModel(WrapperModel):
163
165
  # and that only calls `request_stream` if `event_stream_handler` is set.
164
166
  assert self.event_stream_handler is not None
165
167
 
168
+ self._validate_model_request_parameters(model_request_parameters)
169
+
166
170
  serialized_run_context = self.run_context_type.serialize_run_context(run_context)
167
171
  response = await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
168
172
  activity=self.request_stream_activity,
@@ -178,3 +182,7 @@ class TemporalModel(WrapperModel):
178
182
  **self.activity_config,
179
183
  )
180
184
  yield TemporalStreamedResponse(model_request_parameters, response)
185
+
186
+ def _validate_model_request_parameters(self, model_request_parameters: ModelRequestParameters) -> None:
187
+ if model_request_parameters.allow_image_output:
188
+ raise UserError('Image output is not supported with Temporal because of the 2MB payload size limit.')
@@ -3,6 +3,7 @@ from __future__ import annotations as _annotations
3
3
  from collections.abc import Iterable, Iterator, Mapping
4
4
  from dataclasses import asdict, dataclass, field, fields, is_dataclass
5
5
  from datetime import date
6
+ from enum import Enum
6
7
  from typing import Any, Literal
7
8
  from xml.etree import ElementTree
8
9
 
@@ -26,8 +27,8 @@ def format_as_xml(
26
27
  This is useful since LLMs often find it easier to read semi-structured data (e.g. examples) as XML,
27
28
  rather than JSON etc.
28
29
 
29
- Supports: `str`, `bytes`, `bytearray`, `bool`, `int`, `float`, `date`, `datetime`, `Mapping`,
30
- `Iterable`, `dataclass`, and `BaseModel`.
30
+ Supports: `str`, `bytes`, `bytearray`, `bool`, `int`, `float`, `date`, `datetime`, `Enum`,
31
+ `Mapping`, `Iterable`, `dataclass`, and `BaseModel`.
31
32
 
32
33
  Args:
33
34
  obj: Python Object to serialize to XML.
@@ -101,7 +102,7 @@ class _ToXml:
101
102
  element.text = value
102
103
  elif isinstance(value, bytes | bytearray):
103
104
  element.text = value.decode(errors='ignore')
104
- elif isinstance(value, bool | int | float):
105
+ elif isinstance(value, bool | int | float | Enum):
105
106
  element.text = str(value)
106
107
  elif isinstance(value, date):
107
108
  element.text = value.isoformat()
pydantic_ai/mcp.py CHANGED
@@ -167,6 +167,10 @@ class MCPServer(AbstractToolset[Any], ABC):
167
167
  def id(self) -> str | None:
168
168
  return self._id
169
169
 
170
+ @id.setter
171
+ def id(self, value: str | None):
172
+ self._id = value
173
+
170
174
  @property
171
175
  def label(self) -> str:
172
176
  if self.id:
@@ -414,6 +418,9 @@ class MCPServer(AbstractToolset[Any], ABC):
414
418
  else:
415
419
  assert_never(resource)
416
420
 
421
+ def __eq__(self, value: object, /) -> bool:
422
+ return isinstance(value, MCPServer) and self.id == value.id and self.tool_prefix == value.tool_prefix
423
+
417
424
 
418
425
  class MCPServerStdio(MCPServer):
419
426
  """Runs an MCP server in a subprocess and communicates with it over stdin/stdout.
@@ -568,10 +575,10 @@ class MCPServerStdio(MCPServer):
568
575
  return f'{self.__class__.__name__}({", ".join(repr_args)})'
569
576
 
570
577
  def __eq__(self, value: object, /) -> bool:
571
- if not isinstance(value, MCPServerStdio):
572
- return False # pragma: no cover
573
578
  return (
574
- self.command == value.command
579
+ super().__eq__(value)
580
+ and isinstance(value, MCPServerStdio)
581
+ and self.command == value.command
575
582
  and self.args == value.args
576
583
  and self.env == value.env
577
584
  and self.cwd == value.cwd
@@ -809,9 +816,7 @@ class MCPServerSSE(_MCPServerHTTP):
809
816
  return sse_client # pragma: no cover
810
817
 
811
818
  def __eq__(self, value: object, /) -> bool:
812
- if not isinstance(value, MCPServerSSE):
813
- return False # pragma: no cover
814
- return self.url == value.url
819
+ return super().__eq__(value) and isinstance(value, MCPServerSSE) and self.url == value.url
815
820
 
816
821
 
817
822
  @deprecated('The `MCPServerHTTP` class is deprecated, use `MCPServerSSE` instead.')
@@ -885,9 +890,7 @@ class MCPServerStreamableHTTP(_MCPServerHTTP):
885
890
  return streamablehttp_client # pragma: no cover
886
891
 
887
892
  def __eq__(self, value: object, /) -> bool:
888
- if not isinstance(value, MCPServerStreamableHTTP):
889
- return False # pragma: no cover
890
- return self.url == value.url
893
+ return super().__eq__(value) and isinstance(value, MCPServerStreamableHTTP) and self.url == value.url
891
894
 
892
895
 
893
896
  ToolResult = (
@@ -964,4 +967,11 @@ def load_mcp_servers(config_path: str | Path) -> list[MCPServerStdio | MCPServer
964
967
  raise FileNotFoundError(f'Config file {config_path} not found')
965
968
 
966
969
  config = MCPServerConfig.model_validate_json(config_path.read_bytes())
967
- return list(config.mcp_servers.values())
970
+
971
+ servers: list[MCPServerStdio | MCPServerStreamableHTTP | MCPServerSSE] = []
972
+ for name, server in config.mcp_servers.items():
973
+ server.id = name
974
+ server.tool_prefix = name
975
+ servers.append(server)
976
+
977
+ return servers
pydantic_ai/messages.py CHANGED
@@ -13,7 +13,7 @@ import pydantic
13
13
  import pydantic_core
14
14
  from genai_prices import calc_price, types as genai_types
15
15
  from opentelemetry._events import Event # pyright: ignore[reportPrivateImportUsage]
16
- from typing_extensions import deprecated
16
+ from typing_extensions import Self, deprecated
17
17
 
18
18
  from . import _otel_messages, _utils
19
19
  from ._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc
@@ -461,7 +461,7 @@ class BinaryContent:
461
461
  """The media type of the binary data."""
462
462
 
463
463
  identifier: str
464
- """Identifier for the binary content, such as a unique ID. generating one from the data if not explicitly set
464
+ """Identifier for the binary content, such as a unique ID.
465
465
  This identifier can be provided to the model in a message to allow it to refer to this file in a tool call argument,
466
466
  and the tool can look up the file in question by iterating over the message history and finding the matching `BinaryContent`.
467
467
 
@@ -496,6 +496,33 @@ class BinaryContent:
496
496
  self.vendor_metadata = vendor_metadata
497
497
  self.kind = kind
498
498
 
499
+ @staticmethod
500
+ def narrow_type(bc: BinaryContent) -> BinaryContent | BinaryImage:
501
+ """Narrow the type of the `BinaryContent` to `BinaryImage` if it's an image."""
502
+ if bc.is_image:
503
+ return BinaryImage(
504
+ data=bc.data,
505
+ media_type=bc.media_type,
506
+ identifier=bc.identifier,
507
+ vendor_metadata=bc.vendor_metadata,
508
+ )
509
+ else:
510
+ return bc # pragma: no cover
511
+
512
+ @classmethod
513
+ def from_data_uri(cls, data_uri: str) -> Self:
514
+ """Create a `BinaryContent` from a data URI."""
515
+ prefix = 'data:'
516
+ if not data_uri.startswith(prefix):
517
+ raise ValueError('Data URI must start with "data:"') # pragma: no cover
518
+ media_type, data = data_uri[len(prefix) :].split(';base64,', 1)
519
+ return cls(data=base64.b64decode(data), media_type=media_type)
520
+
521
+ @property
522
+ def data_uri(self) -> str:
523
+ """Convert the `BinaryContent` to a data URI."""
524
+ return f'data:{self.media_type};base64,{base64.b64encode(self.data).decode()}'
525
+
499
526
  @property
500
527
  def is_audio(self) -> bool:
501
528
  """Return `True` if the media type is an audio type."""
@@ -534,6 +561,24 @@ class BinaryContent:
534
561
  __repr__ = _utils.dataclasses_no_defaults_repr
535
562
 
536
563
 
564
+ class BinaryImage(BinaryContent):
565
+ """Binary content that's guaranteed to be an image."""
566
+
567
+ def __init__(
568
+ self,
569
+ data: bytes,
570
+ *,
571
+ media_type: str,
572
+ identifier: str | None = None,
573
+ vendor_metadata: dict[str, Any] | None = None,
574
+ kind: Literal['binary'] = 'binary',
575
+ ):
576
+ super().__init__(data=data, media_type=media_type, identifier=identifier, vendor_metadata=vendor_metadata)
577
+
578
+ if not self.is_image:
579
+ raise ValueError('`BinaryImage` must be have a media type that starts with "image/"') # pragma: no cover
580
+
581
+
537
582
  MultiModalContent = ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent
538
583
  UserContent: TypeAlias = str | MultiModalContent
539
584
 
@@ -934,6 +979,32 @@ class ThinkingPart:
934
979
  __repr__ = _utils.dataclasses_no_defaults_repr
935
980
 
936
981
 
982
+ @dataclass(repr=False)
983
+ class FilePart:
984
+ """A file response from a model."""
985
+
986
+ content: Annotated[BinaryContent, pydantic.AfterValidator(BinaryImage.narrow_type)]
987
+ """The file content of the response."""
988
+
989
+ _: KW_ONLY
990
+
991
+ id: str | None = None
992
+ """The identifier of the file part."""
993
+
994
+ provider_name: str | None = None
995
+ """The name of the provider that generated the response.
996
+ """
997
+
998
+ part_kind: Literal['file'] = 'file'
999
+ """Part type identifier, this is available on all parts as a discriminator."""
1000
+
1001
+ def has_content(self) -> bool:
1002
+ """Return `True` if the file content is non-empty."""
1003
+ return bool(self.content) # pragma: no cover
1004
+
1005
+ __repr__ = _utils.dataclasses_no_defaults_repr
1006
+
1007
+
937
1008
  @dataclass(repr=False)
938
1009
  class BaseToolCallPart:
939
1010
  """A tool call from a model."""
@@ -1016,7 +1087,7 @@ class BuiltinToolCallPart(BaseToolCallPart):
1016
1087
 
1017
1088
 
1018
1089
  ModelResponsePart = Annotated[
1019
- TextPart | ToolCallPart | BuiltinToolCallPart | BuiltinToolReturnPart | ThinkingPart,
1090
+ TextPart | ToolCallPart | BuiltinToolCallPart | BuiltinToolReturnPart | ThinkingPart | FilePart,
1020
1091
  pydantic.Discriminator('part_kind'),
1021
1092
  ]
1022
1093
  """A message part returned by a model."""
@@ -1073,6 +1144,61 @@ class ModelResponse:
1073
1144
  finish_reason: FinishReason | None = None
1074
1145
  """Reason the model finished generating the response, normalized to OpenTelemetry values."""
1075
1146
 
1147
+ @property
1148
+ def text(self) -> str | None:
1149
+ """Get the text in the response."""
1150
+ texts: list[str] = []
1151
+ last_part: ModelResponsePart | None = None
1152
+ for part in self.parts:
1153
+ if isinstance(part, TextPart):
1154
+ # Adjacent text parts should be joined together, but if there are parts in between
1155
+ # (like built-in tool calls) they should have newlines between them
1156
+ if isinstance(last_part, TextPart):
1157
+ texts[-1] += part.content
1158
+ else:
1159
+ texts.append(part.content)
1160
+ last_part = part
1161
+ if not texts:
1162
+ return None
1163
+
1164
+ return '\n\n'.join(texts)
1165
+
1166
+ @property
1167
+ def thinking(self) -> str | None:
1168
+ """Get the thinking in the response."""
1169
+ thinking_parts = [part.content for part in self.parts if isinstance(part, ThinkingPart)]
1170
+ if not thinking_parts:
1171
+ return None
1172
+ return '\n\n'.join(thinking_parts)
1173
+
1174
+ @property
1175
+ def files(self) -> list[BinaryContent]:
1176
+ """Get the files in the response."""
1177
+ return [part.content for part in self.parts if isinstance(part, FilePart)]
1178
+
1179
+ @property
1180
+ def images(self) -> list[BinaryImage]:
1181
+ """Get the images in the response."""
1182
+ return [file for file in self.files if isinstance(file, BinaryImage)]
1183
+
1184
+ @property
1185
+ def tool_calls(self) -> list[ToolCallPart]:
1186
+ """Get the tool calls in the response."""
1187
+ return [part for part in self.parts if isinstance(part, ToolCallPart)]
1188
+
1189
+ @property
1190
+ def builtin_tool_calls(self) -> list[tuple[BuiltinToolCallPart, BuiltinToolReturnPart]]:
1191
+ """Get the builtin tool calls and results in the response."""
1192
+ calls = [part for part in self.parts if isinstance(part, BuiltinToolCallPart)]
1193
+ if not calls:
1194
+ return []
1195
+ returns_by_id = {part.tool_call_id: part for part in self.parts if isinstance(part, BuiltinToolReturnPart)}
1196
+ return [
1197
+ (call_part, returns_by_id[call_part.tool_call_id])
1198
+ for call_part in calls
1199
+ if call_part.tool_call_id in returns_by_id
1200
+ ]
1201
+
1076
1202
  @deprecated('`price` is deprecated, use `cost` instead')
1077
1203
  def price(self) -> genai_types.PriceCalculation: # pragma: no cover
1078
1204
  return self.cost()
@@ -1118,6 +1244,18 @@ class ModelResponse:
1118
1244
  body.setdefault('content', []).append(
1119
1245
  {'kind': kind, **({'text': part.content} if settings.include_content else {})}
1120
1246
  )
1247
+ elif isinstance(part, FilePart):
1248
+ body.setdefault('content', []).append(
1249
+ {
1250
+ 'kind': 'binary',
1251
+ 'media_type': part.content.media_type,
1252
+ **(
1253
+ {'binary_content': base64.b64encode(part.content.data).decode()}
1254
+ if settings.include_content and settings.include_binary_content
1255
+ else {}
1256
+ ),
1257
+ }
1258
+ )
1121
1259
 
1122
1260
  if content := body.get('content'):
1123
1261
  text_content = content[0].get('text')
@@ -1143,6 +1281,11 @@ class ModelResponse:
1143
1281
  **({'content': part.content} if settings.include_content else {}),
1144
1282
  )
1145
1283
  )
1284
+ elif isinstance(part, FilePart):
1285
+ converted_part = _otel_messages.BinaryDataPart(type='binary', media_type=part.content.media_type)
1286
+ if settings.include_content and settings.include_binary_content:
1287
+ converted_part['content'] = base64.b64encode(part.content.data).decode()
1288
+ parts.append(converted_part)
1146
1289
  elif isinstance(part, BaseToolCallPart):
1147
1290
  call_part = _otel_messages.ToolCallPart(type='tool_call', id=part.tool_call_id, name=part.tool_name)
1148
1291
  if isinstance(part, BuiltinToolCallPart):
@@ -1511,6 +1654,9 @@ class FunctionToolResultEvent:
1511
1654
 
1512
1655
  _: KW_ONLY
1513
1656
 
1657
+ content: str | Sequence[UserContent] | None = None
1658
+ """The content that will be sent to the model as a UserPromptPart following the result."""
1659
+
1514
1660
  event_kind: Literal['function_tool_result'] = 'function_tool_result'
1515
1661
  """Event type identifier, used as a discriminator."""
1516
1662
 
@@ -27,6 +27,8 @@ from .._run_context import RunContext
27
27
  from ..builtin_tools import AbstractBuiltinTool
28
28
  from ..exceptions import UserError
29
29
  from ..messages import (
30
+ BinaryImage,
31
+ FilePart,
30
32
  FileUrl,
31
33
  FinalResultEvent,
32
34
  FinishReason,
@@ -141,12 +143,20 @@ KnownModelName = TypeAliasType(
141
143
  'google-gla:gemini-2.0-flash',
142
144
  'google-gla:gemini-2.0-flash-lite',
143
145
  'google-gla:gemini-2.5-flash',
146
+ 'google-gla:gemini-2.5-flash-preview-09-2025',
147
+ 'google-gla:gemini-flash-latest',
144
148
  'google-gla:gemini-2.5-flash-lite',
149
+ 'google-gla:gemini-2.5-flash-lite-preview-09-2025',
150
+ 'google-gla:gemini-flash-lite-latest',
145
151
  'google-gla:gemini-2.5-pro',
146
152
  'google-vertex:gemini-2.0-flash',
147
153
  'google-vertex:gemini-2.0-flash-lite',
148
154
  'google-vertex:gemini-2.5-flash',
155
+ 'google-vertex:gemini-2.5-flash-preview-09-2025',
156
+ 'google-vertex:gemini-flash-latest',
149
157
  'google-vertex:gemini-2.5-flash-lite',
158
+ 'google-vertex:gemini-2.5-flash-lite-preview-09-2025',
159
+ 'google-vertex:gemini-flash-lite-latest',
150
160
  'google-vertex:gemini-2.5-pro',
151
161
  'grok:grok-4',
152
162
  'grok:grok-4-0709',
@@ -300,6 +310,7 @@ class ModelRequestParameters:
300
310
  output_object: OutputObjectDefinition | None = None
301
311
  output_tools: list[ToolDefinition] = field(default_factory=list)
302
312
  allow_text_output: bool = True
313
+ allow_image_output: bool = False
303
314
 
304
315
  @cached_property
305
316
  def tool_defs(self) -> dict[str, ToolDefinition]:
@@ -557,6 +568,7 @@ class StreamedResponse(ABC):
557
568
  finish_reason=self.finish_reason,
558
569
  )
559
570
 
571
+ # TODO (v2): Make this a property
560
572
  def usage(self) -> RequestUsage:
561
573
  """Get the usage of the response so far. This will not be the final usage until the stream is exhausted."""
562
574
  return self._usage
@@ -865,7 +877,9 @@ def _get_final_result_event(e: ModelResponseStreamEvent, params: ModelRequestPar
865
877
  """Return an appropriate FinalResultEvent if `e` corresponds to a part that will produce a final result."""
866
878
  if isinstance(e, PartStartEvent):
867
879
  new_part = e.part
868
- if isinstance(new_part, TextPart) and params.allow_text_output: # pragma: no branch
880
+ if (isinstance(new_part, TextPart) and params.allow_text_output) or (
881
+ isinstance(new_part, FilePart) and params.allow_image_output and isinstance(new_part.content, BinaryImage)
882
+ ):
869
883
  return FinalResultEvent(tool_name=None, tool_call_id=None)
870
884
  elif isinstance(new_part, ToolCallPart) and (tool_def := params.tool_defs.get(new_part.tool_name)):
871
885
  if tool_def.kind == 'output':
@@ -20,6 +20,7 @@ from ..messages import (
20
20
  BuiltinToolCallPart,
21
21
  BuiltinToolReturnPart,
22
22
  DocumentUrl,
23
+ FilePart,
23
24
  FinishReason,
24
25
  ImageUrl,
25
26
  ModelMessage,
@@ -545,6 +546,9 @@ class AnthropicModel(Model):
545
546
  ),
546
547
  )
547
548
  )
549
+ elif isinstance(response_part, FilePart): # pragma: no cover
550
+ # Files generated by models are not sent back to models that don't themselves generate files.
551
+ pass
548
552
  else:
549
553
  assert_never(response_part)
550
554
  if len(assistant_content_params) > 0:
@@ -693,17 +697,17 @@ class AnthropicStreamedResponse(StreamedResponse):
693
697
  if maybe_event is not None: # pragma: no branch
694
698
  yield maybe_event
695
699
  elif isinstance(current_block, BetaServerToolUseBlock):
696
- yield self._parts_manager.handle_builtin_tool_call_part(
700
+ yield self._parts_manager.handle_part(
697
701
  vendor_part_id=event.index,
698
702
  part=_map_server_tool_use_block(current_block, self.provider_name),
699
703
  )
700
704
  elif isinstance(current_block, BetaWebSearchToolResultBlock):
701
- yield self._parts_manager.handle_builtin_tool_return_part(
705
+ yield self._parts_manager.handle_part(
702
706
  vendor_part_id=event.index,
703
707
  part=_map_web_search_tool_result_block(current_block, self.provider_name),
704
708
  )
705
709
  elif isinstance(current_block, BetaCodeExecutionToolResultBlock):
706
- yield self._parts_manager.handle_builtin_tool_return_part(
710
+ yield self._parts_manager.handle_part(
707
711
  vendor_part_id=event.index,
708
712
  part=_map_code_execution_tool_result_block(current_block, self.provider_name),
709
713
  )
@@ -13,6 +13,7 @@ from .._utils import generate_tool_call_id as _generate_tool_call_id, guard_tool
13
13
  from ..messages import (
14
14
  BuiltinToolCallPart,
15
15
  BuiltinToolReturnPart,
16
+ FilePart,
16
17
  FinishReason,
17
18
  ModelMessage,
18
19
  ModelRequest,
@@ -255,6 +256,9 @@ class CohereModel(Model):
255
256
  elif isinstance(item, BuiltinToolCallPart | BuiltinToolReturnPart): # pragma: no cover
256
257
  # This is currently never returned from cohere
257
258
  pass
259
+ elif isinstance(item, FilePart): # pragma: no cover
260
+ # Files generated by models are not sent back to models that don't themselves generate files.
261
+ pass
258
262
  else:
259
263
  assert_never(item)
260
264
 
@@ -18,6 +18,7 @@ from ..messages import (
18
18
  BinaryContent,
19
19
  BuiltinToolCallPart,
20
20
  BuiltinToolReturnPart,
21
+ FilePart,
21
22
  ModelMessage,
22
23
  ModelRequest,
23
24
  ModelResponse,
@@ -319,12 +320,12 @@ class FunctionStreamedResponse(StreamedResponse):
319
320
  if content := delta.args_as_json_str(): # pragma: no branch
320
321
  response_tokens = _estimate_string_tokens(content)
321
322
  self._usage += usage.RequestUsage(output_tokens=response_tokens)
322
- yield self._parts_manager.handle_builtin_tool_call_part(vendor_part_id=dtc_index, part=delta)
323
+ yield self._parts_manager.handle_part(vendor_part_id=dtc_index, part=delta)
323
324
  elif isinstance(delta, BuiltinToolReturnPart):
324
325
  if content := delta.model_response_str(): # pragma: no branch
325
326
  response_tokens = _estimate_string_tokens(content)
326
327
  self._usage += usage.RequestUsage(output_tokens=response_tokens)
327
- yield self._parts_manager.handle_builtin_tool_return_part(vendor_part_id=dtc_index, part=delta)
328
+ yield self._parts_manager.handle_part(vendor_part_id=dtc_index, part=delta)
328
329
  else:
329
330
  assert_never(delta)
330
331
 
@@ -371,10 +372,12 @@ def _estimate_usage(messages: Iterable[ModelMessage]) -> usage.RequestUsage:
371
372
  response_tokens += _estimate_string_tokens(part.content)
372
373
  elif isinstance(part, ToolCallPart):
373
374
  response_tokens += 1 + _estimate_string_tokens(part.args_as_json_str())
374
- elif isinstance(part, BuiltinToolCallPart): # pragma: no cover
375
+ elif isinstance(part, BuiltinToolCallPart):
375
376
  response_tokens += 1 + _estimate_string_tokens(part.args_as_json_str())
376
- elif isinstance(part, BuiltinToolReturnPart): # pragma: no cover
377
+ elif isinstance(part, BuiltinToolReturnPart):
377
378
  response_tokens += _estimate_string_tokens(part.model_response_str())
379
+ elif isinstance(part, FilePart):
380
+ response_tokens += _estimate_string_tokens([part.content])
378
381
  else:
379
382
  assert_never(part)
380
383
  else: