mirascope 2.0.0a3__py3-none-any.whl → 2.0.0a5__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 (118) hide show
  1. mirascope/api/_generated/__init__.py +78 -6
  2. mirascope/api/_generated/api_keys/__init__.py +7 -0
  3. mirascope/api/_generated/api_keys/client.py +453 -0
  4. mirascope/api/_generated/api_keys/raw_client.py +853 -0
  5. mirascope/api/_generated/api_keys/types/__init__.py +9 -0
  6. mirascope/api/_generated/api_keys/types/api_keys_create_response.py +36 -0
  7. mirascope/api/_generated/api_keys/types/api_keys_get_response.py +35 -0
  8. mirascope/api/_generated/api_keys/types/api_keys_list_response_item.py +35 -0
  9. mirascope/api/_generated/client.py +14 -0
  10. mirascope/api/_generated/environments/__init__.py +17 -0
  11. mirascope/api/_generated/environments/client.py +532 -0
  12. mirascope/api/_generated/environments/raw_client.py +1088 -0
  13. mirascope/api/_generated/environments/types/__init__.py +15 -0
  14. mirascope/api/_generated/environments/types/environments_create_response.py +26 -0
  15. mirascope/api/_generated/environments/types/environments_get_response.py +26 -0
  16. mirascope/api/_generated/environments/types/environments_list_response_item.py +26 -0
  17. mirascope/api/_generated/environments/types/environments_update_response.py +26 -0
  18. mirascope/api/_generated/errors/__init__.py +11 -1
  19. mirascope/api/_generated/errors/conflict_error.py +15 -0
  20. mirascope/api/_generated/errors/forbidden_error.py +15 -0
  21. mirascope/api/_generated/errors/internal_server_error.py +15 -0
  22. mirascope/api/_generated/errors/not_found_error.py +15 -0
  23. mirascope/api/_generated/organizations/__init__.py +25 -0
  24. mirascope/api/_generated/organizations/client.py +404 -0
  25. mirascope/api/_generated/organizations/raw_client.py +902 -0
  26. mirascope/api/_generated/organizations/types/__init__.py +23 -0
  27. mirascope/api/_generated/organizations/types/organizations_create_response.py +25 -0
  28. mirascope/api/_generated/organizations/types/organizations_create_response_role.py +7 -0
  29. mirascope/api/_generated/organizations/types/organizations_get_response.py +25 -0
  30. mirascope/api/_generated/organizations/types/organizations_get_response_role.py +7 -0
  31. mirascope/api/_generated/organizations/types/organizations_list_response_item.py +25 -0
  32. mirascope/api/_generated/organizations/types/organizations_list_response_item_role.py +7 -0
  33. mirascope/api/_generated/organizations/types/organizations_update_response.py +25 -0
  34. mirascope/api/_generated/organizations/types/organizations_update_response_role.py +7 -0
  35. mirascope/api/_generated/projects/__init__.py +17 -0
  36. mirascope/api/_generated/projects/client.py +482 -0
  37. mirascope/api/_generated/projects/raw_client.py +1058 -0
  38. mirascope/api/_generated/projects/types/__init__.py +15 -0
  39. mirascope/api/_generated/projects/types/projects_create_response.py +31 -0
  40. mirascope/api/_generated/projects/types/projects_get_response.py +31 -0
  41. mirascope/api/_generated/projects/types/projects_list_response_item.py +31 -0
  42. mirascope/api/_generated/projects/types/projects_update_response.py +31 -0
  43. mirascope/api/_generated/reference.md +1311 -0
  44. mirascope/api/_generated/types/__init__.py +20 -4
  45. mirascope/api/_generated/types/already_exists_error.py +24 -0
  46. mirascope/api/_generated/types/already_exists_error_tag.py +5 -0
  47. mirascope/api/_generated/types/database_error.py +24 -0
  48. mirascope/api/_generated/types/database_error_tag.py +5 -0
  49. mirascope/api/_generated/types/http_api_decode_error.py +1 -3
  50. mirascope/api/_generated/types/issue.py +1 -5
  51. mirascope/api/_generated/types/not_found_error_body.py +24 -0
  52. mirascope/api/_generated/types/not_found_error_tag.py +5 -0
  53. mirascope/api/_generated/types/permission_denied_error.py +24 -0
  54. mirascope/api/_generated/types/permission_denied_error_tag.py +7 -0
  55. mirascope/api/_generated/types/property_key.py +2 -2
  56. mirascope/api/_generated/types/{property_key_tag.py → property_key_key.py} +3 -5
  57. mirascope/api/_generated/types/{property_key_tag_tag.py → property_key_key_tag.py} +1 -1
  58. mirascope/llm/__init__.py +6 -2
  59. mirascope/llm/exceptions.py +28 -0
  60. mirascope/llm/providers/__init__.py +12 -4
  61. mirascope/llm/providers/anthropic/__init__.py +6 -1
  62. mirascope/llm/providers/anthropic/_utils/__init__.py +17 -5
  63. mirascope/llm/providers/anthropic/_utils/beta_decode.py +271 -0
  64. mirascope/llm/providers/anthropic/_utils/beta_encode.py +216 -0
  65. mirascope/llm/providers/anthropic/_utils/decode.py +39 -7
  66. mirascope/llm/providers/anthropic/_utils/encode.py +156 -64
  67. mirascope/llm/providers/anthropic/_utils/errors.py +46 -0
  68. mirascope/llm/providers/anthropic/beta_provider.py +328 -0
  69. mirascope/llm/providers/anthropic/model_id.py +10 -27
  70. mirascope/llm/providers/anthropic/model_info.py +87 -0
  71. mirascope/llm/providers/anthropic/provider.py +132 -145
  72. mirascope/llm/providers/base/__init__.py +2 -1
  73. mirascope/llm/providers/base/_utils.py +15 -1
  74. mirascope/llm/providers/base/base_provider.py +173 -58
  75. mirascope/llm/providers/google/_utils/__init__.py +2 -0
  76. mirascope/llm/providers/google/_utils/decode.py +55 -3
  77. mirascope/llm/providers/google/_utils/encode.py +14 -6
  78. mirascope/llm/providers/google/_utils/errors.py +49 -0
  79. mirascope/llm/providers/google/model_id.py +7 -13
  80. mirascope/llm/providers/google/model_info.py +62 -0
  81. mirascope/llm/providers/google/provider.py +13 -8
  82. mirascope/llm/providers/mlx/_utils.py +31 -2
  83. mirascope/llm/providers/mlx/encoding/transformers.py +17 -1
  84. mirascope/llm/providers/mlx/provider.py +12 -0
  85. mirascope/llm/providers/ollama/__init__.py +19 -0
  86. mirascope/llm/providers/ollama/provider.py +71 -0
  87. mirascope/llm/providers/openai/__init__.py +10 -1
  88. mirascope/llm/providers/openai/_utils/__init__.py +5 -0
  89. mirascope/llm/providers/openai/_utils/errors.py +46 -0
  90. mirascope/llm/providers/openai/completions/__init__.py +6 -1
  91. mirascope/llm/providers/openai/completions/_utils/decode.py +57 -5
  92. mirascope/llm/providers/openai/completions/_utils/encode.py +9 -8
  93. mirascope/llm/providers/openai/completions/base_provider.py +513 -0
  94. mirascope/llm/providers/openai/completions/provider.py +13 -447
  95. mirascope/llm/providers/openai/model_info.py +57 -0
  96. mirascope/llm/providers/openai/provider.py +30 -5
  97. mirascope/llm/providers/openai/responses/_utils/decode.py +55 -4
  98. mirascope/llm/providers/openai/responses/_utils/encode.py +9 -9
  99. mirascope/llm/providers/openai/responses/provider.py +33 -28
  100. mirascope/llm/providers/provider_id.py +11 -1
  101. mirascope/llm/providers/provider_registry.py +59 -4
  102. mirascope/llm/providers/together/__init__.py +19 -0
  103. mirascope/llm/providers/together/provider.py +40 -0
  104. mirascope/llm/responses/__init__.py +3 -0
  105. mirascope/llm/responses/base_response.py +4 -0
  106. mirascope/llm/responses/base_stream_response.py +25 -1
  107. mirascope/llm/responses/finish_reason.py +1 -0
  108. mirascope/llm/responses/response.py +9 -0
  109. mirascope/llm/responses/root_response.py +5 -1
  110. mirascope/llm/responses/usage.py +95 -0
  111. mirascope/ops/_internal/closure.py +62 -11
  112. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/METADATA +3 -3
  113. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/RECORD +115 -56
  114. mirascope/llm/providers/load_provider.py +0 -48
  115. mirascope/llm/providers/openai/shared/__init__.py +0 -7
  116. mirascope/llm/providers/openai/shared/_utils.py +0 -59
  117. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/WHEEL +0 -0
  118. {mirascope-2.0.0a3.dist-info → mirascope-2.0.0a5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,271 @@
1
+ """Beta Anthropic response decoding."""
2
+
3
+ import json
4
+ from typing import Any, TypeAlias, cast
5
+
6
+ from anthropic.lib.streaming._beta_messages import (
7
+ BetaAsyncMessageStreamManager,
8
+ BetaMessageStreamManager,
9
+ )
10
+ from anthropic.types.beta import (
11
+ BetaContentBlock,
12
+ BetaRawMessageStreamEvent,
13
+ BetaRedactedThinkingBlockParam,
14
+ BetaTextBlockParam,
15
+ BetaThinkingBlockParam,
16
+ BetaToolUseBlockParam,
17
+ )
18
+ from anthropic.types.beta.parsed_beta_message import ParsedBetaMessage
19
+
20
+ from ....content import (
21
+ AssistantContentPart,
22
+ Text,
23
+ TextChunk,
24
+ TextEndChunk,
25
+ TextStartChunk,
26
+ Thought,
27
+ ThoughtChunk,
28
+ ThoughtEndChunk,
29
+ ThoughtStartChunk,
30
+ ToolCall,
31
+ ToolCallChunk,
32
+ ToolCallEndChunk,
33
+ ToolCallStartChunk,
34
+ )
35
+ from ....messages import AssistantMessage
36
+ from ....responses import (
37
+ AsyncChunkIterator,
38
+ ChunkIterator,
39
+ FinishReason,
40
+ FinishReasonChunk,
41
+ RawMessageChunk,
42
+ RawStreamEventChunk,
43
+ Usage,
44
+ UsageDeltaChunk,
45
+ )
46
+ from ..model_id import model_name
47
+ from .decode import decode_usage
48
+
49
+ BETA_FINISH_REASON_MAP = {
50
+ "max_tokens": FinishReason.MAX_TOKENS,
51
+ "refusal": FinishReason.REFUSAL,
52
+ "model_context_window_exceeded": FinishReason.CONTEXT_LENGTH_EXCEEDED,
53
+ }
54
+
55
+
56
+ def _decode_beta_assistant_content(content: BetaContentBlock) -> AssistantContentPart:
57
+ """Convert Beta content block to mirascope AssistantContentPart."""
58
+ if content.type == "text":
59
+ return Text(text=content.text)
60
+ elif content.type == "tool_use":
61
+ return ToolCall(
62
+ id=content.id,
63
+ name=content.name,
64
+ args=json.dumps(content.input),
65
+ )
66
+ elif content.type == "thinking":
67
+ return Thought(thought=content.thinking)
68
+ else:
69
+ raise NotImplementedError(
70
+ f"Support for beta content type `{content.type}` is not yet implemented."
71
+ )
72
+
73
+
74
+ def beta_decode_response(
75
+ response: ParsedBetaMessage[Any],
76
+ model_id: str,
77
+ ) -> tuple[AssistantMessage, FinishReason | None, Usage]:
78
+ """Convert Beta message to mirascope AssistantMessage and usage."""
79
+ assistant_message = AssistantMessage(
80
+ content=[_decode_beta_assistant_content(part) for part in response.content],
81
+ provider_id="anthropic",
82
+ model_id=model_id,
83
+ provider_model_name=model_name(model_id),
84
+ raw_message={
85
+ "role": response.role,
86
+ "content": [
87
+ part.model_dump(exclude_none=True) for part in response.content
88
+ ],
89
+ },
90
+ )
91
+ finish_reason = (
92
+ BETA_FINISH_REASON_MAP.get(response.stop_reason)
93
+ if response.stop_reason
94
+ else None
95
+ )
96
+ usage = decode_usage(response.usage)
97
+ return assistant_message, finish_reason, usage
98
+
99
+
100
+ BetaContentBlockParam: TypeAlias = (
101
+ BetaTextBlockParam
102
+ | BetaThinkingBlockParam
103
+ | BetaToolUseBlockParam
104
+ | BetaRedactedThinkingBlockParam
105
+ )
106
+
107
+
108
+ class _BetaChunkProcessor:
109
+ """Processes Beta stream events and maintains state across events."""
110
+
111
+ def __init__(self) -> None:
112
+ self.current_block_param: BetaContentBlockParam | None = None
113
+ self.accumulated_tool_json: str = ""
114
+ self.accumulated_blocks: list[BetaContentBlockParam] = []
115
+
116
+ def process_event(self, event: BetaRawMessageStreamEvent) -> ChunkIterator:
117
+ """Process a single Beta event and yield the appropriate content chunks."""
118
+ yield RawStreamEventChunk(raw_stream_event=event)
119
+
120
+ if event.type == "content_block_start":
121
+ content_block = event.content_block
122
+
123
+ if content_block.type == "text":
124
+ self.current_block_param = {
125
+ "type": "text",
126
+ "text": content_block.text,
127
+ }
128
+ yield TextStartChunk()
129
+ elif content_block.type == "tool_use":
130
+ self.current_block_param = {
131
+ "type": "tool_use",
132
+ "id": content_block.id,
133
+ "name": content_block.name,
134
+ "input": {},
135
+ }
136
+ self.accumulated_tool_json = ""
137
+ yield ToolCallStartChunk(
138
+ id=content_block.id,
139
+ name=content_block.name,
140
+ )
141
+ elif content_block.type == "thinking":
142
+ self.current_block_param = {
143
+ "type": "thinking",
144
+ "thinking": "",
145
+ "signature": "",
146
+ }
147
+ yield ThoughtStartChunk()
148
+ elif content_block.type == "redacted_thinking": # pragma: no cover
149
+ self.current_block_param = {
150
+ "type": "redacted_thinking",
151
+ "data": content_block.data,
152
+ }
153
+ else:
154
+ raise NotImplementedError(
155
+ f"Support for beta content block type `{content_block.type}` "
156
+ "is not yet implemented."
157
+ )
158
+
159
+ elif event.type == "content_block_delta":
160
+ if self.current_block_param is None: # pragma: no cover
161
+ raise RuntimeError("Received delta without a current block")
162
+
163
+ delta = event.delta
164
+ if delta.type == "text_delta":
165
+ if self.current_block_param["type"] != "text": # pragma: no cover
166
+ raise RuntimeError(
167
+ f"Received text_delta for {self.current_block_param['type']} block"
168
+ )
169
+ self.current_block_param["text"] += delta.text
170
+ yield TextChunk(delta=delta.text)
171
+ elif delta.type == "input_json_delta":
172
+ if self.current_block_param["type"] != "tool_use": # pragma: no cover
173
+ raise RuntimeError(
174
+ f"Received input_json_delta for {self.current_block_param['type']} block"
175
+ )
176
+ self.accumulated_tool_json += delta.partial_json
177
+ yield ToolCallChunk(delta=delta.partial_json)
178
+ elif delta.type == "thinking_delta":
179
+ if self.current_block_param["type"] != "thinking": # pragma: no cover
180
+ raise RuntimeError(
181
+ f"Received thinking_delta for {self.current_block_param['type']} block"
182
+ )
183
+ self.current_block_param["thinking"] += delta.thinking
184
+ yield ThoughtChunk(delta=delta.thinking)
185
+ elif delta.type == "signature_delta":
186
+ if self.current_block_param["type"] != "thinking": # pragma: no cover
187
+ raise RuntimeError(
188
+ f"Received signature_delta for {self.current_block_param['type']} block"
189
+ )
190
+ self.current_block_param["signature"] += delta.signature
191
+ else:
192
+ raise RuntimeError(
193
+ f"Received unsupported delta type: {delta.type}"
194
+ ) # pragma: no cover
195
+
196
+ elif event.type == "content_block_stop":
197
+ if self.current_block_param is None: # pragma: no cover
198
+ raise RuntimeError("Received stop without a current block")
199
+
200
+ block_type = self.current_block_param["type"]
201
+
202
+ if block_type == "text":
203
+ yield TextEndChunk()
204
+ elif block_type == "tool_use":
205
+ if self.current_block_param["type"] != "tool_use": # pragma: no cover
206
+ raise RuntimeError(
207
+ f"Block type mismatch: stored {self.current_block_param['type']}, expected tool_use"
208
+ )
209
+ self.current_block_param["input"] = (
210
+ json.loads(self.accumulated_tool_json)
211
+ if self.accumulated_tool_json
212
+ else {}
213
+ )
214
+ yield ToolCallEndChunk()
215
+ elif block_type == "thinking":
216
+ yield ThoughtEndChunk()
217
+ else:
218
+ raise NotImplementedError
219
+
220
+ self.accumulated_blocks.append(self.current_block_param)
221
+ self.current_block_param = None
222
+
223
+ elif event.type == "message_delta":
224
+ if event.delta.stop_reason:
225
+ finish_reason = BETA_FINISH_REASON_MAP.get(event.delta.stop_reason)
226
+ if finish_reason is not None:
227
+ yield FinishReasonChunk(finish_reason=finish_reason)
228
+
229
+ # Emit usage delta
230
+ usage = event.usage
231
+ yield UsageDeltaChunk(
232
+ input_tokens=usage.input_tokens or 0,
233
+ output_tokens=usage.output_tokens,
234
+ cache_read_tokens=usage.cache_read_input_tokens or 0,
235
+ cache_write_tokens=usage.cache_creation_input_tokens or 0,
236
+ reasoning_tokens=0,
237
+ )
238
+
239
+ def raw_message_chunk(self) -> RawMessageChunk:
240
+ return RawMessageChunk(
241
+ raw_message=cast(
242
+ dict[str, Any],
243
+ {
244
+ "role": "assistant",
245
+ "content": self.accumulated_blocks,
246
+ },
247
+ )
248
+ )
249
+
250
+
251
+ def beta_decode_stream(
252
+ beta_stream_manager: BetaMessageStreamManager[Any],
253
+ ) -> ChunkIterator:
254
+ """Returns a ChunkIterator converted from a Beta MessageStreamManager."""
255
+ processor = _BetaChunkProcessor()
256
+ with beta_stream_manager as stream:
257
+ for event in stream._raw_stream: # pyright: ignore[reportPrivateUsage]
258
+ yield from processor.process_event(event)
259
+ yield processor.raw_message_chunk()
260
+
261
+
262
+ async def beta_decode_async_stream(
263
+ beta_stream_manager: BetaAsyncMessageStreamManager[Any],
264
+ ) -> AsyncChunkIterator:
265
+ """Returns an AsyncChunkIterator converted from a Beta MessageStreamManager."""
266
+ processor = _BetaChunkProcessor()
267
+ async with beta_stream_manager as stream:
268
+ async for event in stream._raw_stream: # pyright: ignore[reportPrivateUsage]
269
+ for item in processor.process_event(event):
270
+ yield item
271
+ yield processor.raw_message_chunk()
@@ -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]