mirascope 2.0.0a3__py3-none-any.whl → 2.0.0a4__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.
- mirascope/api/_generated/__init__.py +62 -6
- mirascope/api/_generated/client.py +8 -0
- mirascope/api/_generated/errors/__init__.py +11 -1
- mirascope/api/_generated/errors/conflict_error.py +15 -0
- mirascope/api/_generated/errors/forbidden_error.py +15 -0
- mirascope/api/_generated/errors/internal_server_error.py +15 -0
- mirascope/api/_generated/errors/not_found_error.py +15 -0
- mirascope/api/_generated/organizations/__init__.py +25 -0
- mirascope/api/_generated/organizations/client.py +380 -0
- mirascope/api/_generated/organizations/raw_client.py +876 -0
- mirascope/api/_generated/organizations/types/__init__.py +23 -0
- mirascope/api/_generated/organizations/types/organizations_create_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_get_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_list_response_item.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
- mirascope/api/_generated/organizations/types/organizations_update_response.py +24 -0
- mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
- mirascope/api/_generated/projects/__init__.py +17 -0
- mirascope/api/_generated/projects/client.py +458 -0
- mirascope/api/_generated/projects/raw_client.py +1016 -0
- mirascope/api/_generated/projects/types/__init__.py +15 -0
- mirascope/api/_generated/projects/types/projects_create_response.py +30 -0
- mirascope/api/_generated/projects/types/projects_get_response.py +30 -0
- mirascope/api/_generated/projects/types/projects_list_response_item.py +30 -0
- mirascope/api/_generated/projects/types/projects_update_response.py +30 -0
- mirascope/api/_generated/reference.md +586 -0
- mirascope/api/_generated/types/__init__.py +20 -4
- mirascope/api/_generated/types/already_exists_error.py +24 -0
- mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
- mirascope/api/_generated/types/database_error.py +24 -0
- mirascope/api/_generated/types/database_error_tag.py +5 -0
- mirascope/api/_generated/types/http_api_decode_error.py +1 -3
- mirascope/api/_generated/types/issue.py +1 -5
- mirascope/api/_generated/types/not_found_error_body.py +24 -0
- mirascope/api/_generated/types/not_found_error_tag.py +5 -0
- mirascope/api/_generated/types/permission_denied_error.py +24 -0
- mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
- mirascope/api/_generated/types/property_key.py +2 -2
- mirascope/api/_generated/types/{property_key_tag.py → property_key_key.py} +3 -5
- mirascope/api/_generated/types/{property_key_tag_tag.py → property_key_key_tag.py} +1 -1
- mirascope/llm/__init__.py +4 -0
- mirascope/llm/providers/__init__.py +6 -0
- mirascope/llm/providers/anthropic/__init__.py +6 -1
- mirascope/llm/providers/anthropic/_utils/__init__.py +15 -5
- mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
- mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
- mirascope/llm/providers/anthropic/_utils/decode.py +39 -7
- mirascope/llm/providers/anthropic/_utils/encode.py +156 -64
- mirascope/llm/providers/anthropic/beta_provider.py +322 -0
- mirascope/llm/providers/anthropic/model_id.py +10 -27
- mirascope/llm/providers/anthropic/model_info.py +87 -0
- mirascope/llm/providers/anthropic/provider.py +127 -145
- mirascope/llm/providers/base/_utils.py +15 -1
- mirascope/llm/providers/google/_utils/decode.py +55 -3
- mirascope/llm/providers/google/_utils/encode.py +14 -6
- mirascope/llm/providers/google/model_id.py +7 -13
- mirascope/llm/providers/google/model_info.py +62 -0
- mirascope/llm/providers/google/provider.py +8 -4
- mirascope/llm/providers/load_provider.py +8 -2
- mirascope/llm/providers/mlx/_utils.py +23 -1
- mirascope/llm/providers/mlx/encoding/transformers.py +17 -1
- mirascope/llm/providers/mlx/provider.py +4 -0
- mirascope/llm/providers/ollama/__init__.py +19 -0
- mirascope/llm/providers/ollama/provider.py +71 -0
- mirascope/llm/providers/openai/completions/__init__.py +6 -1
- mirascope/llm/providers/openai/completions/_utils/decode.py +57 -5
- mirascope/llm/providers/openai/completions/_utils/encode.py +9 -8
- mirascope/llm/providers/openai/completions/base_provider.py +513 -0
- mirascope/llm/providers/openai/completions/provider.py +13 -447
- mirascope/llm/providers/openai/model_info.py +57 -0
- mirascope/llm/providers/openai/provider.py +16 -4
- mirascope/llm/providers/openai/responses/_utils/decode.py +55 -4
- mirascope/llm/providers/openai/responses/_utils/encode.py +9 -9
- mirascope/llm/providers/openai/responses/provider.py +20 -21
- mirascope/llm/providers/provider_id.py +11 -1
- mirascope/llm/providers/provider_registry.py +3 -1
- mirascope/llm/providers/together/__init__.py +19 -0
- mirascope/llm/providers/together/provider.py +40 -0
- mirascope/llm/responses/__init__.py +3 -0
- mirascope/llm/responses/base_response.py +4 -0
- mirascope/llm/responses/base_stream_response.py +25 -1
- mirascope/llm/responses/finish_reason.py +1 -0
- mirascope/llm/responses/response.py +9 -0
- mirascope/llm/responses/root_response.py +5 -1
- mirascope/llm/responses/usage.py +95 -0
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/METADATA +3 -3
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/RECORD +91 -50
- mirascope/llm/providers/openai/shared/__init__.py +0 -7
- mirascope/llm/providers/openai/shared/_utils.py +0 -59
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/WHEEL +0 -0
- {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Beta Anthropic message encoding and request preparation."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any, TypedDict, cast
|
|
5
|
+
from typing_extensions import Required
|
|
6
|
+
|
|
7
|
+
from anthropic import Omit
|
|
8
|
+
from anthropic.types.anthropic_beta_param import AnthropicBetaParam
|
|
9
|
+
from anthropic.types.beta import (
|
|
10
|
+
BetaContentBlockParam,
|
|
11
|
+
BetaMessageParam,
|
|
12
|
+
BetaTextBlockParam,
|
|
13
|
+
BetaThinkingConfigParam,
|
|
14
|
+
BetaToolChoiceParam,
|
|
15
|
+
BetaToolParam,
|
|
16
|
+
)
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from ....content import ContentPart
|
|
20
|
+
from ....exceptions import FormattingModeNotSupportedError
|
|
21
|
+
from ....formatting import (
|
|
22
|
+
Format,
|
|
23
|
+
FormattableT,
|
|
24
|
+
_utils as _formatting_utils,
|
|
25
|
+
resolve_format,
|
|
26
|
+
)
|
|
27
|
+
from ....messages import AssistantMessage, Message, UserMessage
|
|
28
|
+
from ....tools import AnyToolSchema, BaseToolkit
|
|
29
|
+
from ...base import Params, _utils as _base_utils
|
|
30
|
+
from ..model_id import model_name
|
|
31
|
+
from ..model_info import MODELS_WITHOUT_STRICT_STRUCTURED_OUTPUTS
|
|
32
|
+
from .encode import (
|
|
33
|
+
DEFAULT_MAX_TOKENS,
|
|
34
|
+
FORMAT_TOOL_NAME,
|
|
35
|
+
convert_tool_to_tool_param,
|
|
36
|
+
encode_content,
|
|
37
|
+
process_params,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
DEFAULT_FORMAT_MODE = "strict"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BetaParseKwargs(TypedDict, total=False):
|
|
44
|
+
"""Kwargs for Anthropic beta.messages.parse method."""
|
|
45
|
+
|
|
46
|
+
model: Required[str]
|
|
47
|
+
max_tokens: Required[int]
|
|
48
|
+
messages: Sequence[BetaMessageParam]
|
|
49
|
+
system: Sequence[BetaTextBlockParam] | Omit
|
|
50
|
+
tools: Sequence[BetaToolParam] | Omit
|
|
51
|
+
tool_choice: BetaToolChoiceParam | Omit
|
|
52
|
+
temperature: float | Omit
|
|
53
|
+
top_p: float | Omit
|
|
54
|
+
top_k: int | Omit
|
|
55
|
+
stop_sequences: list[str] | Omit
|
|
56
|
+
thinking: BetaThinkingConfigParam | Omit
|
|
57
|
+
betas: list[AnthropicBetaParam]
|
|
58
|
+
output_format: type[BaseModel]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _beta_encode_content(
|
|
62
|
+
content: Sequence[ContentPart],
|
|
63
|
+
encode_thoughts: bool,
|
|
64
|
+
add_cache_control: bool = False,
|
|
65
|
+
) -> str | Sequence[BetaContentBlockParam]:
|
|
66
|
+
"""Convert mirascope content to Beta Anthropic content format."""
|
|
67
|
+
result = encode_content(content, encode_thoughts, add_cache_control)
|
|
68
|
+
if isinstance(result, str):
|
|
69
|
+
return result
|
|
70
|
+
return cast(Sequence[BetaContentBlockParam], result)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _beta_encode_message(
|
|
74
|
+
message: UserMessage | AssistantMessage,
|
|
75
|
+
model_id: str,
|
|
76
|
+
encode_thoughts: bool,
|
|
77
|
+
add_cache_control: bool = False,
|
|
78
|
+
) -> BetaMessageParam:
|
|
79
|
+
"""Convert user or assistant Message to Beta MessageParam format.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
message: The message to encode
|
|
83
|
+
model_id: The Anthropic model ID
|
|
84
|
+
encode_thoughts: Whether to encode thought blocks as text
|
|
85
|
+
add_cache_control: Whether to add cache_control to the last content block
|
|
86
|
+
"""
|
|
87
|
+
if (
|
|
88
|
+
message.role == "assistant"
|
|
89
|
+
and message.provider_id == "anthropic"
|
|
90
|
+
and message.model_id == model_id
|
|
91
|
+
and message.raw_message
|
|
92
|
+
and not encode_thoughts
|
|
93
|
+
and not add_cache_control
|
|
94
|
+
):
|
|
95
|
+
raw = cast(dict[str, Any], message.raw_message)
|
|
96
|
+
return BetaMessageParam(
|
|
97
|
+
role=raw["role"],
|
|
98
|
+
content=raw["content"],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
content = _beta_encode_content(message.content, encode_thoughts, add_cache_control)
|
|
102
|
+
|
|
103
|
+
return BetaMessageParam(
|
|
104
|
+
role=message.role,
|
|
105
|
+
content=content,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _beta_encode_messages(
|
|
110
|
+
messages: Sequence[UserMessage | AssistantMessage],
|
|
111
|
+
model_id: str,
|
|
112
|
+
encode_thoughts: bool,
|
|
113
|
+
) -> Sequence[BetaMessageParam]:
|
|
114
|
+
"""Encode messages and add cache control for multi-turn conversations.
|
|
115
|
+
|
|
116
|
+
If the conversation contains assistant messages (indicating multi-turn),
|
|
117
|
+
adds cache_control to the last content block of the last message.
|
|
118
|
+
"""
|
|
119
|
+
# Detect multi-turn conversations by checking for assistant messages
|
|
120
|
+
has_assistant_message = any(msg.role == "assistant" for msg in messages)
|
|
121
|
+
|
|
122
|
+
# Encode messages, adding cache_control to the last message if multi-turn
|
|
123
|
+
encoded_messages: list[BetaMessageParam] = []
|
|
124
|
+
for i, message in enumerate(messages):
|
|
125
|
+
is_last = i == len(messages) - 1
|
|
126
|
+
add_cache = has_assistant_message and is_last
|
|
127
|
+
encoded_messages.append(
|
|
128
|
+
_beta_encode_message(message, model_id, encode_thoughts, add_cache)
|
|
129
|
+
)
|
|
130
|
+
return encoded_messages
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _beta_convert_tool_to_tool_param(tool: AnyToolSchema) -> BetaToolParam:
|
|
134
|
+
"""Convert a single Mirascope tool to Beta Anthropic tool format."""
|
|
135
|
+
return cast(BetaToolParam, convert_tool_to_tool_param(tool))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def beta_encode_request(
|
|
139
|
+
*,
|
|
140
|
+
model_id: str,
|
|
141
|
+
messages: Sequence[Message],
|
|
142
|
+
tools: Sequence[AnyToolSchema] | BaseToolkit[AnyToolSchema] | None,
|
|
143
|
+
format: type[FormattableT] | Format[FormattableT] | None,
|
|
144
|
+
params: Params,
|
|
145
|
+
) -> tuple[Sequence[Message], Format[FormattableT] | None, BetaParseKwargs]:
|
|
146
|
+
"""Prepares a request for the Anthropic beta.messages.parse method."""
|
|
147
|
+
|
|
148
|
+
processed = process_params(params, DEFAULT_MAX_TOKENS)
|
|
149
|
+
encode_thoughts = processed.pop("encode_thoughts", False)
|
|
150
|
+
max_tokens = processed.pop("max_tokens", DEFAULT_MAX_TOKENS)
|
|
151
|
+
|
|
152
|
+
kwargs: BetaParseKwargs = BetaParseKwargs(
|
|
153
|
+
{
|
|
154
|
+
"model": model_name(model_id),
|
|
155
|
+
"max_tokens": max_tokens,
|
|
156
|
+
"betas": ["structured-outputs-2025-11-13"],
|
|
157
|
+
**processed,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
tools = tools.tools if isinstance(tools, BaseToolkit) else tools or []
|
|
162
|
+
anthropic_tools = [_beta_convert_tool_to_tool_param(tool) for tool in tools]
|
|
163
|
+
format = resolve_format(format, default_mode=DEFAULT_FORMAT_MODE)
|
|
164
|
+
|
|
165
|
+
if format is not None:
|
|
166
|
+
if format.mode == "strict":
|
|
167
|
+
if model_name(model_id) in MODELS_WITHOUT_STRICT_STRUCTURED_OUTPUTS:
|
|
168
|
+
raise FormattingModeNotSupportedError(
|
|
169
|
+
formatting_mode=format.mode,
|
|
170
|
+
provider_id="anthropic",
|
|
171
|
+
model_id=model_id,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
kwargs["output_format"] = cast(type[BaseModel], format.formattable)
|
|
175
|
+
|
|
176
|
+
if format.mode == "tool":
|
|
177
|
+
format_tool_schema = _formatting_utils.create_tool_schema(format)
|
|
178
|
+
anthropic_tools.append(_beta_convert_tool_to_tool_param(format_tool_schema))
|
|
179
|
+
if tools:
|
|
180
|
+
kwargs["tool_choice"] = {"type": "any"}
|
|
181
|
+
else:
|
|
182
|
+
kwargs["tool_choice"] = {
|
|
183
|
+
"type": "tool",
|
|
184
|
+
"name": FORMAT_TOOL_NAME,
|
|
185
|
+
"disable_parallel_tool_use": True,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if format.formatting_instructions:
|
|
189
|
+
messages = _base_utils.add_system_instructions(
|
|
190
|
+
messages, format.formatting_instructions
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if anthropic_tools:
|
|
194
|
+
# Add cache control to the last tool for prompt caching
|
|
195
|
+
last_tool = anthropic_tools[-1]
|
|
196
|
+
last_tool["cache_control"] = {"type": "ephemeral"}
|
|
197
|
+
kwargs["tools"] = anthropic_tools
|
|
198
|
+
|
|
199
|
+
system_message_content, remaining_messages = _base_utils.extract_system_message(
|
|
200
|
+
messages
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
kwargs["messages"] = _beta_encode_messages(
|
|
204
|
+
remaining_messages, model_id, encode_thoughts
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if system_message_content:
|
|
208
|
+
kwargs["system"] = [
|
|
209
|
+
BetaTextBlockParam(
|
|
210
|
+
type="text",
|
|
211
|
+
text=system_message_content,
|
|
212
|
+
cache_control={"type": "ephemeral"},
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
return messages, format, kwargs
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""Anthropic response decoding."""
|
|
1
|
+
"""Standard Anthropic response decoding."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any, TypeAlias, cast
|
|
5
5
|
|
|
6
6
|
from anthropic import types as anthropic_types
|
|
7
7
|
from anthropic.lib.streaming import AsyncMessageStreamManager, MessageStreamManager
|
|
8
|
+
from anthropic.types.beta import BetaUsage
|
|
8
9
|
|
|
9
10
|
from ....content import (
|
|
10
11
|
AssistantContentPart,
|
|
@@ -29,6 +30,8 @@ from ....responses import (
|
|
|
29
30
|
FinishReasonChunk,
|
|
30
31
|
RawMessageChunk,
|
|
31
32
|
RawStreamEventChunk,
|
|
33
|
+
Usage,
|
|
34
|
+
UsageDeltaChunk,
|
|
32
35
|
)
|
|
33
36
|
from ..model_id import AnthropicModelId, model_name
|
|
34
37
|
|
|
@@ -58,11 +61,30 @@ def _decode_assistant_content(
|
|
|
58
61
|
)
|
|
59
62
|
|
|
60
63
|
|
|
64
|
+
def decode_usage(
|
|
65
|
+
usage: anthropic_types.Usage | BetaUsage,
|
|
66
|
+
) -> Usage:
|
|
67
|
+
"""Convert Anthropic Usage (or BetaUsage) to Mirascope Usage."""
|
|
68
|
+
|
|
69
|
+
cache_read_tokens = usage.cache_read_input_tokens or 0
|
|
70
|
+
cache_write_tokens = usage.cache_creation_input_tokens or 0
|
|
71
|
+
input_tokens = usage.input_tokens + cache_read_tokens + cache_write_tokens
|
|
72
|
+
output_tokens = usage.output_tokens
|
|
73
|
+
return Usage(
|
|
74
|
+
input_tokens=input_tokens,
|
|
75
|
+
output_tokens=output_tokens,
|
|
76
|
+
cache_read_tokens=cache_read_tokens,
|
|
77
|
+
cache_write_tokens=cache_write_tokens,
|
|
78
|
+
reasoning_tokens=0,
|
|
79
|
+
raw=usage,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
61
83
|
def decode_response(
|
|
62
84
|
response: anthropic_types.Message,
|
|
63
85
|
model_id: AnthropicModelId,
|
|
64
|
-
) -> tuple[AssistantMessage, FinishReason | None]:
|
|
65
|
-
"""Convert Anthropic message to mirascope AssistantMessage."""
|
|
86
|
+
) -> tuple[AssistantMessage, FinishReason | None, Usage]:
|
|
87
|
+
"""Convert Anthropic message to mirascope AssistantMessage and usage."""
|
|
66
88
|
assistant_message = AssistantMessage(
|
|
67
89
|
content=[_decode_assistant_content(part) for part in response.content],
|
|
68
90
|
provider_id="anthropic",
|
|
@@ -78,14 +100,14 @@ def decode_response(
|
|
|
78
100
|
if response.stop_reason
|
|
79
101
|
else None
|
|
80
102
|
)
|
|
81
|
-
|
|
103
|
+
usage = decode_usage(response.usage)
|
|
104
|
+
return assistant_message, finish_reason, usage
|
|
82
105
|
|
|
83
106
|
|
|
84
107
|
ContentBlock: TypeAlias = (
|
|
85
108
|
anthropic_types.TextBlockParam
|
|
86
109
|
| anthropic_types.ThinkingBlockParam
|
|
87
110
|
| anthropic_types.ToolUseBlockParam
|
|
88
|
-
| anthropic_types.ThinkingBlockParam
|
|
89
111
|
| anthropic_types.RedactedThinkingBlockParam
|
|
90
112
|
)
|
|
91
113
|
|
|
@@ -210,6 +232,16 @@ class _AnthropicChunkProcessor:
|
|
|
210
232
|
if finish_reason is not None:
|
|
211
233
|
yield FinishReasonChunk(finish_reason=finish_reason)
|
|
212
234
|
|
|
235
|
+
# Emit usage delta
|
|
236
|
+
usage = event.usage
|
|
237
|
+
yield UsageDeltaChunk(
|
|
238
|
+
input_tokens=usage.input_tokens or 0,
|
|
239
|
+
output_tokens=usage.output_tokens,
|
|
240
|
+
cache_read_tokens=usage.cache_read_input_tokens or 0,
|
|
241
|
+
cache_write_tokens=usage.cache_creation_input_tokens or 0,
|
|
242
|
+
reasoning_tokens=0,
|
|
243
|
+
)
|
|
244
|
+
|
|
213
245
|
def raw_message_chunk(self) -> RawMessageChunk:
|
|
214
246
|
return RawMessageChunk(
|
|
215
247
|
raw_message=cast(
|
|
@@ -225,7 +257,7 @@ class _AnthropicChunkProcessor:
|
|
|
225
257
|
def decode_stream(
|
|
226
258
|
anthropic_stream_manager: MessageStreamManager,
|
|
227
259
|
) -> ChunkIterator:
|
|
228
|
-
"""Returns a ChunkIterator converted from an Anthropic MessageStreamManager"""
|
|
260
|
+
"""Returns a ChunkIterator converted from an Anthropic MessageStreamManager."""
|
|
229
261
|
processor = _AnthropicChunkProcessor()
|
|
230
262
|
with anthropic_stream_manager as stream:
|
|
231
263
|
for event in stream._raw_stream: # pyright: ignore[reportPrivateUsage]
|
|
@@ -236,7 +268,7 @@ def decode_stream(
|
|
|
236
268
|
async def decode_async_stream(
|
|
237
269
|
anthropic_stream_manager: AsyncMessageStreamManager,
|
|
238
270
|
) -> AsyncChunkIterator:
|
|
239
|
-
"""Returns an AsyncChunkIterator converted from an Anthropic MessageStreamManager"""
|
|
271
|
+
"""Returns an AsyncChunkIterator converted from an Anthropic MessageStreamManager."""
|
|
240
272
|
processor = _AnthropicChunkProcessor()
|
|
241
273
|
async with anthropic_stream_manager as stream:
|
|
242
274
|
async for event in stream._raw_stream: # pyright: ignore[reportPrivateUsage]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""Anthropic
|
|
1
|
+
"""Shared Anthropic encoding utilities."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from collections.abc import Sequence
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from typing import Literal, TypedDict, cast
|
|
6
|
+
from typing import Any, Literal, TypedDict, cast
|
|
7
7
|
from typing_extensions import Required
|
|
8
8
|
|
|
9
9
|
from anthropic import Omit, types as anthropic_types
|
|
@@ -22,14 +22,14 @@ from ...base import Params, _utils as _base_utils
|
|
|
22
22
|
from ..model_id import AnthropicModelId, model_name
|
|
23
23
|
|
|
24
24
|
DEFAULT_MAX_TOKENS = 16000
|
|
25
|
+
# TODO: Change DEFAULT_FORMAT_MODE to strict when strict is no longer a beta feature.
|
|
26
|
+
DEFAULT_FORMAT_MODE = "tool"
|
|
25
27
|
|
|
26
28
|
AnthropicImageMimeType = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
27
29
|
|
|
28
30
|
|
|
29
|
-
def encode_image_mime_type(
|
|
30
|
-
|
|
31
|
-
) -> AnthropicImageMimeType:
|
|
32
|
-
"""Convert an ImageMimeType into anthropic supported mime type"""
|
|
31
|
+
def encode_image_mime_type(mime_type: ImageMimeType) -> AnthropicImageMimeType:
|
|
32
|
+
"""Convert an ImageMimeType into anthropic supported mime type."""
|
|
33
33
|
if mime_type in ("image/jpeg", "image/png", "image/gif", "image/webp"):
|
|
34
34
|
return mime_type
|
|
35
35
|
raise FeatureNotSupportedError(
|
|
@@ -37,13 +37,60 @@ def encode_image_mime_type(
|
|
|
37
37
|
) # pragma: no cover
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class ProcessedParams(TypedDict, total=False):
|
|
41
|
+
"""Common parameters processed from Params."""
|
|
42
|
+
|
|
43
|
+
temperature: float
|
|
44
|
+
max_tokens: int
|
|
45
|
+
top_p: float
|
|
46
|
+
top_k: int
|
|
47
|
+
stop_sequences: list[str]
|
|
48
|
+
thinking: dict[str, Any]
|
|
49
|
+
encode_thoughts: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def process_params(params: Params, default_max_tokens: int) -> ProcessedParams:
|
|
53
|
+
"""Process common Anthropic parameters from Params.
|
|
54
|
+
|
|
55
|
+
Returns a dict with processed parameters that can be merged into kwargs.
|
|
56
|
+
"""
|
|
57
|
+
result: ProcessedParams = {
|
|
58
|
+
"max_tokens": default_max_tokens,
|
|
59
|
+
"encode_thoughts": False,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
with _base_utils.ensure_all_params_accessed(
|
|
63
|
+
params=params, provider_id="anthropic", unsupported_params=["seed"]
|
|
64
|
+
) as param_accessor:
|
|
65
|
+
if param_accessor.temperature is not None:
|
|
66
|
+
result["temperature"] = param_accessor.temperature
|
|
67
|
+
if param_accessor.max_tokens is not None:
|
|
68
|
+
result["max_tokens"] = param_accessor.max_tokens
|
|
69
|
+
if param_accessor.top_p is not None:
|
|
70
|
+
result["top_p"] = param_accessor.top_p
|
|
71
|
+
if param_accessor.top_k is not None:
|
|
72
|
+
result["top_k"] = param_accessor.top_k
|
|
73
|
+
if param_accessor.stop_sequences is not None:
|
|
74
|
+
result["stop_sequences"] = param_accessor.stop_sequences
|
|
75
|
+
if param_accessor.thinking is not None:
|
|
76
|
+
if param_accessor.thinking:
|
|
77
|
+
budget_tokens = max(1024, result["max_tokens"] // 2)
|
|
78
|
+
result["thinking"] = {"type": "enabled", "budget_tokens": budget_tokens}
|
|
79
|
+
else:
|
|
80
|
+
result["thinking"] = {"type": "disabled"}
|
|
81
|
+
if param_accessor.encode_thoughts_as_text:
|
|
82
|
+
result["encode_thoughts"] = True
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
40
87
|
class MessageCreateKwargs(TypedDict, total=False):
|
|
41
88
|
"""Kwargs for Anthropic Message.create method."""
|
|
42
89
|
|
|
43
90
|
model: Required[str]
|
|
44
91
|
max_tokens: Required[int]
|
|
45
92
|
messages: Sequence[anthropic_types.MessageParam]
|
|
46
|
-
system:
|
|
93
|
+
system: Sequence[anthropic_types.TextBlockParam] | Omit
|
|
47
94
|
tools: Sequence[anthropic_types.ToolParam] | Omit
|
|
48
95
|
tool_choice: anthropic_types.ToolChoiceParam | Omit
|
|
49
96
|
temperature: float | Omit
|
|
@@ -53,8 +100,10 @@ class MessageCreateKwargs(TypedDict, total=False):
|
|
|
53
100
|
thinking: anthropic_types.ThinkingConfigParam | Omit
|
|
54
101
|
|
|
55
102
|
|
|
56
|
-
def
|
|
57
|
-
content: Sequence[ContentPart],
|
|
103
|
+
def encode_content(
|
|
104
|
+
content: Sequence[ContentPart],
|
|
105
|
+
encode_thoughts: bool,
|
|
106
|
+
add_cache_control: bool,
|
|
58
107
|
) -> str | Sequence[anthropic_types.ContentBlockParam]:
|
|
59
108
|
"""Convert mirascope content to Anthropic content format."""
|
|
60
109
|
|
|
@@ -65,15 +114,42 @@ def _encode_content(
|
|
|
65
114
|
"anthropic",
|
|
66
115
|
message="Anthropic does not support empty message content.",
|
|
67
116
|
)
|
|
117
|
+
if add_cache_control:
|
|
118
|
+
return [
|
|
119
|
+
anthropic_types.TextBlockParam(
|
|
120
|
+
type="text",
|
|
121
|
+
text=content[0].text,
|
|
122
|
+
cache_control={"type": "ephemeral"},
|
|
123
|
+
)
|
|
124
|
+
]
|
|
68
125
|
return content[0].text
|
|
69
126
|
|
|
70
127
|
blocks: list[anthropic_types.ContentBlockParam] = []
|
|
71
128
|
|
|
72
|
-
|
|
129
|
+
# Find the last cacheable content part (text, image, tool_result, or tool_call)
|
|
130
|
+
last_cacheable_index = -1
|
|
131
|
+
if add_cache_control:
|
|
132
|
+
for i in range(len(content) - 1, -1, -1):
|
|
133
|
+
part = content[i]
|
|
134
|
+
if part.type in ("text", "image", "tool_output", "tool_call"):
|
|
135
|
+
if part.type == "text" and not part.text: # pragma: no cover
|
|
136
|
+
continue # Skip empty text
|
|
137
|
+
last_cacheable_index = i
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
for i, part in enumerate(content):
|
|
141
|
+
should_add_cache = add_cache_control and i == last_cacheable_index
|
|
142
|
+
|
|
73
143
|
if part.type == "text":
|
|
74
144
|
if part.text:
|
|
75
145
|
blocks.append(
|
|
76
|
-
anthropic_types.TextBlockParam(
|
|
146
|
+
anthropic_types.TextBlockParam(
|
|
147
|
+
type="text",
|
|
148
|
+
text=part.text,
|
|
149
|
+
cache_control={"type": "ephemeral"}
|
|
150
|
+
if should_add_cache
|
|
151
|
+
else None,
|
|
152
|
+
)
|
|
77
153
|
)
|
|
78
154
|
elif part.type == "image":
|
|
79
155
|
source: (
|
|
@@ -91,7 +167,13 @@ def _encode_content(
|
|
|
91
167
|
type="url",
|
|
92
168
|
url=part.source.url,
|
|
93
169
|
)
|
|
94
|
-
blocks.append(
|
|
170
|
+
blocks.append(
|
|
171
|
+
anthropic_types.ImageBlockParam(
|
|
172
|
+
type="image",
|
|
173
|
+
source=source,
|
|
174
|
+
cache_control={"type": "ephemeral"} if should_add_cache else None,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
95
177
|
elif part.type == "audio":
|
|
96
178
|
raise FeatureNotSupportedError(
|
|
97
179
|
"audio input",
|
|
@@ -104,6 +186,7 @@ def _encode_content(
|
|
|
104
186
|
type="tool_result",
|
|
105
187
|
tool_use_id=part.id,
|
|
106
188
|
content=str(part.value),
|
|
189
|
+
cache_control={"type": "ephemeral"} if should_add_cache else None,
|
|
107
190
|
)
|
|
108
191
|
)
|
|
109
192
|
elif part.type == "tool_call":
|
|
@@ -113,6 +196,7 @@ def _encode_content(
|
|
|
113
196
|
id=part.id,
|
|
114
197
|
name=part.name,
|
|
115
198
|
input=json.loads(part.args),
|
|
199
|
+
cache_control={"type": "ephemeral"} if should_add_cache else None,
|
|
116
200
|
)
|
|
117
201
|
)
|
|
118
202
|
elif part.type == "thought":
|
|
@@ -139,33 +223,60 @@ def _encode_message(
|
|
|
139
223
|
message: UserMessage | AssistantMessage,
|
|
140
224
|
model_id: AnthropicModelId,
|
|
141
225
|
encode_thoughts: bool,
|
|
226
|
+
add_cache_control: bool = False,
|
|
142
227
|
) -> anthropic_types.MessageParam:
|
|
143
|
-
"""Convert user or assistant
|
|
228
|
+
"""Convert user or assistant Message to Anthropic MessageParam format.
|
|
144
229
|
|
|
145
230
|
Args:
|
|
146
|
-
|
|
147
|
-
model_id: The Anthropic model ID
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
A Sequence of converted Anthropic `MessageParam`
|
|
231
|
+
message: The message to encode
|
|
232
|
+
model_id: The Anthropic model ID
|
|
233
|
+
encode_thoughts: Whether to encode thought blocks as text
|
|
234
|
+
add_cache_control: Whether to add cache_control to the last content block
|
|
151
235
|
"""
|
|
152
|
-
|
|
153
236
|
if (
|
|
154
237
|
message.role == "assistant"
|
|
155
238
|
and message.provider_id == "anthropic"
|
|
156
239
|
and message.model_id == model_id
|
|
157
240
|
and message.raw_message
|
|
158
241
|
and not encode_thoughts
|
|
242
|
+
and not add_cache_control
|
|
159
243
|
):
|
|
160
244
|
return cast(anthropic_types.MessageParam, message.raw_message)
|
|
245
|
+
|
|
246
|
+
content = encode_content(message.content, encode_thoughts, add_cache_control)
|
|
247
|
+
|
|
161
248
|
return {
|
|
162
249
|
"role": message.role,
|
|
163
|
-
"content":
|
|
250
|
+
"content": content,
|
|
164
251
|
}
|
|
165
252
|
|
|
166
253
|
|
|
254
|
+
def _encode_messages(
|
|
255
|
+
messages: Sequence[UserMessage | AssistantMessage],
|
|
256
|
+
model_id: AnthropicModelId,
|
|
257
|
+
encode_thoughts: bool,
|
|
258
|
+
) -> Sequence[anthropic_types.MessageParam]:
|
|
259
|
+
"""Encode messages and add cache control for multi-turn conversations.
|
|
260
|
+
|
|
261
|
+
If the conversation contains assistant messages (indicating multi-turn),
|
|
262
|
+
adds cache_control to the last content block of the last message.
|
|
263
|
+
"""
|
|
264
|
+
# Detect multi-turn conversations by checking for assistant messages
|
|
265
|
+
has_assistant_message = any(msg.role == "assistant" for msg in messages)
|
|
266
|
+
|
|
267
|
+
# Encode messages, adding cache_control to the last message if multi-turn
|
|
268
|
+
encoded_messages: list[anthropic_types.MessageParam] = []
|
|
269
|
+
for i, message in enumerate(messages):
|
|
270
|
+
is_last = i == len(messages) - 1
|
|
271
|
+
add_cache = has_assistant_message and is_last
|
|
272
|
+
encoded_messages.append(
|
|
273
|
+
_encode_message(message, model_id, encode_thoughts, add_cache)
|
|
274
|
+
)
|
|
275
|
+
return encoded_messages
|
|
276
|
+
|
|
277
|
+
|
|
167
278
|
@lru_cache(maxsize=128)
|
|
168
|
-
def
|
|
279
|
+
def convert_tool_to_tool_param(tool: AnyToolSchema) -> anthropic_types.ToolParam:
|
|
169
280
|
"""Convert a single Mirascope tool to Anthropic tool format with caching."""
|
|
170
281
|
schema_dict = tool.parameters.model_dump(by_alias=True, exclude_none=True)
|
|
171
282
|
schema_dict["type"] = "object"
|
|
@@ -184,54 +295,29 @@ def encode_request(
|
|
|
184
295
|
format: type[FormattableT] | Format[FormattableT] | None,
|
|
185
296
|
params: Params,
|
|
186
297
|
) -> tuple[Sequence[Message], Format[FormattableT] | None, MessageCreateKwargs]:
|
|
187
|
-
"""Prepares a request for the
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
298
|
+
"""Prepares a request for the Anthropic messages.create method."""
|
|
299
|
+
|
|
300
|
+
processed = process_params(params, DEFAULT_MAX_TOKENS)
|
|
301
|
+
encode_thoughts = processed.pop("encode_thoughts", False)
|
|
302
|
+
max_tokens = processed.pop("max_tokens", DEFAULT_MAX_TOKENS)
|
|
192
303
|
|
|
193
304
|
kwargs: MessageCreateKwargs = MessageCreateKwargs(
|
|
194
|
-
{
|
|
195
|
-
"model": model_name(model_id),
|
|
196
|
-
"max_tokens": DEFAULT_MAX_TOKENS,
|
|
197
|
-
}
|
|
305
|
+
{"model": model_name(model_id), "max_tokens": max_tokens, **processed}
|
|
198
306
|
)
|
|
199
|
-
encode_thoughts = False
|
|
200
|
-
|
|
201
|
-
with _base_utils.ensure_all_params_accessed(
|
|
202
|
-
params=params, provider_id="anthropic", unsupported_params=["seed"]
|
|
203
|
-
) as param_accessor:
|
|
204
|
-
if param_accessor.temperature is not None:
|
|
205
|
-
kwargs["temperature"] = param_accessor.temperature
|
|
206
|
-
if param_accessor.max_tokens is not None:
|
|
207
|
-
kwargs["max_tokens"] = param_accessor.max_tokens
|
|
208
|
-
if param_accessor.top_p is not None:
|
|
209
|
-
kwargs["top_p"] = param_accessor.top_p
|
|
210
|
-
if param_accessor.top_k is not None:
|
|
211
|
-
kwargs["top_k"] = param_accessor.top_k
|
|
212
|
-
if param_accessor.stop_sequences is not None:
|
|
213
|
-
kwargs["stop_sequences"] = param_accessor.stop_sequences
|
|
214
|
-
if param_accessor.thinking is not None:
|
|
215
|
-
if param_accessor.thinking:
|
|
216
|
-
# Set budget to 50% of max_tokens with minimum of 1024
|
|
217
|
-
budget_tokens = max(1024, kwargs["max_tokens"] // 2)
|
|
218
|
-
kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget_tokens}
|
|
219
|
-
else:
|
|
220
|
-
kwargs["thinking"] = {"type": "disabled"}
|
|
221
|
-
if param_accessor.encode_thoughts_as_text:
|
|
222
|
-
encode_thoughts = True
|
|
223
307
|
|
|
224
308
|
tools = tools.tools if isinstance(tools, BaseToolkit) else tools or []
|
|
225
|
-
anthropic_tools = [
|
|
226
|
-
format = resolve_format(format, default_mode=
|
|
309
|
+
anthropic_tools = [convert_tool_to_tool_param(tool) for tool in tools]
|
|
310
|
+
format = resolve_format(format, default_mode=DEFAULT_FORMAT_MODE)
|
|
227
311
|
if format is not None:
|
|
228
312
|
if format.mode == "strict":
|
|
229
313
|
raise FormattingModeNotSupportedError(
|
|
230
|
-
formatting_mode="strict",
|
|
314
|
+
formatting_mode="strict",
|
|
315
|
+
provider_id="anthropic",
|
|
316
|
+
model_id=model_id,
|
|
231
317
|
)
|
|
232
|
-
|
|
318
|
+
if format.mode == "tool":
|
|
233
319
|
format_tool_schema = _formatting_utils.create_tool_schema(format)
|
|
234
|
-
anthropic_tools.append(
|
|
320
|
+
anthropic_tools.append(convert_tool_to_tool_param(format_tool_schema))
|
|
235
321
|
if tools:
|
|
236
322
|
kwargs["tool_choice"] = {"type": "any"}
|
|
237
323
|
else:
|
|
@@ -247,18 +333,24 @@ def encode_request(
|
|
|
247
333
|
)
|
|
248
334
|
|
|
249
335
|
if anthropic_tools:
|
|
336
|
+
# Add cache control to the last tool for prompt caching
|
|
337
|
+
last_tool = anthropic_tools[-1]
|
|
338
|
+
last_tool["cache_control"] = {"type": "ephemeral"}
|
|
250
339
|
kwargs["tools"] = anthropic_tools
|
|
251
340
|
|
|
252
341
|
system_message_content, remaining_messages = _base_utils.extract_system_message(
|
|
253
342
|
messages
|
|
254
343
|
)
|
|
255
344
|
|
|
256
|
-
kwargs["messages"] =
|
|
257
|
-
_encode_message(remaining_message, model_id, encode_thoughts)
|
|
258
|
-
for remaining_message in remaining_messages
|
|
259
|
-
]
|
|
345
|
+
kwargs["messages"] = _encode_messages(remaining_messages, model_id, encode_thoughts)
|
|
260
346
|
|
|
261
347
|
if system_message_content:
|
|
262
|
-
kwargs["system"] =
|
|
348
|
+
kwargs["system"] = [
|
|
349
|
+
anthropic_types.TextBlockParam(
|
|
350
|
+
type="text",
|
|
351
|
+
text=system_message_content,
|
|
352
|
+
cache_control={"type": "ephemeral"},
|
|
353
|
+
)
|
|
354
|
+
]
|
|
263
355
|
|
|
264
356
|
return messages, format, kwargs
|