pydantic-ai-slim 0.2.20__py3-none-any.whl → 0.3.0__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.
- pydantic_ai/_agent_graph.py +7 -1
- pydantic_ai/_parts_manager.py +74 -2
- pydantic_ai/_thinking_part.py +36 -0
- pydantic_ai/messages.py +84 -3
- pydantic_ai/models/anthropic.py +53 -9
- pydantic_ai/models/bedrock.py +23 -4
- pydantic_ai/models/cohere.py +9 -1
- pydantic_ai/models/function.py +5 -0
- pydantic_ai/models/gemini.py +33 -8
- pydantic_ai/models/google.py +14 -2
- pydantic_ai/models/groq.py +13 -2
- pydantic_ai/models/instrumented.py +1 -1
- pydantic_ai/models/mistral.py +9 -1
- pydantic_ai/models/openai.py +84 -5
- pydantic_ai/models/test.py +9 -6
- {pydantic_ai_slim-0.2.20.dist-info → pydantic_ai_slim-0.3.0.dist-info}/METADATA +7 -7
- {pydantic_ai_slim-0.2.20.dist-info → pydantic_ai_slim-0.3.0.dist-info}/RECORD +20 -19
- {pydantic_ai_slim-0.2.20.dist-info → pydantic_ai_slim-0.3.0.dist-info}/WHEEL +0 -0
- {pydantic_ai_slim-0.2.20.dist-info → pydantic_ai_slim-0.3.0.dist-info}/entry_points.txt +0 -0
- {pydantic_ai_slim-0.2.20.dist-info → pydantic_ai_slim-0.3.0.dist-info}/licenses/LICENSE +0 -0
pydantic_ai/_agent_graph.py
CHANGED
|
@@ -442,7 +442,7 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
442
442
|
async for _event in stream:
|
|
443
443
|
pass
|
|
444
444
|
|
|
445
|
-
async def _run_stream(
|
|
445
|
+
async def _run_stream( # noqa: C901
|
|
446
446
|
self, ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]]
|
|
447
447
|
) -> AsyncIterator[_messages.HandleResponseEvent]:
|
|
448
448
|
if self._events_iterator is None:
|
|
@@ -458,6 +458,12 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
|
|
|
458
458
|
texts.append(part.content)
|
|
459
459
|
elif isinstance(part, _messages.ToolCallPart):
|
|
460
460
|
tool_calls.append(part)
|
|
461
|
+
elif isinstance(part, _messages.ThinkingPart):
|
|
462
|
+
# We don't need to do anything with thinking parts in this tool-calling node.
|
|
463
|
+
# We need to handle text parts in case there are no tool calls and/or the desired output comes
|
|
464
|
+
# from the text, but thinking parts should not directly influence the execution of tools or
|
|
465
|
+
# determination of the next node of graph execution here.
|
|
466
|
+
pass
|
|
461
467
|
else:
|
|
462
468
|
assert_never(part)
|
|
463
469
|
|
pydantic_ai/_parts_manager.py
CHANGED
|
@@ -25,6 +25,8 @@ from pydantic_ai.messages import (
|
|
|
25
25
|
PartStartEvent,
|
|
26
26
|
TextPart,
|
|
27
27
|
TextPartDelta,
|
|
28
|
+
ThinkingPart,
|
|
29
|
+
ThinkingPartDelta,
|
|
28
30
|
ToolCallPart,
|
|
29
31
|
ToolCallPartDelta,
|
|
30
32
|
)
|
|
@@ -86,8 +88,7 @@ class ModelResponsePartsManager:
|
|
|
86
88
|
A `PartStartEvent` if a new part was created, or a `PartDeltaEvent` if an existing part was updated.
|
|
87
89
|
|
|
88
90
|
Raises:
|
|
89
|
-
UnexpectedModelBehavior: If attempting to apply text content to a part that is
|
|
90
|
-
not a TextPart.
|
|
91
|
+
UnexpectedModelBehavior: If attempting to apply text content to a part that is not a TextPart.
|
|
91
92
|
"""
|
|
92
93
|
existing_text_part_and_index: tuple[TextPart, int] | None = None
|
|
93
94
|
|
|
@@ -122,6 +123,77 @@ class ModelResponsePartsManager:
|
|
|
122
123
|
self._parts[part_index] = part_delta.apply(existing_text_part)
|
|
123
124
|
return PartDeltaEvent(index=part_index, delta=part_delta)
|
|
124
125
|
|
|
126
|
+
def handle_thinking_delta(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
vendor_part_id: Hashable | None,
|
|
130
|
+
content: str | None = None,
|
|
131
|
+
signature: str | None = None,
|
|
132
|
+
) -> ModelResponseStreamEvent:
|
|
133
|
+
"""Handle incoming thinking content, creating or updating a ThinkingPart in the manager as appropriate.
|
|
134
|
+
|
|
135
|
+
When `vendor_part_id` is None, the latest part is updated if it exists and is a ThinkingPart;
|
|
136
|
+
otherwise, a new ThinkingPart is created. When a non-None ID is specified, the ThinkingPart corresponding
|
|
137
|
+
to that vendor ID is either created or updated.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
vendor_part_id: The ID the vendor uses to identify this piece
|
|
141
|
+
of thinking. If None, a new part will be created unless the latest part is already
|
|
142
|
+
a ThinkingPart.
|
|
143
|
+
content: The thinking content to append to the appropriate ThinkingPart.
|
|
144
|
+
signature: An optional signature for the thinking content.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A `PartStartEvent` if a new part was created, or a `PartDeltaEvent` if an existing part was updated.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
UnexpectedModelBehavior: If attempting to apply a thinking delta to a part that is not a ThinkingPart.
|
|
151
|
+
"""
|
|
152
|
+
existing_thinking_part_and_index: tuple[ThinkingPart, int] | None = None
|
|
153
|
+
|
|
154
|
+
if vendor_part_id is None:
|
|
155
|
+
# If the vendor_part_id is None, check if the latest part is a ThinkingPart to update
|
|
156
|
+
if self._parts:
|
|
157
|
+
part_index = len(self._parts) - 1
|
|
158
|
+
latest_part = self._parts[part_index]
|
|
159
|
+
if isinstance(latest_part, ThinkingPart): # pragma: no branch
|
|
160
|
+
existing_thinking_part_and_index = latest_part, part_index
|
|
161
|
+
else:
|
|
162
|
+
# Otherwise, attempt to look up an existing ThinkingPart by vendor_part_id
|
|
163
|
+
part_index = self._vendor_id_to_part_index.get(vendor_part_id)
|
|
164
|
+
if part_index is not None:
|
|
165
|
+
existing_part = self._parts[part_index]
|
|
166
|
+
if not isinstance(existing_part, ThinkingPart):
|
|
167
|
+
raise UnexpectedModelBehavior(f'Cannot apply a thinking delta to {existing_part=}')
|
|
168
|
+
existing_thinking_part_and_index = existing_part, part_index
|
|
169
|
+
|
|
170
|
+
if existing_thinking_part_and_index is None:
|
|
171
|
+
if content is not None:
|
|
172
|
+
# There is no existing thinking part that should be updated, so create a new one
|
|
173
|
+
new_part_index = len(self._parts)
|
|
174
|
+
part = ThinkingPart(content=content, signature=signature)
|
|
175
|
+
if vendor_part_id is not None: # pragma: no branch
|
|
176
|
+
self._vendor_id_to_part_index[vendor_part_id] = new_part_index
|
|
177
|
+
self._parts.append(part)
|
|
178
|
+
return PartStartEvent(index=new_part_index, part=part)
|
|
179
|
+
else:
|
|
180
|
+
raise UnexpectedModelBehavior('Cannot create a ThinkingPart with no content')
|
|
181
|
+
else:
|
|
182
|
+
if content is not None:
|
|
183
|
+
# Update the existing ThinkingPart with the new content delta
|
|
184
|
+
existing_thinking_part, part_index = existing_thinking_part_and_index
|
|
185
|
+
part_delta = ThinkingPartDelta(content_delta=content)
|
|
186
|
+
self._parts[part_index] = part_delta.apply(existing_thinking_part)
|
|
187
|
+
return PartDeltaEvent(index=part_index, delta=part_delta)
|
|
188
|
+
elif signature is not None:
|
|
189
|
+
# Update the existing ThinkingPart with the new signature delta
|
|
190
|
+
existing_thinking_part, part_index = existing_thinking_part_and_index
|
|
191
|
+
part_delta = ThinkingPartDelta(signature_delta=signature)
|
|
192
|
+
self._parts[part_index] = part_delta.apply(existing_thinking_part)
|
|
193
|
+
return PartDeltaEvent(index=part_index, delta=part_delta)
|
|
194
|
+
else:
|
|
195
|
+
raise UnexpectedModelBehavior('Cannot update a ThinkingPart with no content or signature')
|
|
196
|
+
|
|
125
197
|
def handle_tool_call_delta(
|
|
126
198
|
self,
|
|
127
199
|
*,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
2
|
+
|
|
3
|
+
from pydantic_ai.messages import TextPart, ThinkingPart
|
|
4
|
+
|
|
5
|
+
START_THINK_TAG = '<think>'
|
|
6
|
+
END_THINK_TAG = '</think>'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def split_content_into_text_and_thinking(content: str) -> list[ThinkingPart | TextPart]:
|
|
10
|
+
"""Split a string into text and thinking parts.
|
|
11
|
+
|
|
12
|
+
Some models don't return the thinking part as a separate part, but rather as a tag in the content.
|
|
13
|
+
This function splits the content into text and thinking parts.
|
|
14
|
+
|
|
15
|
+
We use the `<think>` tag because that's how Groq uses it in the `raw` format, so instead of using `<Thinking>` or
|
|
16
|
+
something else, we just match the tag to make it easier for other models that don't support the `ThinkingPart`.
|
|
17
|
+
"""
|
|
18
|
+
parts: list[ThinkingPart | TextPart] = []
|
|
19
|
+
|
|
20
|
+
start_index = content.find(START_THINK_TAG)
|
|
21
|
+
while start_index >= 0:
|
|
22
|
+
before_think, content = content[:start_index], content[start_index + len(START_THINK_TAG) :]
|
|
23
|
+
if before_think:
|
|
24
|
+
parts.append(TextPart(content=before_think))
|
|
25
|
+
end_index = content.find(END_THINK_TAG)
|
|
26
|
+
if end_index >= 0:
|
|
27
|
+
think_content, content = content[:end_index], content[end_index + len(END_THINK_TAG) :]
|
|
28
|
+
parts.append(ThinkingPart(content=think_content))
|
|
29
|
+
else:
|
|
30
|
+
# We lose the `<think>` tag, but it shouldn't matter.
|
|
31
|
+
parts.append(TextPart(content=content))
|
|
32
|
+
content = ''
|
|
33
|
+
start_index = content.find(START_THINK_TAG)
|
|
34
|
+
if content:
|
|
35
|
+
parts.append(TextPart(content=content))
|
|
36
|
+
return parts
|
pydantic_ai/messages.py
CHANGED
|
@@ -14,7 +14,10 @@ from opentelemetry._events import Event # pyright: ignore[reportPrivateImportUs
|
|
|
14
14
|
from typing_extensions import TypeAlias
|
|
15
15
|
|
|
16
16
|
from . import _utils
|
|
17
|
-
from ._utils import
|
|
17
|
+
from ._utils import (
|
|
18
|
+
generate_tool_call_id as _generate_tool_call_id,
|
|
19
|
+
now_utc as _now_utc,
|
|
20
|
+
)
|
|
18
21
|
from .exceptions import UnexpectedModelBehavior
|
|
19
22
|
from .usage import Usage
|
|
20
23
|
|
|
@@ -531,6 +534,32 @@ class TextPart:
|
|
|
531
534
|
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
532
535
|
|
|
533
536
|
|
|
537
|
+
@dataclass(repr=False)
|
|
538
|
+
class ThinkingPart:
|
|
539
|
+
"""A thinking response from a model."""
|
|
540
|
+
|
|
541
|
+
content: str
|
|
542
|
+
"""The thinking content of the response."""
|
|
543
|
+
|
|
544
|
+
id: str | None = None
|
|
545
|
+
"""The identifier of the thinking part."""
|
|
546
|
+
|
|
547
|
+
signature: str | None = None
|
|
548
|
+
"""The signature of the thinking.
|
|
549
|
+
|
|
550
|
+
The signature is only available on the Anthropic models.
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
part_kind: Literal['thinking'] = 'thinking'
|
|
554
|
+
"""Part type identifier, this is available on all parts as a discriminator."""
|
|
555
|
+
|
|
556
|
+
def has_content(self) -> bool:
|
|
557
|
+
"""Return `True` if the thinking content is non-empty."""
|
|
558
|
+
return bool(self.content) # pragma: no cover
|
|
559
|
+
|
|
560
|
+
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
561
|
+
|
|
562
|
+
|
|
534
563
|
@dataclass(repr=False)
|
|
535
564
|
class ToolCallPart:
|
|
536
565
|
"""A tool call from a model."""
|
|
@@ -589,7 +618,7 @@ class ToolCallPart:
|
|
|
589
618
|
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
590
619
|
|
|
591
620
|
|
|
592
|
-
ModelResponsePart = Annotated[Union[TextPart, ToolCallPart], pydantic.Discriminator('part_kind')]
|
|
621
|
+
ModelResponsePart = Annotated[Union[TextPart, ToolCallPart, ThinkingPart], pydantic.Discriminator('part_kind')]
|
|
593
622
|
"""A message part returned by a model."""
|
|
594
623
|
|
|
595
624
|
|
|
@@ -699,6 +728,56 @@ class TextPartDelta:
|
|
|
699
728
|
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
700
729
|
|
|
701
730
|
|
|
731
|
+
@dataclass(repr=False)
|
|
732
|
+
class ThinkingPartDelta:
|
|
733
|
+
"""A partial update (delta) for a `ThinkingPart` to append new thinking content."""
|
|
734
|
+
|
|
735
|
+
content_delta: str | None = None
|
|
736
|
+
"""The incremental thinking content to add to the existing `ThinkingPart` content."""
|
|
737
|
+
|
|
738
|
+
signature_delta: str | None = None
|
|
739
|
+
"""Optional signature delta.
|
|
740
|
+
|
|
741
|
+
Note this is never treated as a delta — it can replace None.
|
|
742
|
+
"""
|
|
743
|
+
|
|
744
|
+
part_delta_kind: Literal['thinking'] = 'thinking'
|
|
745
|
+
"""Part delta type identifier, used as a discriminator."""
|
|
746
|
+
|
|
747
|
+
@overload
|
|
748
|
+
def apply(self, part: ModelResponsePart) -> ThinkingPart: ...
|
|
749
|
+
|
|
750
|
+
@overload
|
|
751
|
+
def apply(self, part: ModelResponsePart | ThinkingPartDelta) -> ThinkingPart | ThinkingPartDelta: ...
|
|
752
|
+
|
|
753
|
+
def apply(self, part: ModelResponsePart | ThinkingPartDelta) -> ThinkingPart | ThinkingPartDelta:
|
|
754
|
+
"""Apply this thinking delta to an existing `ThinkingPart`.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
part: The existing model response part, which must be a `ThinkingPart`.
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
A new `ThinkingPart` with updated thinking content.
|
|
761
|
+
|
|
762
|
+
Raises:
|
|
763
|
+
ValueError: If `part` is not a `ThinkingPart`.
|
|
764
|
+
"""
|
|
765
|
+
if isinstance(part, ThinkingPart):
|
|
766
|
+
return replace(part, content=part.content + self.content_delta if self.content_delta else None)
|
|
767
|
+
elif isinstance(part, ThinkingPartDelta):
|
|
768
|
+
if self.content_delta is None and self.signature_delta is None:
|
|
769
|
+
raise ValueError('Cannot apply ThinkingPartDelta with no content or signature')
|
|
770
|
+
if self.signature_delta is not None:
|
|
771
|
+
return replace(part, signature_delta=self.signature_delta)
|
|
772
|
+
if self.content_delta is not None:
|
|
773
|
+
return replace(part, content_delta=self.content_delta)
|
|
774
|
+
raise ValueError( # pragma: no cover
|
|
775
|
+
f'Cannot apply ThinkingPartDeltas to non-ThinkingParts or non-ThinkingPartDeltas ({part=}, {self=})'
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
779
|
+
|
|
780
|
+
|
|
702
781
|
@dataclass(repr=False)
|
|
703
782
|
class ToolCallPartDelta:
|
|
704
783
|
"""A partial update (delta) for a `ToolCallPart` to modify tool name, arguments, or tool call ID."""
|
|
@@ -818,7 +897,9 @@ class ToolCallPartDelta:
|
|
|
818
897
|
__repr__ = _utils.dataclasses_no_defaults_repr
|
|
819
898
|
|
|
820
899
|
|
|
821
|
-
ModelResponsePartDelta = Annotated[
|
|
900
|
+
ModelResponsePartDelta = Annotated[
|
|
901
|
+
Union[TextPartDelta, ThinkingPartDelta, ToolCallPartDelta], pydantic.Discriminator('part_delta_kind')
|
|
902
|
+
]
|
|
822
903
|
"""A partial update (delta) for any model response part."""
|
|
823
904
|
|
|
824
905
|
|
pydantic_ai/models/anthropic.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator
|
|
5
6
|
from contextlib import asynccontextmanager
|
|
6
7
|
from dataclasses import dataclass, field
|
|
@@ -23,6 +24,7 @@ from ..messages import (
|
|
|
23
24
|
RetryPromptPart,
|
|
24
25
|
SystemPromptPart,
|
|
25
26
|
TextPart,
|
|
27
|
+
ThinkingPart,
|
|
26
28
|
ToolCallPart,
|
|
27
29
|
ToolReturnPart,
|
|
28
30
|
UserPromptPart,
|
|
@@ -52,9 +54,15 @@ try:
|
|
|
52
54
|
BetaRawMessageStartEvent,
|
|
53
55
|
BetaRawMessageStopEvent,
|
|
54
56
|
BetaRawMessageStreamEvent,
|
|
57
|
+
BetaRedactedThinkingBlock,
|
|
58
|
+
BetaSignatureDelta,
|
|
55
59
|
BetaTextBlock,
|
|
56
60
|
BetaTextBlockParam,
|
|
57
61
|
BetaTextDelta,
|
|
62
|
+
BetaThinkingBlock,
|
|
63
|
+
BetaThinkingBlockParam,
|
|
64
|
+
BetaThinkingConfigParam,
|
|
65
|
+
BetaThinkingDelta,
|
|
58
66
|
BetaToolChoiceParam,
|
|
59
67
|
BetaToolParam,
|
|
60
68
|
BetaToolResultBlockParam,
|
|
@@ -90,7 +98,14 @@ class AnthropicModelSettings(ModelSettings, total=False):
|
|
|
90
98
|
anthropic_metadata: BetaMetadataParam
|
|
91
99
|
"""An object describing metadata about the request.
|
|
92
100
|
|
|
93
|
-
Contains `user_id`, an external identifier for the user who is associated with the request.
|
|
101
|
+
Contains `user_id`, an external identifier for the user who is associated with the request.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
anthropic_thinking: BetaThinkingConfigParam
|
|
105
|
+
"""Determine whether the model should generate a thinking block.
|
|
106
|
+
|
|
107
|
+
See [the Anthropic docs](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) for more information.
|
|
108
|
+
"""
|
|
94
109
|
|
|
95
110
|
|
|
96
111
|
@dataclass(init=False)
|
|
@@ -227,6 +242,7 @@ class AnthropicModel(Model):
|
|
|
227
242
|
tools=tools or NOT_GIVEN,
|
|
228
243
|
tool_choice=tool_choice or NOT_GIVEN,
|
|
229
244
|
stream=stream,
|
|
245
|
+
thinking=model_settings.get('anthropic_thinking', NOT_GIVEN),
|
|
230
246
|
stop_sequences=model_settings.get('stop_sequences', NOT_GIVEN),
|
|
231
247
|
temperature=model_settings.get('temperature', NOT_GIVEN),
|
|
232
248
|
top_p=model_settings.get('top_p', NOT_GIVEN),
|
|
@@ -246,6 +262,14 @@ class AnthropicModel(Model):
|
|
|
246
262
|
for item in response.content:
|
|
247
263
|
if isinstance(item, BetaTextBlock):
|
|
248
264
|
items.append(TextPart(content=item.text))
|
|
265
|
+
elif isinstance(item, BetaRedactedThinkingBlock): # pragma: no cover
|
|
266
|
+
warnings.warn(
|
|
267
|
+
'PydanticAI currently does not handle redacted thinking blocks. '
|
|
268
|
+
'If you have a suggestion on how we should handle them, please open an issue.',
|
|
269
|
+
UserWarning,
|
|
270
|
+
)
|
|
271
|
+
elif isinstance(item, BetaThinkingBlock):
|
|
272
|
+
items.append(ThinkingPart(content=item.thinking, signature=item.signature))
|
|
249
273
|
else:
|
|
250
274
|
assert isinstance(item, BetaToolUseBlock), f'unexpected item type {type(item)}'
|
|
251
275
|
items.append(
|
|
@@ -312,11 +336,21 @@ class AnthropicModel(Model):
|
|
|
312
336
|
if len(user_content_params) > 0:
|
|
313
337
|
anthropic_messages.append(BetaMessageParam(role='user', content=user_content_params))
|
|
314
338
|
elif isinstance(m, ModelResponse):
|
|
315
|
-
assistant_content_params: list[BetaTextBlockParam | BetaToolUseBlockParam] = []
|
|
339
|
+
assistant_content_params: list[BetaTextBlockParam | BetaToolUseBlockParam | BetaThinkingBlockParam] = []
|
|
316
340
|
for response_part in m.parts:
|
|
317
341
|
if isinstance(response_part, TextPart):
|
|
318
342
|
if response_part.content: # Only add non-empty text
|
|
319
343
|
assistant_content_params.append(BetaTextBlockParam(text=response_part.content, type='text'))
|
|
344
|
+
elif isinstance(response_part, ThinkingPart):
|
|
345
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
346
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
347
|
+
# assert response_part.signature is not None, 'Thinking part must have a signature'
|
|
348
|
+
# assistant_content_params.append(
|
|
349
|
+
# BetaThinkingBlockParam(
|
|
350
|
+
# thinking=response_part.content, signature=response_part.signature, type='thinking'
|
|
351
|
+
# )
|
|
352
|
+
# )
|
|
353
|
+
pass
|
|
320
354
|
else:
|
|
321
355
|
tool_use_block_param = BetaToolUseBlockParam(
|
|
322
356
|
id=_guard_tool_call_id(t=response_part),
|
|
@@ -445,10 +479,14 @@ class AnthropicStreamedResponse(StreamedResponse):
|
|
|
445
479
|
if isinstance(event, BetaRawContentBlockStartEvent):
|
|
446
480
|
current_block = event.content_block
|
|
447
481
|
if isinstance(current_block, BetaTextBlock) and current_block.text:
|
|
448
|
-
yield self._parts_manager.handle_text_delta(
|
|
449
|
-
|
|
482
|
+
yield self._parts_manager.handle_text_delta(vendor_part_id='content', content=current_block.text)
|
|
483
|
+
elif isinstance(current_block, BetaThinkingBlock):
|
|
484
|
+
yield self._parts_manager.handle_thinking_delta(
|
|
485
|
+
vendor_part_id='thinking',
|
|
486
|
+
content=current_block.thinking,
|
|
487
|
+
signature=current_block.signature,
|
|
450
488
|
)
|
|
451
|
-
elif isinstance(current_block, BetaToolUseBlock):
|
|
489
|
+
elif isinstance(current_block, BetaToolUseBlock):
|
|
452
490
|
maybe_event = self._parts_manager.handle_tool_call_delta(
|
|
453
491
|
vendor_part_id=current_block.id,
|
|
454
492
|
tool_name=current_block.name,
|
|
@@ -460,14 +498,20 @@ class AnthropicStreamedResponse(StreamedResponse):
|
|
|
460
498
|
|
|
461
499
|
elif isinstance(event, BetaRawContentBlockDeltaEvent):
|
|
462
500
|
if isinstance(event.delta, BetaTextDelta):
|
|
463
|
-
yield self._parts_manager.handle_text_delta(
|
|
464
|
-
|
|
501
|
+
yield self._parts_manager.handle_text_delta(vendor_part_id='content', content=event.delta.text)
|
|
502
|
+
elif isinstance(event.delta, BetaThinkingDelta):
|
|
503
|
+
yield self._parts_manager.handle_thinking_delta(
|
|
504
|
+
vendor_part_id='thinking', content=event.delta.thinking
|
|
505
|
+
)
|
|
506
|
+
elif isinstance(event.delta, BetaSignatureDelta):
|
|
507
|
+
yield self._parts_manager.handle_thinking_delta(
|
|
508
|
+
vendor_part_id='thinking', signature=event.delta.signature
|
|
465
509
|
)
|
|
466
|
-
elif (
|
|
510
|
+
elif (
|
|
467
511
|
current_block
|
|
468
512
|
and event.delta.type == 'input_json_delta'
|
|
469
513
|
and isinstance(current_block, BetaToolUseBlock)
|
|
470
|
-
):
|
|
514
|
+
): # pragma: no branch
|
|
471
515
|
maybe_event = self._parts_manager.handle_tool_call_delta(
|
|
472
516
|
vendor_part_id=current_block.id,
|
|
473
517
|
tool_name='',
|
pydantic_ai/models/bedrock.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
4
|
import typing
|
|
5
|
+
import warnings
|
|
5
6
|
from collections.abc import AsyncIterator, Iterable, Iterator, Mapping
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
8
|
from dataclasses import dataclass, field
|
|
@@ -27,6 +28,7 @@ from pydantic_ai.messages import (
|
|
|
27
28
|
RetryPromptPart,
|
|
28
29
|
SystemPromptPart,
|
|
29
30
|
TextPart,
|
|
31
|
+
ThinkingPart,
|
|
30
32
|
ToolCallPart,
|
|
31
33
|
ToolReturnPart,
|
|
32
34
|
UserPromptPart,
|
|
@@ -265,11 +267,16 @@ class BedrockConverseModel(Model):
|
|
|
265
267
|
items: list[ModelResponsePart] = []
|
|
266
268
|
if message := response['output'].get('message'): # pragma: no branch
|
|
267
269
|
for item in message['content']:
|
|
270
|
+
if reasoning_content := item.get('reasoningContent'):
|
|
271
|
+
reasoning_text = reasoning_content.get('reasoningText')
|
|
272
|
+
if reasoning_text: # pragma: no branch
|
|
273
|
+
thinking_part = ThinkingPart(content=reasoning_text['text'])
|
|
274
|
+
if reasoning_signature := reasoning_text.get('signature'):
|
|
275
|
+
thinking_part.signature = reasoning_signature
|
|
276
|
+
items.append(thinking_part)
|
|
268
277
|
if text := item.get('text'):
|
|
269
278
|
items.append(TextPart(content=text))
|
|
270
|
-
|
|
271
|
-
tool_use = item.get('toolUse')
|
|
272
|
-
assert tool_use is not None, f'Found a content that is not a text or tool use: {item}'
|
|
279
|
+
elif tool_use := item.get('toolUse'):
|
|
273
280
|
items.append(
|
|
274
281
|
ToolCallPart(
|
|
275
282
|
tool_name=tool_use['name'],
|
|
@@ -385,7 +392,7 @@ class BedrockConverseModel(Model):
|
|
|
385
392
|
|
|
386
393
|
return tool_config
|
|
387
394
|
|
|
388
|
-
async def _map_messages(
|
|
395
|
+
async def _map_messages( # noqa: C901
|
|
389
396
|
self, messages: list[ModelMessage]
|
|
390
397
|
) -> tuple[list[SystemContentBlockTypeDef], list[MessageUnionTypeDef]]:
|
|
391
398
|
"""Maps a `pydantic_ai.Message` to the Bedrock `MessageUnionTypeDef`.
|
|
@@ -448,6 +455,9 @@ class BedrockConverseModel(Model):
|
|
|
448
455
|
for item in message.parts:
|
|
449
456
|
if isinstance(item, TextPart):
|
|
450
457
|
content.append({'text': item.content})
|
|
458
|
+
elif isinstance(item, ThinkingPart):
|
|
459
|
+
# NOTE: We don't pass the thinking part to Bedrock since it raises an error.
|
|
460
|
+
pass
|
|
451
461
|
else:
|
|
452
462
|
assert isinstance(item, ToolCallPart)
|
|
453
463
|
content.append(self._map_tool_call(item))
|
|
@@ -592,6 +602,15 @@ class BedrockStreamedResponse(StreamedResponse):
|
|
|
592
602
|
if 'contentBlockDelta' in chunk:
|
|
593
603
|
index = chunk['contentBlockDelta']['contentBlockIndex']
|
|
594
604
|
delta = chunk['contentBlockDelta']['delta']
|
|
605
|
+
if 'reasoningContent' in delta:
|
|
606
|
+
if text := delta['reasoningContent'].get('text'):
|
|
607
|
+
yield self._parts_manager.handle_thinking_delta(vendor_part_id=index, content=text)
|
|
608
|
+
else: # pragma: no cover
|
|
609
|
+
warnings.warn(
|
|
610
|
+
f'Only text reasoning content is supported yet, but you got {delta["reasoningContent"]}. '
|
|
611
|
+
'Please report this to the maintainers.',
|
|
612
|
+
UserWarning,
|
|
613
|
+
)
|
|
595
614
|
if 'text' in delta:
|
|
596
615
|
yield self._parts_manager.handle_text_delta(vendor_part_id=index, content=delta['text'])
|
|
597
616
|
if 'toolUse' in delta:
|
pydantic_ai/models/cohere.py
CHANGED
|
@@ -6,6 +6,8 @@ from typing import Literal, Union, cast
|
|
|
6
6
|
|
|
7
7
|
from typing_extensions import assert_never
|
|
8
8
|
|
|
9
|
+
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
10
|
+
|
|
9
11
|
from .. import ModelHTTPError, usage
|
|
10
12
|
from .._utils import generate_tool_call_id as _generate_tool_call_id, guard_tool_call_id as _guard_tool_call_id
|
|
11
13
|
from ..messages import (
|
|
@@ -16,6 +18,7 @@ from ..messages import (
|
|
|
16
18
|
RetryPromptPart,
|
|
17
19
|
SystemPromptPart,
|
|
18
20
|
TextPart,
|
|
21
|
+
ThinkingPart,
|
|
19
22
|
ToolCallPart,
|
|
20
23
|
ToolReturnPart,
|
|
21
24
|
UserPromptPart,
|
|
@@ -187,7 +190,7 @@ class CohereModel(Model):
|
|
|
187
190
|
# While Cohere's API returns a list, it only does that for future proofing
|
|
188
191
|
# and currently only one item is being returned.
|
|
189
192
|
choice = response.message.content[0]
|
|
190
|
-
parts.
|
|
193
|
+
parts.extend(split_content_into_text_and_thinking(choice.text))
|
|
191
194
|
for c in response.message.tool_calls or []:
|
|
192
195
|
if c.function and c.function.name and c.function.arguments: # pragma: no branch
|
|
193
196
|
parts.append(
|
|
@@ -211,6 +214,11 @@ class CohereModel(Model):
|
|
|
211
214
|
for item in message.parts:
|
|
212
215
|
if isinstance(item, TextPart):
|
|
213
216
|
texts.append(item.content)
|
|
217
|
+
elif isinstance(item, ThinkingPart):
|
|
218
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
219
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
220
|
+
# texts.append(f'<think>\n{item.content}\n</think>')
|
|
221
|
+
pass
|
|
214
222
|
elif isinstance(item, ToolCallPart):
|
|
215
223
|
tool_calls.append(self._map_tool_call(item))
|
|
216
224
|
else:
|
pydantic_ai/models/function.py
CHANGED
|
@@ -24,6 +24,7 @@ from ..messages import (
|
|
|
24
24
|
RetryPromptPart,
|
|
25
25
|
SystemPromptPart,
|
|
26
26
|
TextPart,
|
|
27
|
+
ThinkingPart,
|
|
27
28
|
ToolCallPart,
|
|
28
29
|
ToolReturnPart,
|
|
29
30
|
UserContent,
|
|
@@ -268,6 +269,10 @@ def _estimate_usage(messages: Iterable[ModelMessage]) -> usage.Usage:
|
|
|
268
269
|
for part in message.parts:
|
|
269
270
|
if isinstance(part, TextPart):
|
|
270
271
|
response_tokens += _estimate_string_tokens(part.content)
|
|
272
|
+
elif isinstance(part, ThinkingPart):
|
|
273
|
+
# NOTE: We don't send ThinkingPart to the providers yet.
|
|
274
|
+
# If you are unsatisfied with this, please open an issue.
|
|
275
|
+
pass
|
|
271
276
|
elif isinstance(part, ToolCallPart):
|
|
272
277
|
call = part
|
|
273
278
|
response_tokens += 1 + _estimate_string_tokens(call.args_as_json_str())
|
pydantic_ai/models/gemini.py
CHANGED
|
@@ -27,6 +27,7 @@ from ..messages import (
|
|
|
27
27
|
RetryPromptPart,
|
|
28
28
|
SystemPromptPart,
|
|
29
29
|
TextPart,
|
|
30
|
+
ThinkingPart,
|
|
30
31
|
ToolCallPart,
|
|
31
32
|
ToolReturnPart,
|
|
32
33
|
UserPromptPart,
|
|
@@ -94,6 +95,15 @@ class GeminiModelSettings(ModelSettings, total=False):
|
|
|
94
95
|
See the [Gemini API docs](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/add-labels-to-api-calls) for use cases and limitations.
|
|
95
96
|
"""
|
|
96
97
|
|
|
98
|
+
gemini_thinking_config: ThinkingConfig
|
|
99
|
+
"""Thinking is on by default in both the API and AI Studio.
|
|
100
|
+
|
|
101
|
+
Being on by default doesn't mean the model will send back thoughts. For that, you need to set `include_thoughts`
|
|
102
|
+
to `True`. If you want to turn it off, set `thinking_budget` to `0`.
|
|
103
|
+
|
|
104
|
+
See more about it on <https://ai.google.dev/gemini-api/docs/thinking>.
|
|
105
|
+
"""
|
|
106
|
+
|
|
97
107
|
|
|
98
108
|
@dataclass(init=False)
|
|
99
109
|
class GeminiModel(Model):
|
|
@@ -379,7 +389,7 @@ def _settings_to_generation_config(model_settings: GeminiModelSettings) -> _Gemi
|
|
|
379
389
|
if (frequency_penalty := model_settings.get('frequency_penalty')) is not None:
|
|
380
390
|
config['frequency_penalty'] = frequency_penalty
|
|
381
391
|
if (thinkingConfig := model_settings.get('gemini_thinking_config')) is not None:
|
|
382
|
-
config['thinking_config'] = thinkingConfig # pragma: no cover
|
|
392
|
+
config['thinking_config'] = thinkingConfig # pragma: lax no cover
|
|
383
393
|
return config
|
|
384
394
|
|
|
385
395
|
|
|
@@ -576,6 +586,11 @@ def _content_model_response(m: ModelResponse) -> _GeminiContent:
|
|
|
576
586
|
for item in m.parts:
|
|
577
587
|
if isinstance(item, ToolCallPart):
|
|
578
588
|
parts.append(_function_call_part_from_call(item))
|
|
589
|
+
elif isinstance(item, ThinkingPart):
|
|
590
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
591
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
592
|
+
# parts.append(_GeminiTextPart(text=item.content, thought=True))
|
|
593
|
+
pass
|
|
579
594
|
elif isinstance(item, TextPart):
|
|
580
595
|
if item.content:
|
|
581
596
|
parts.append(_GeminiTextPart(text=item.content))
|
|
@@ -584,29 +599,34 @@ def _content_model_response(m: ModelResponse) -> _GeminiContent:
|
|
|
584
599
|
return _GeminiContent(role='model', parts=parts)
|
|
585
600
|
|
|
586
601
|
|
|
587
|
-
class
|
|
602
|
+
class _BasePart(TypedDict):
|
|
603
|
+
thought: NotRequired[bool]
|
|
604
|
+
"""Indicates if the part is thought from the model."""
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class _GeminiTextPart(_BasePart):
|
|
588
608
|
text: str
|
|
589
609
|
|
|
590
610
|
|
|
591
|
-
class _GeminiInlineData(
|
|
611
|
+
class _GeminiInlineData(_BasePart):
|
|
592
612
|
data: str
|
|
593
613
|
mime_type: Annotated[str, pydantic.Field(alias='mimeType')]
|
|
594
614
|
|
|
595
615
|
|
|
596
|
-
class _GeminiInlineDataPart(
|
|
616
|
+
class _GeminiInlineDataPart(_BasePart):
|
|
597
617
|
"""See <https://ai.google.dev/api/caching#Blob>."""
|
|
598
618
|
|
|
599
619
|
inline_data: Annotated[_GeminiInlineData, pydantic.Field(alias='inlineData')]
|
|
600
620
|
|
|
601
621
|
|
|
602
|
-
class _GeminiFileData(
|
|
622
|
+
class _GeminiFileData(_BasePart):
|
|
603
623
|
"""See <https://ai.google.dev/api/caching#FileData>."""
|
|
604
624
|
|
|
605
625
|
file_uri: Annotated[str, pydantic.Field(alias='fileUri')]
|
|
606
626
|
mime_type: Annotated[str, pydantic.Field(alias='mimeType')]
|
|
607
627
|
|
|
608
628
|
|
|
609
|
-
class _GeminiFileDataPart(
|
|
629
|
+
class _GeminiFileDataPart(_BasePart):
|
|
610
630
|
file_data: Annotated[_GeminiFileData, pydantic.Field(alias='fileData')]
|
|
611
631
|
|
|
612
632
|
|
|
@@ -615,7 +635,7 @@ class _GeminiThoughtPart(TypedDict):
|
|
|
615
635
|
thought_signature: Annotated[str, pydantic.Field(alias='thoughtSignature')]
|
|
616
636
|
|
|
617
637
|
|
|
618
|
-
class _GeminiFunctionCallPart(
|
|
638
|
+
class _GeminiFunctionCallPart(_BasePart):
|
|
619
639
|
function_call: Annotated[_GeminiFunctionCall, pydantic.Field(alias='functionCall')]
|
|
620
640
|
|
|
621
641
|
|
|
@@ -633,7 +653,12 @@ def _process_response_from_parts(
|
|
|
633
653
|
items: list[ModelResponsePart] = []
|
|
634
654
|
for part in parts:
|
|
635
655
|
if 'text' in part:
|
|
636
|
-
|
|
656
|
+
# NOTE: Google doesn't include the `thought` field anymore. We handle this here in case they decide to
|
|
657
|
+
# change their mind and start including it again.
|
|
658
|
+
if part.get('thought'): # pragma: no cover
|
|
659
|
+
items.append(ThinkingPart(content=part['text']))
|
|
660
|
+
else:
|
|
661
|
+
items.append(TextPart(content=part['text']))
|
|
637
662
|
elif 'function_call' in part:
|
|
638
663
|
items.append(ToolCallPart(tool_name=part['function_call']['name'], args=part['function_call']['args']))
|
|
639
664
|
elif 'function_response' in part: # pragma: no cover
|
pydantic_ai/models/google.py
CHANGED
|
@@ -23,6 +23,7 @@ from ..messages import (
|
|
|
23
23
|
RetryPromptPart,
|
|
24
24
|
SystemPromptPart,
|
|
25
25
|
TextPart,
|
|
26
|
+
ThinkingPart,
|
|
26
27
|
ToolCallPart,
|
|
27
28
|
ToolReturnPart,
|
|
28
29
|
UserPromptPart,
|
|
@@ -413,7 +414,10 @@ class GeminiStreamedResponse(StreamedResponse):
|
|
|
413
414
|
assert candidate.content.parts is not None
|
|
414
415
|
for part in candidate.content.parts:
|
|
415
416
|
if part.text is not None:
|
|
416
|
-
|
|
417
|
+
if part.thought:
|
|
418
|
+
yield self._parts_manager.handle_thinking_delta(vendor_part_id='thinking', content=part.text)
|
|
419
|
+
else:
|
|
420
|
+
yield self._parts_manager.handle_text_delta(vendor_part_id='content', content=part.text)
|
|
417
421
|
elif part.function_call:
|
|
418
422
|
maybe_event = self._parts_manager.handle_tool_call_delta(
|
|
419
423
|
vendor_part_id=uuid4(),
|
|
@@ -446,6 +450,11 @@ def _content_model_response(m: ModelResponse) -> ContentDict:
|
|
|
446
450
|
elif isinstance(item, TextPart):
|
|
447
451
|
if item.content: # pragma: no branch
|
|
448
452
|
parts.append({'text': item.content})
|
|
453
|
+
elif isinstance(item, ThinkingPart): # pragma: no cover
|
|
454
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
455
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
456
|
+
# parts.append({'text': item.content, 'thought': True})
|
|
457
|
+
pass
|
|
449
458
|
else:
|
|
450
459
|
assert_never(item)
|
|
451
460
|
return ContentDict(role='model', parts=parts)
|
|
@@ -461,7 +470,10 @@ def _process_response_from_parts(
|
|
|
461
470
|
items: list[ModelResponsePart] = []
|
|
462
471
|
for part in parts:
|
|
463
472
|
if part.text is not None:
|
|
464
|
-
|
|
473
|
+
if part.thought:
|
|
474
|
+
items.append(ThinkingPart(content=part.text))
|
|
475
|
+
else:
|
|
476
|
+
items.append(TextPart(content=part.text))
|
|
465
477
|
elif part.function_call:
|
|
466
478
|
assert part.function_call.name is not None
|
|
467
479
|
tool_call_part = ToolCallPart(tool_name=part.function_call.name, args=part.function_call.args)
|
pydantic_ai/models/groq.py
CHANGED
|
@@ -9,6 +9,8 @@ from typing import Literal, Union, cast, overload
|
|
|
9
9
|
|
|
10
10
|
from typing_extensions import assert_never
|
|
11
11
|
|
|
12
|
+
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
13
|
+
|
|
12
14
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
|
|
13
15
|
from .._utils import guard_tool_call_id as _guard_tool_call_id, number_to_datetime
|
|
14
16
|
from ..messages import (
|
|
@@ -23,6 +25,7 @@ from ..messages import (
|
|
|
23
25
|
RetryPromptPart,
|
|
24
26
|
SystemPromptPart,
|
|
25
27
|
TextPart,
|
|
28
|
+
ThinkingPart,
|
|
26
29
|
ToolCallPart,
|
|
27
30
|
ToolReturnPart,
|
|
28
31
|
UserPromptPart,
|
|
@@ -95,7 +98,7 @@ class GroqModelSettings(ModelSettings, total=False):
|
|
|
95
98
|
ALL FIELDS MUST BE `groq_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
|
|
96
99
|
"""
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
groq_reasoning_format: Literal['hidden', 'raw', 'parsed']
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
@dataclass(init=False)
|
|
@@ -234,6 +237,7 @@ class GroqModel(Model):
|
|
|
234
237
|
timeout=model_settings.get('timeout', NOT_GIVEN),
|
|
235
238
|
seed=model_settings.get('seed', NOT_GIVEN),
|
|
236
239
|
presence_penalty=model_settings.get('presence_penalty', NOT_GIVEN),
|
|
240
|
+
reasoning_format=model_settings.get('groq_reasoning_format', NOT_GIVEN),
|
|
237
241
|
frequency_penalty=model_settings.get('frequency_penalty', NOT_GIVEN),
|
|
238
242
|
logit_bias=model_settings.get('logit_bias', NOT_GIVEN),
|
|
239
243
|
extra_headers=extra_headers,
|
|
@@ -249,8 +253,12 @@ class GroqModel(Model):
|
|
|
249
253
|
timestamp = number_to_datetime(response.created)
|
|
250
254
|
choice = response.choices[0]
|
|
251
255
|
items: list[ModelResponsePart] = []
|
|
256
|
+
# NOTE: The `reasoning` field is only present if `groq_reasoning_format` is set to `parsed`.
|
|
257
|
+
if choice.message.reasoning is not None:
|
|
258
|
+
items.append(ThinkingPart(content=choice.message.reasoning))
|
|
252
259
|
if choice.message.content is not None:
|
|
253
|
-
|
|
260
|
+
# NOTE: The `<think>` tag is only present if `groq_reasoning_format` is set to `raw`.
|
|
261
|
+
items.extend(split_content_into_text_and_thinking(choice.message.content))
|
|
254
262
|
if choice.message.tool_calls is not None:
|
|
255
263
|
for c in choice.message.tool_calls:
|
|
256
264
|
items.append(ToolCallPart(tool_name=c.function.name, args=c.function.arguments, tool_call_id=c.id))
|
|
@@ -293,6 +301,9 @@ class GroqModel(Model):
|
|
|
293
301
|
texts.append(item.content)
|
|
294
302
|
elif isinstance(item, ToolCallPart):
|
|
295
303
|
tool_calls.append(self._map_tool_call(item))
|
|
304
|
+
elif isinstance(item, ThinkingPart):
|
|
305
|
+
# Skip thinking parts when mapping to Groq messages
|
|
306
|
+
continue
|
|
296
307
|
else:
|
|
297
308
|
assert_never(item)
|
|
298
309
|
message_param = chat.ChatCompletionAssistantMessageParam(role='assistant')
|
|
@@ -134,7 +134,7 @@ class InstrumentationSettings:
|
|
|
134
134
|
**tokens_histogram_kwargs,
|
|
135
135
|
explicit_bucket_boundaries_advisory=TOKEN_HISTOGRAM_BOUNDARIES,
|
|
136
136
|
)
|
|
137
|
-
except TypeError:
|
|
137
|
+
except TypeError: # pragma: lax no cover
|
|
138
138
|
# Older OTel/logfire versions don't support explicit_bucket_boundaries_advisory
|
|
139
139
|
self.tokens_histogram = self.meter.create_histogram(
|
|
140
140
|
**tokens_histogram_kwargs, # pyright: ignore
|
pydantic_ai/models/mistral.py
CHANGED
|
@@ -11,6 +11,8 @@ import pydantic_core
|
|
|
11
11
|
from httpx import Timeout
|
|
12
12
|
from typing_extensions import assert_never
|
|
13
13
|
|
|
14
|
+
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
15
|
+
|
|
14
16
|
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils
|
|
15
17
|
from .._utils import generate_tool_call_id as _generate_tool_call_id, now_utc as _now_utc, number_to_datetime
|
|
16
18
|
from ..messages import (
|
|
@@ -25,6 +27,7 @@ from ..messages import (
|
|
|
25
27
|
RetryPromptPart,
|
|
26
28
|
SystemPromptPart,
|
|
27
29
|
TextPart,
|
|
30
|
+
ThinkingPart,
|
|
28
31
|
ToolCallPart,
|
|
29
32
|
ToolReturnPart,
|
|
30
33
|
UserPromptPart,
|
|
@@ -322,7 +325,7 @@ class MistralModel(Model):
|
|
|
322
325
|
|
|
323
326
|
parts: list[ModelResponsePart] = []
|
|
324
327
|
if text := _map_content(content):
|
|
325
|
-
parts.
|
|
328
|
+
parts.extend(split_content_into_text_and_thinking(text))
|
|
326
329
|
|
|
327
330
|
if isinstance(tool_calls, list):
|
|
328
331
|
for tool_call in tool_calls:
|
|
@@ -484,6 +487,11 @@ class MistralModel(Model):
|
|
|
484
487
|
for part in message.parts:
|
|
485
488
|
if isinstance(part, TextPart):
|
|
486
489
|
content_chunks.append(MistralTextChunk(text=part.content))
|
|
490
|
+
elif isinstance(part, ThinkingPart):
|
|
491
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
492
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
493
|
+
# content_chunks.append(MistralTextChunk(text=f'<think>{part.content}</think>'))
|
|
494
|
+
pass
|
|
487
495
|
elif isinstance(part, ToolCallPart):
|
|
488
496
|
tool_calls.append(self._map_tool_call(part))
|
|
489
497
|
else:
|
pydantic_ai/models/openai.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import Any, Literal, Union, cast, overload
|
|
|
10
10
|
|
|
11
11
|
from typing_extensions import assert_never
|
|
12
12
|
|
|
13
|
+
from pydantic_ai._thinking_part import split_content_into_text_and_thinking
|
|
13
14
|
from pydantic_ai.profiles.openai import OpenAIModelProfile
|
|
14
15
|
from pydantic_ai.providers import Provider, infer_provider
|
|
15
16
|
|
|
@@ -28,6 +29,7 @@ from ..messages import (
|
|
|
28
29
|
RetryPromptPart,
|
|
29
30
|
SystemPromptPart,
|
|
30
31
|
TextPart,
|
|
32
|
+
ThinkingPart,
|
|
31
33
|
ToolCallPart,
|
|
32
34
|
ToolReturnPart,
|
|
33
35
|
UserPromptPart,
|
|
@@ -137,6 +139,9 @@ class OpenAIResponsesModelSettings(OpenAIModelSettings, total=False):
|
|
|
137
139
|
"""
|
|
138
140
|
|
|
139
141
|
openai_reasoning_generate_summary: Literal['detailed', 'concise']
|
|
142
|
+
"""Deprecated alias for `openai_reasoning_summary`."""
|
|
143
|
+
|
|
144
|
+
openai_reasoning_summary: Literal['detailed', 'concise']
|
|
140
145
|
"""A summary of the reasoning performed by the model.
|
|
141
146
|
|
|
142
147
|
This can be useful for debugging and understanding the model's reasoning process.
|
|
@@ -325,6 +330,10 @@ class OpenAIModel(Model):
|
|
|
325
330
|
timestamp = number_to_datetime(response.created)
|
|
326
331
|
choice = response.choices[0]
|
|
327
332
|
items: list[ModelResponsePart] = []
|
|
333
|
+
# The `reasoning_content` is only present in DeepSeek models.
|
|
334
|
+
if reasoning_content := getattr(choice.message, 'reasoning_content', None):
|
|
335
|
+
items.append(ThinkingPart(content=reasoning_content))
|
|
336
|
+
|
|
328
337
|
vendor_details: dict[str, Any] | None = None
|
|
329
338
|
|
|
330
339
|
# Add logprobs to vendor_details if available
|
|
@@ -345,7 +354,7 @@ class OpenAIModel(Model):
|
|
|
345
354
|
}
|
|
346
355
|
|
|
347
356
|
if choice.message.content is not None:
|
|
348
|
-
items.
|
|
357
|
+
items.extend(split_content_into_text_and_thinking(choice.message.content))
|
|
349
358
|
if choice.message.tool_calls is not None:
|
|
350
359
|
for c in choice.message.tool_calls:
|
|
351
360
|
part = ToolCallPart(c.function.name, c.function.arguments, tool_call_id=c.id)
|
|
@@ -394,6 +403,11 @@ class OpenAIModel(Model):
|
|
|
394
403
|
for item in message.parts:
|
|
395
404
|
if isinstance(item, TextPart):
|
|
396
405
|
texts.append(item.content)
|
|
406
|
+
elif isinstance(item, ThinkingPart):
|
|
407
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
408
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
409
|
+
# texts.append(f'<think>\n{item.content}\n</think>')
|
|
410
|
+
pass
|
|
397
411
|
elif isinstance(item, ToolCallPart):
|
|
398
412
|
tool_calls.append(self._map_tool_call(item))
|
|
399
413
|
else:
|
|
@@ -611,7 +625,12 @@ class OpenAIResponsesModel(Model):
|
|
|
611
625
|
items: list[ModelResponsePart] = []
|
|
612
626
|
items.append(TextPart(response.output_text))
|
|
613
627
|
for item in response.output:
|
|
614
|
-
if item.type == '
|
|
628
|
+
if item.type == 'reasoning':
|
|
629
|
+
for summary in item.summary:
|
|
630
|
+
# NOTE: We use the same id for all summaries because we can merge them on the round trip.
|
|
631
|
+
# The providers don't force the signature to be unique.
|
|
632
|
+
items.append(ThinkingPart(content=summary.text, id=item.id))
|
|
633
|
+
elif item.type == 'function_call':
|
|
615
634
|
items.append(ToolCallPart(item.name, item.arguments, tool_call_id=item.call_id))
|
|
616
635
|
return ModelResponse(
|
|
617
636
|
items,
|
|
@@ -710,11 +729,22 @@ class OpenAIResponsesModel(Model):
|
|
|
710
729
|
|
|
711
730
|
def _get_reasoning(self, model_settings: OpenAIResponsesModelSettings) -> Reasoning | NotGiven:
|
|
712
731
|
reasoning_effort = model_settings.get('openai_reasoning_effort', None)
|
|
732
|
+
reasoning_summary = model_settings.get('openai_reasoning_summary', None)
|
|
713
733
|
reasoning_generate_summary = model_settings.get('openai_reasoning_generate_summary', None)
|
|
714
734
|
|
|
715
|
-
if
|
|
735
|
+
if reasoning_summary and reasoning_generate_summary: # pragma: no cover
|
|
736
|
+
raise ValueError('`openai_reasoning_summary` and `openai_reasoning_generate_summary` cannot both be set.')
|
|
737
|
+
|
|
738
|
+
if reasoning_generate_summary is not None: # pragma: no cover
|
|
739
|
+
warnings.warn(
|
|
740
|
+
'`openai_reasoning_generate_summary` is deprecated, use `openai_reasoning_summary` instead',
|
|
741
|
+
DeprecationWarning,
|
|
742
|
+
)
|
|
743
|
+
reasoning_summary = reasoning_generate_summary
|
|
744
|
+
|
|
745
|
+
if reasoning_effort is None and reasoning_summary is None:
|
|
716
746
|
return NOT_GIVEN
|
|
717
|
-
return Reasoning(effort=reasoning_effort,
|
|
747
|
+
return Reasoning(effort=reasoning_effort, summary=reasoning_summary)
|
|
718
748
|
|
|
719
749
|
def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.FunctionToolParam]:
|
|
720
750
|
tools = [self._map_tool_definition(r) for r in model_request_parameters.function_tools]
|
|
@@ -770,11 +800,30 @@ class OpenAIResponsesModel(Model):
|
|
|
770
800
|
else:
|
|
771
801
|
assert_never(part)
|
|
772
802
|
elif isinstance(message, ModelResponse):
|
|
803
|
+
# last_thinking_part_idx: int | None = None
|
|
773
804
|
for item in message.parts:
|
|
774
805
|
if isinstance(item, TextPart):
|
|
775
806
|
openai_messages.append(responses.EasyInputMessageParam(role='assistant', content=item.content))
|
|
776
807
|
elif isinstance(item, ToolCallPart):
|
|
777
808
|
openai_messages.append(self._map_tool_call(item))
|
|
809
|
+
elif isinstance(item, ThinkingPart):
|
|
810
|
+
# NOTE: We don't send ThinkingPart to the providers yet. If you are unsatisfied with this,
|
|
811
|
+
# please open an issue. The below code is the code to send thinking to the provider.
|
|
812
|
+
# if last_thinking_part_idx is not None:
|
|
813
|
+
# reasoning_item = cast(responses.ResponseReasoningItemParam, openai_messages[last_thinking_part_idx]) # fmt: skip
|
|
814
|
+
# if item.id == reasoning_item['id']:
|
|
815
|
+
# assert isinstance(reasoning_item['summary'], list)
|
|
816
|
+
# reasoning_item['summary'].append(Summary(text=item.content, type='summary_text'))
|
|
817
|
+
# continue
|
|
818
|
+
# last_thinking_part_idx = len(openai_messages)
|
|
819
|
+
# openai_messages.append(
|
|
820
|
+
# responses.ResponseReasoningItemParam(
|
|
821
|
+
# id=item.id or generate_tool_call_id(),
|
|
822
|
+
# summary=[Summary(text=item.content, type='summary_text')],
|
|
823
|
+
# type='reasoning',
|
|
824
|
+
# )
|
|
825
|
+
# )
|
|
826
|
+
pass
|
|
778
827
|
else:
|
|
779
828
|
assert_never(item)
|
|
780
829
|
else:
|
|
@@ -948,13 +997,43 @@ class OpenAIResponsesStreamedResponse(StreamedResponse):
|
|
|
948
997
|
vendor_part_id=chunk.item.id,
|
|
949
998
|
tool_name=chunk.item.name,
|
|
950
999
|
args=chunk.item.arguments,
|
|
951
|
-
tool_call_id=chunk.item.
|
|
1000
|
+
tool_call_id=chunk.item.call_id,
|
|
1001
|
+
)
|
|
1002
|
+
elif isinstance(chunk.item, responses.ResponseReasoningItem):
|
|
1003
|
+
content = chunk.item.summary[0].text if chunk.item.summary else ''
|
|
1004
|
+
yield self._parts_manager.handle_thinking_delta(
|
|
1005
|
+
vendor_part_id=chunk.item.id,
|
|
1006
|
+
content=content,
|
|
1007
|
+
signature=chunk.item.id,
|
|
1008
|
+
)
|
|
1009
|
+
elif isinstance(chunk.item, responses.ResponseOutputMessage):
|
|
1010
|
+
pass
|
|
1011
|
+
else:
|
|
1012
|
+
warnings.warn( # pragma: no cover
|
|
1013
|
+
f'Handling of this item type is not yet implemented. Please report on our GitHub: {chunk}',
|
|
1014
|
+
UserWarning,
|
|
952
1015
|
)
|
|
953
1016
|
|
|
954
1017
|
elif isinstance(chunk, responses.ResponseOutputItemDoneEvent):
|
|
955
1018
|
# NOTE: We only need this if the tool call deltas don't include the final info.
|
|
956
1019
|
pass
|
|
957
1020
|
|
|
1021
|
+
elif isinstance(chunk, responses.ResponseReasoningSummaryPartAddedEvent):
|
|
1022
|
+
pass # there's nothing we need to do here
|
|
1023
|
+
|
|
1024
|
+
elif isinstance(chunk, responses.ResponseReasoningSummaryPartDoneEvent):
|
|
1025
|
+
pass # there's nothing we need to do here
|
|
1026
|
+
|
|
1027
|
+
elif isinstance(chunk, responses.ResponseReasoningSummaryTextDoneEvent):
|
|
1028
|
+
pass # there's nothing we need to do here
|
|
1029
|
+
|
|
1030
|
+
elif isinstance(chunk, responses.ResponseReasoningSummaryTextDeltaEvent):
|
|
1031
|
+
yield self._parts_manager.handle_thinking_delta(
|
|
1032
|
+
vendor_part_id=chunk.item_id,
|
|
1033
|
+
content=chunk.delta,
|
|
1034
|
+
signature=chunk.item_id,
|
|
1035
|
+
)
|
|
1036
|
+
|
|
958
1037
|
elif isinstance(chunk, responses.ResponseTextDeltaEvent):
|
|
959
1038
|
yield self._parts_manager.handle_text_delta(vendor_part_id=chunk.content_index, content=chunk.delta)
|
|
960
1039
|
|
pydantic_ai/models/test.py
CHANGED
|
@@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta
|
|
|
9
9
|
from typing import Any, Literal
|
|
10
10
|
|
|
11
11
|
import pydantic_core
|
|
12
|
+
from typing_extensions import assert_never
|
|
12
13
|
|
|
13
14
|
from .. import _utils
|
|
14
15
|
from ..messages import (
|
|
@@ -19,17 +20,14 @@ from ..messages import (
|
|
|
19
20
|
ModelResponseStreamEvent,
|
|
20
21
|
RetryPromptPart,
|
|
21
22
|
TextPart,
|
|
23
|
+
ThinkingPart,
|
|
22
24
|
ToolCallPart,
|
|
23
25
|
ToolReturnPart,
|
|
24
26
|
)
|
|
25
27
|
from ..settings import ModelSettings
|
|
26
28
|
from ..tools import ToolDefinition
|
|
27
29
|
from ..usage import Usage
|
|
28
|
-
from . import
|
|
29
|
-
Model,
|
|
30
|
-
ModelRequestParameters,
|
|
31
|
-
StreamedResponse,
|
|
32
|
-
)
|
|
30
|
+
from . import Model, ModelRequestParameters, StreamedResponse
|
|
33
31
|
from .function import _estimate_string_tokens, _estimate_usage # pyright: ignore[reportPrivateUsage]
|
|
34
32
|
|
|
35
33
|
|
|
@@ -254,10 +252,15 @@ class TestStreamedResponse(StreamedResponse):
|
|
|
254
252
|
for word in words:
|
|
255
253
|
self._usage += _get_string_usage(word)
|
|
256
254
|
yield self._parts_manager.handle_text_delta(vendor_part_id=i, content=word)
|
|
257
|
-
|
|
255
|
+
elif isinstance(part, ToolCallPart):
|
|
258
256
|
yield self._parts_manager.handle_tool_call_part(
|
|
259
257
|
vendor_part_id=i, tool_name=part.tool_name, args=part.args, tool_call_id=part.tool_call_id
|
|
260
258
|
)
|
|
259
|
+
elif isinstance(part, ThinkingPart): # pragma: no cover
|
|
260
|
+
# NOTE: There's no way to reach this part of the code, since we don't generate ThinkingPart on TestModel.
|
|
261
|
+
assert False, "This should be unreachable — we don't generate ThinkingPart on TestModel."
|
|
262
|
+
else:
|
|
263
|
+
assert_never(part)
|
|
261
264
|
|
|
262
265
|
@property
|
|
263
266
|
def model_name(self) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
|
|
5
5
|
Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,15 +30,15 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
|
30
30
|
Requires-Dist: griffe>=1.3.2
|
|
31
31
|
Requires-Dist: httpx>=0.27
|
|
32
32
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
33
|
-
Requires-Dist: pydantic-graph==0.
|
|
33
|
+
Requires-Dist: pydantic-graph==0.3.0
|
|
34
34
|
Requires-Dist: pydantic>=2.10
|
|
35
35
|
Requires-Dist: typing-inspection>=0.4.0
|
|
36
36
|
Provides-Extra: a2a
|
|
37
|
-
Requires-Dist: fasta2a==0.
|
|
37
|
+
Requires-Dist: fasta2a==0.3.0; extra == 'a2a'
|
|
38
38
|
Provides-Extra: anthropic
|
|
39
39
|
Requires-Dist: anthropic>=0.52.0; extra == 'anthropic'
|
|
40
40
|
Provides-Extra: bedrock
|
|
41
|
-
Requires-Dist: boto3>=1.
|
|
41
|
+
Requires-Dist: boto3>=1.37.24; extra == 'bedrock'
|
|
42
42
|
Provides-Extra: cli
|
|
43
43
|
Requires-Dist: argcomplete>=3.5.0; extra == 'cli'
|
|
44
44
|
Requires-Dist: prompt-toolkit>=3; extra == 'cli'
|
|
@@ -48,11 +48,11 @@ Requires-Dist: cohere>=5.13.11; (platform_system != 'Emscripten') and extra == '
|
|
|
48
48
|
Provides-Extra: duckduckgo
|
|
49
49
|
Requires-Dist: duckduckgo-search>=7.0.0; extra == 'duckduckgo'
|
|
50
50
|
Provides-Extra: evals
|
|
51
|
-
Requires-Dist: pydantic-evals==0.
|
|
51
|
+
Requires-Dist: pydantic-evals==0.3.0; extra == 'evals'
|
|
52
52
|
Provides-Extra: google
|
|
53
53
|
Requires-Dist: google-genai>=1.15.0; extra == 'google'
|
|
54
54
|
Provides-Extra: groq
|
|
55
|
-
Requires-Dist: groq>=0.
|
|
55
|
+
Requires-Dist: groq>=0.19.0; extra == 'groq'
|
|
56
56
|
Provides-Extra: logfire
|
|
57
57
|
Requires-Dist: logfire>=3.11.0; extra == 'logfire'
|
|
58
58
|
Provides-Extra: mcp
|
|
@@ -60,7 +60,7 @@ Requires-Dist: mcp>=1.9.4; (python_version >= '3.10') and extra == 'mcp'
|
|
|
60
60
|
Provides-Extra: mistral
|
|
61
61
|
Requires-Dist: mistralai>=1.2.5; extra == 'mistral'
|
|
62
62
|
Provides-Extra: openai
|
|
63
|
-
Requires-Dist: openai>=1.
|
|
63
|
+
Requires-Dist: openai>=1.76.0; extra == 'openai'
|
|
64
64
|
Provides-Extra: tavily
|
|
65
65
|
Requires-Dist: tavily-python>=0.5.0; extra == 'tavily'
|
|
66
66
|
Provides-Extra: vertexai
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
pydantic_ai/__init__.py,sha256=5flxyMQJVrHRMQ3MYaZf1el2ctNs0JmPClKbw2Q-Lsk,1160
|
|
2
2
|
pydantic_ai/__main__.py,sha256=Q_zJU15DUA01YtlJ2mnaLCoId2YmgmreVEERGuQT-Y0,132
|
|
3
3
|
pydantic_ai/_a2a.py,sha256=8nNtx6GENDt2Ej3f1ui9L-FuNQBYVELpJFfwz-y7fUw,7234
|
|
4
|
-
pydantic_ai/_agent_graph.py,sha256=
|
|
4
|
+
pydantic_ai/_agent_graph.py,sha256=8Y1xEFwNCp0hVsRst_2I4WQymeTFRJ4Ec5-lURMd5HQ,39671
|
|
5
5
|
pydantic_ai/_cli.py,sha256=kc9UxGjYsKK0IR4No-V5BGiAtq2fY6eZZ9rBkAdHWOM,12948
|
|
6
6
|
pydantic_ai/_function_schema.py,sha256=VXHGnudrpyW40UJqCopgSUB_IuSip5pEEBSLGhVEuFI,10846
|
|
7
7
|
pydantic_ai/_griffe.py,sha256=Sf_DisE9k2TA0VFeVIK2nf1oOct5MygW86PBCACJkFA,5244
|
|
8
8
|
pydantic_ai/_output.py,sha256=HYhcaqcisU16PT_EFdl2VuV5MI-nRFbUPzijd_rTTgM,16787
|
|
9
|
-
pydantic_ai/_parts_manager.py,sha256=
|
|
9
|
+
pydantic_ai/_parts_manager.py,sha256=Lioi8b7Nfyax09yQu8jTkMzxd26dYDrdAqhYvjRSKqQ,16182
|
|
10
10
|
pydantic_ai/_system_prompt.py,sha256=W5wYN6rH5JCshl1xI2s0ygevBCutCraqyG6t75yZubk,1117
|
|
11
|
+
pydantic_ai/_thinking_part.py,sha256=mzx2RZSfiQxAKpljEflrcXRXmFKxtp6bKVyorY3UYZk,1554
|
|
11
12
|
pydantic_ai/_utils.py,sha256=qi2NjYpIVOgCHDMPgyV8oUL42Fv2_rLyj8KdOUO5fQU,11319
|
|
12
13
|
pydantic_ai/agent.py,sha256=oNW5ffihOF1Kn13N3GZ9wudyooGxN0O3r1wGJAwYUMY,94448
|
|
13
14
|
pydantic_ai/direct.py,sha256=tXRcQ3fMkykaawO51VxnSwQnqcEmu1LhCy7U9gOyM-g,7768
|
|
@@ -15,7 +16,7 @@ pydantic_ai/exceptions.py,sha256=IdFw594Ou7Vn4YFa7xdZ040_j_6nmyA3MPANbC7sys4,317
|
|
|
15
16
|
pydantic_ai/format_as_xml.py,sha256=IINfh1evWDphGahqHNLBArB5dQ4NIqS3S-kru35ztGg,372
|
|
16
17
|
pydantic_ai/format_prompt.py,sha256=qdKep95Sjlr7u1-qag4JwPbjoURbG0GbeU_l5ODTNw4,4466
|
|
17
18
|
pydantic_ai/mcp.py,sha256=OkbwSBODgeC4BX2QIvTmECZJbeSYtjZ15ZPnEyf95UI,20157
|
|
18
|
-
pydantic_ai/messages.py,sha256=
|
|
19
|
+
pydantic_ai/messages.py,sha256=Z8cNpaEcMgdJpyE9ydBLBDJV0A-Hf-GllLAWeUKY4_0,36124
|
|
19
20
|
pydantic_ai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
21
|
pydantic_ai/result.py,sha256=YlcR0QAQIejz3fbZ50zYfHKIZco0dwmnZTxytV-n3oM,24609
|
|
21
22
|
pydantic_ai/settings.py,sha256=eRJs2fI2yaIrhtYRlWqKlC9KnFaJHvslgSll8NQ20jc,3533
|
|
@@ -27,18 +28,18 @@ pydantic_ai/common_tools/tavily.py,sha256=Q1xxSF5HtXAaZ10Pp-OaDOHXwJf2mco9wScGEQ
|
|
|
27
28
|
pydantic_ai/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
29
|
pydantic_ai/ext/langchain.py,sha256=TI8B6eBjEGKFfvwyLgC_-0eeba4hDJq7wLZ0OZhbiWw,1967
|
|
29
30
|
pydantic_ai/models/__init__.py,sha256=LhBw4yxIEMByJPthAiWtQwGgNlj3cQkOaX6wtzeMFjA,27947
|
|
30
|
-
pydantic_ai/models/anthropic.py,sha256=
|
|
31
|
-
pydantic_ai/models/bedrock.py,sha256=
|
|
32
|
-
pydantic_ai/models/cohere.py,sha256=
|
|
31
|
+
pydantic_ai/models/anthropic.py,sha256=s7yvNObBfS-gcXLT0vU8UXjLHITsbr5kkXgP1SYkPms,23832
|
|
32
|
+
pydantic_ai/models/bedrock.py,sha256=67qf_mFnx0kfmKoI96zLOAUn3P47PxPqMrQsaYUrJJ0,29120
|
|
33
|
+
pydantic_ai/models/cohere.py,sha256=UU04-_O-KLgC4DUpM-g4FBPoTOatbmVJJ7mkZNBGsbQ,12626
|
|
33
34
|
pydantic_ai/models/fallback.py,sha256=idOYGMo3CZzpCBT8DDiuPAAgnV2jzluDUq3ESb3KteM,4981
|
|
34
|
-
pydantic_ai/models/function.py,sha256=
|
|
35
|
-
pydantic_ai/models/gemini.py,sha256=
|
|
36
|
-
pydantic_ai/models/google.py,sha256=
|
|
37
|
-
pydantic_ai/models/groq.py,sha256=
|
|
38
|
-
pydantic_ai/models/instrumented.py,sha256=
|
|
39
|
-
pydantic_ai/models/mistral.py,sha256=
|
|
40
|
-
pydantic_ai/models/openai.py,sha256=
|
|
41
|
-
pydantic_ai/models/test.py,sha256=
|
|
35
|
+
pydantic_ai/models/function.py,sha256=xvN_oNKw0X4c16oe1l3MX2_kJtFWMOMaseMNO6eNBYI,11709
|
|
36
|
+
pydantic_ai/models/gemini.py,sha256=d8HY9nc-tcuWFmA5OdKsWABMTpXq68sUL6xE8zY6dzs,37383
|
|
37
|
+
pydantic_ai/models/google.py,sha256=AVXC3CPG1aduGXSc0XFEYnrT6LsNKfNWp-kmf1SQssg,22294
|
|
38
|
+
pydantic_ai/models/groq.py,sha256=lojKRdvg0p-EtZ20Z2CS4I0goq4CoGkLj3LuYHA6o-I,18497
|
|
39
|
+
pydantic_ai/models/instrumented.py,sha256=vVq7mS071EXS2PZ3NJ4Zgt93iQgAscFr2dyg9fAeuCE,15703
|
|
40
|
+
pydantic_ai/models/mistral.py,sha256=LHm3F2yVKoE1uDjEPtTPug6duHwr4A42qey2Pncqqx4,30093
|
|
41
|
+
pydantic_ai/models/openai.py,sha256=onyJSKCo5zj_VY22RTQnPRE0Bpxu1ojgtftveQF_VQc,49633
|
|
42
|
+
pydantic_ai/models/test.py,sha256=X5QVCsBAWXxw4MKet-UTGZ0FteUnCHoK3Py3ngJM2Zk,17437
|
|
42
43
|
pydantic_ai/models/wrapper.py,sha256=43ntRkTF7rVBYLC-Ihdo1fkwpeveOpA_1fXe1fd3W9Y,1690
|
|
43
44
|
pydantic_ai/profiles/__init__.py,sha256=uO_f1kSqrnXuO0x5U0EHTTMRYcmOiOoa-tS1OZppxBk,1426
|
|
44
45
|
pydantic_ai/profiles/_json_schema.py,sha256=3ofRGnBca9WzqlUbw0C1ywhv_V7eGTmFAf2O7Bs5zgk,7199
|
|
@@ -69,8 +70,8 @@ pydantic_ai/providers/mistral.py,sha256=EIUSENjFuGzBhvbdrarUTM4VPkesIMnZrzfnEKHO
|
|
|
69
70
|
pydantic_ai/providers/openai.py,sha256=7iGij0EaFylab7dTZAZDgXr78tr-HsZrn9EI9AkWBNQ,3091
|
|
70
71
|
pydantic_ai/providers/openrouter.py,sha256=NXjNdnlXIBrBMMqbzcWQnowXOuZh4NHikXenBn5h3mc,4061
|
|
71
72
|
pydantic_ai/providers/together.py,sha256=zFVSMSm5jXbpkNouvBOTjWrPmlPpCp6sQS5LMSyVjrQ,3482
|
|
72
|
-
pydantic_ai_slim-0.
|
|
73
|
-
pydantic_ai_slim-0.
|
|
74
|
-
pydantic_ai_slim-0.
|
|
75
|
-
pydantic_ai_slim-0.
|
|
76
|
-
pydantic_ai_slim-0.
|
|
73
|
+
pydantic_ai_slim-0.3.0.dist-info/METADATA,sha256=GmMBkJvakRA_lUHh_jO941_uxk5JwGKgWNle0dLCAOQ,3846
|
|
74
|
+
pydantic_ai_slim-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
75
|
+
pydantic_ai_slim-0.3.0.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
|
|
76
|
+
pydantic_ai_slim-0.3.0.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
|
|
77
|
+
pydantic_ai_slim-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|