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.
Files changed (93) hide show
  1. mirascope/api/_generated/__init__.py +62 -6
  2. mirascope/api/_generated/client.py +8 -0
  3. mirascope/api/_generated/errors/__init__.py +11 -1
  4. mirascope/api/_generated/errors/conflict_error.py +15 -0
  5. mirascope/api/_generated/errors/forbidden_error.py +15 -0
  6. mirascope/api/_generated/errors/internal_server_error.py +15 -0
  7. mirascope/api/_generated/errors/not_found_error.py +15 -0
  8. mirascope/api/_generated/organizations/__init__.py +25 -0
  9. mirascope/api/_generated/organizations/client.py +380 -0
  10. mirascope/api/_generated/organizations/raw_client.py +876 -0
  11. mirascope/api/_generated/organizations/types/__init__.py +23 -0
  12. mirascope/api/_generated/organizations/types/organizations_create_response.py +24 -0
  13. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
  14. mirascope/api/_generated/organizations/types/organizations_get_response.py +24 -0
  15. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
  16. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +24 -0
  17. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
  18. mirascope/api/_generated/organizations/types/organizations_update_response.py +24 -0
  19. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
  20. mirascope/api/_generated/projects/__init__.py +17 -0
  21. mirascope/api/_generated/projects/client.py +458 -0
  22. mirascope/api/_generated/projects/raw_client.py +1016 -0
  23. mirascope/api/_generated/projects/types/__init__.py +15 -0
  24. mirascope/api/_generated/projects/types/projects_create_response.py +30 -0
  25. mirascope/api/_generated/projects/types/projects_get_response.py +30 -0
  26. mirascope/api/_generated/projects/types/projects_list_response_item.py +30 -0
  27. mirascope/api/_generated/projects/types/projects_update_response.py +30 -0
  28. mirascope/api/_generated/reference.md +586 -0
  29. mirascope/api/_generated/types/__init__.py +20 -4
  30. mirascope/api/_generated/types/already_exists_error.py +24 -0
  31. mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
  32. mirascope/api/_generated/types/database_error.py +24 -0
  33. mirascope/api/_generated/types/database_error_tag.py +5 -0
  34. mirascope/api/_generated/types/http_api_decode_error.py +1 -3
  35. mirascope/api/_generated/types/issue.py +1 -5
  36. mirascope/api/_generated/types/not_found_error_body.py +24 -0
  37. mirascope/api/_generated/types/not_found_error_tag.py +5 -0
  38. mirascope/api/_generated/types/permission_denied_error.py +24 -0
  39. mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
  40. mirascope/api/_generated/types/property_key.py +2 -2
  41. mirascope/api/_generated/types/{property_key_tag.py → property_key_key.py} +3 -5
  42. mirascope/api/_generated/types/{property_key_tag_tag.py → property_key_key_tag.py} +1 -1
  43. mirascope/llm/__init__.py +4 -0
  44. mirascope/llm/providers/__init__.py +6 -0
  45. mirascope/llm/providers/anthropic/__init__.py +6 -1
  46. mirascope/llm/providers/anthropic/_utils/__init__.py +15 -5
  47. mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
  48. mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
  49. mirascope/llm/providers/anthropic/_utils/decode.py +39 -7
  50. mirascope/llm/providers/anthropic/_utils/encode.py +156 -64
  51. mirascope/llm/providers/anthropic/beta_provider.py +322 -0
  52. mirascope/llm/providers/anthropic/model_id.py +10 -27
  53. mirascope/llm/providers/anthropic/model_info.py +87 -0
  54. mirascope/llm/providers/anthropic/provider.py +127 -145
  55. mirascope/llm/providers/base/_utils.py +15 -1
  56. mirascope/llm/providers/google/_utils/decode.py +55 -3
  57. mirascope/llm/providers/google/_utils/encode.py +14 -6
  58. mirascope/llm/providers/google/model_id.py +7 -13
  59. mirascope/llm/providers/google/model_info.py +62 -0
  60. mirascope/llm/providers/google/provider.py +8 -4
  61. mirascope/llm/providers/load_provider.py +8 -2
  62. mirascope/llm/providers/mlx/_utils.py +23 -1
  63. mirascope/llm/providers/mlx/encoding/transformers.py +17 -1
  64. mirascope/llm/providers/mlx/provider.py +4 -0
  65. mirascope/llm/providers/ollama/__init__.py +19 -0
  66. mirascope/llm/providers/ollama/provider.py +71 -0
  67. mirascope/llm/providers/openai/completions/__init__.py +6 -1
  68. mirascope/llm/providers/openai/completions/_utils/decode.py +57 -5
  69. mirascope/llm/providers/openai/completions/_utils/encode.py +9 -8
  70. mirascope/llm/providers/openai/completions/base_provider.py +513 -0
  71. mirascope/llm/providers/openai/completions/provider.py +13 -447
  72. mirascope/llm/providers/openai/model_info.py +57 -0
  73. mirascope/llm/providers/openai/provider.py +16 -4
  74. mirascope/llm/providers/openai/responses/_utils/decode.py +55 -4
  75. mirascope/llm/providers/openai/responses/_utils/encode.py +9 -9
  76. mirascope/llm/providers/openai/responses/provider.py +20 -21
  77. mirascope/llm/providers/provider_id.py +11 -1
  78. mirascope/llm/providers/provider_registry.py +3 -1
  79. mirascope/llm/providers/together/__init__.py +19 -0
  80. mirascope/llm/providers/together/provider.py +40 -0
  81. mirascope/llm/responses/__init__.py +3 -0
  82. mirascope/llm/responses/base_response.py +4 -0
  83. mirascope/llm/responses/base_stream_response.py +25 -1
  84. mirascope/llm/responses/finish_reason.py +1 -0
  85. mirascope/llm/responses/response.py +9 -0
  86. mirascope/llm/responses/root_response.py +5 -1
  87. mirascope/llm/responses/usage.py +95 -0
  88. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/METADATA +3 -3
  89. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/RECORD +91 -50
  90. mirascope/llm/providers/openai/shared/__init__.py +0 -7
  91. mirascope/llm/providers/openai/shared/_utils.py +0 -59
  92. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a4.dist-info}/WHEEL +0 -0
  93. {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
- return assistant_message, finish_reason
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 message encoding and request preparation."""
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
- mime_type: ImageMimeType,
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: str | Omit
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 _encode_content(
57
- content: Sequence[ContentPart], encode_thoughts: bool
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
- for part in content:
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(type="text", text=part.text)
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(anthropic_types.ImageBlockParam(type="image", source=source))
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 `Message`s to Anthropic `MessageParam` format.
228
+ """Convert user or assistant Message to Anthropic MessageParam format.
144
229
 
145
230
  Args:
146
- messages: A Sequence containing `UserMessage`s or `AssistantMessage`s
147
- model_id: The Anthropic model ID being used
148
-
149
- Returns:
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": _encode_content(message.content, encode_thoughts),
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 _convert_tool_to_tool_param(tool: AnyToolSchema) -> anthropic_types.ToolParam:
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 `Anthropic.messages.create` method."""
188
- if not model_id.startswith("anthropic/"): # pragma: no cover
189
- raise ValueError(
190
- f"Model ID must start with 'anthropic/' prefix, got: {model_id}"
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 = [_convert_tool_to_tool_param(tool) for tool in tools]
226
- format = resolve_format(format, default_mode="tool")
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", provider_id="anthropic"
314
+ formatting_mode="strict",
315
+ provider_id="anthropic",
316
+ model_id=model_id,
231
317
  )
232
- elif format.mode == "tool":
318
+ if format.mode == "tool":
233
319
  format_tool_schema = _formatting_utils.create_tool_schema(format)
234
- anthropic_tools.append(_convert_tool_to_tool_param(format_tool_schema))
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"] = system_message_content
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