aidial-adapter-anthropic 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. aidial_adapter_anthropic/_utils/json.py +116 -0
  2. aidial_adapter_anthropic/_utils/list.py +84 -0
  3. aidial_adapter_anthropic/_utils/pydantic.py +6 -0
  4. aidial_adapter_anthropic/_utils/resource.py +54 -0
  5. aidial_adapter_anthropic/_utils/text.py +4 -0
  6. aidial_adapter_anthropic/adapter/__init__.py +4 -0
  7. aidial_adapter_anthropic/adapter/_base.py +95 -0
  8. aidial_adapter_anthropic/adapter/_claude/adapter.py +549 -0
  9. aidial_adapter_anthropic/adapter/_claude/blocks.py +128 -0
  10. aidial_adapter_anthropic/adapter/_claude/citations.py +63 -0
  11. aidial_adapter_anthropic/adapter/_claude/config.py +39 -0
  12. aidial_adapter_anthropic/adapter/_claude/converters.py +303 -0
  13. aidial_adapter_anthropic/adapter/_claude/params.py +25 -0
  14. aidial_adapter_anthropic/adapter/_claude/state.py +45 -0
  15. aidial_adapter_anthropic/adapter/_claude/tokenizer/__init__.py +10 -0
  16. aidial_adapter_anthropic/adapter/_claude/tokenizer/anthropic.py +57 -0
  17. aidial_adapter_anthropic/adapter/_claude/tokenizer/approximate.py +260 -0
  18. aidial_adapter_anthropic/adapter/_claude/tokenizer/base.py +26 -0
  19. aidial_adapter_anthropic/adapter/_claude/tools.py +98 -0
  20. aidial_adapter_anthropic/adapter/_decorator/base.py +53 -0
  21. aidial_adapter_anthropic/adapter/_decorator/preprocess.py +63 -0
  22. aidial_adapter_anthropic/adapter/_decorator/replicator.py +32 -0
  23. aidial_adapter_anthropic/adapter/_errors.py +71 -0
  24. aidial_adapter_anthropic/adapter/_tokenize.py +12 -0
  25. aidial_adapter_anthropic/adapter/_truncate_prompt.py +168 -0
  26. aidial_adapter_anthropic/adapter/claude.py +17 -0
  27. aidial_adapter_anthropic/dial/_attachments.py +238 -0
  28. aidial_adapter_anthropic/dial/_lazy_stage.py +40 -0
  29. aidial_adapter_anthropic/dial/_message.py +341 -0
  30. aidial_adapter_anthropic/dial/consumer.py +235 -0
  31. aidial_adapter_anthropic/dial/request.py +170 -0
  32. aidial_adapter_anthropic/dial/resource.py +189 -0
  33. aidial_adapter_anthropic/dial/storage.py +138 -0
  34. aidial_adapter_anthropic/dial/token_usage.py +19 -0
  35. aidial_adapter_anthropic/dial/tools.py +180 -0
  36. aidial_adapter_anthropic-0.1.0.dist-info/LICENSE +202 -0
  37. aidial_adapter_anthropic-0.1.0.dist-info/METADATA +121 -0
  38. aidial_adapter_anthropic-0.1.0.dist-info/RECORD +39 -0
  39. aidial_adapter_anthropic-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,63 @@
1
+ from typing import Callable, assert_never
2
+
3
+ from anthropic.types.beta import (
4
+ BetaCitationCharLocation as CitationCharLocation,
5
+ )
6
+ from anthropic.types.beta import (
7
+ BetaCitationContentBlockLocation as CitationContentBlockLocation,
8
+ )
9
+ from anthropic.types.beta import (
10
+ BetaCitationPageLocation as CitationPageLocation,
11
+ )
12
+ from anthropic.types.beta import (
13
+ BetaCitationSearchResultLocation as CitationSearchResultLocation,
14
+ )
15
+ from anthropic.types.beta import (
16
+ BetaCitationsWebSearchResultLocation as CitationsWebSearchResultLocation,
17
+ )
18
+ from anthropic.types.beta import BetaTextCitation as TextCitation
19
+
20
+ from aidial_adapter_anthropic.dial.consumer import Consumer
21
+ from aidial_adapter_anthropic.dial.resource import DialResource
22
+
23
+
24
+ def _add_document_citation(
25
+ consumer: Consumer,
26
+ get_document: Callable[[int], DialResource | None],
27
+ document_index: int,
28
+ ):
29
+ resource = get_document(document_index)
30
+ document = None if resource is None else resource.to_attachment()
31
+
32
+ # NOTE: multiple citations to the same document are merged into one citation
33
+ # until we find a better API to handle citations embedded in text.
34
+ display_index = consumer.add_citation_attachment(
35
+ document_id=document_index, document=document
36
+ )
37
+
38
+ # NOTE: avoid adding citation URLs into the generated content,
39
+ # since such references aren't easily portable (e.g. when a conversion is duplicated).
40
+ consumer.append_content(f"[{display_index}]")
41
+
42
+
43
+ def create_citations(
44
+ consumer: Consumer,
45
+ get_document: Callable[[int], DialResource | None],
46
+ citation: TextCitation,
47
+ ):
48
+ match citation:
49
+ case CitationCharLocation(
50
+ document_index=document_index
51
+ ) | CitationPageLocation(document_index=document_index):
52
+ _add_document_citation(consumer, get_document, document_index)
53
+
54
+ # Custom document aren't supported yet
55
+ case CitationContentBlockLocation():
56
+ pass
57
+ # web search isn't supported yet
58
+ case CitationsWebSearchResultLocation():
59
+ pass
60
+ case CitationSearchResultLocation():
61
+ pass
62
+ case _:
63
+ assert_never(citation)
@@ -0,0 +1,39 @@
1
+ from typing import List, Literal
2
+
3
+ from anthropic.types.anthropic_beta_param import AnthropicBetaParam
4
+ from anthropic.types.beta import BetaThinkingConfigParam as ThinkingConfigParam
5
+ from pydantic import Field
6
+
7
+ from aidial_adapter_anthropic._utils.pydantic import ExtraForbidModel
8
+
9
+
10
+ class ThinkingConfigEnabled(ExtraForbidModel):
11
+ type: Literal["enabled"]
12
+ budget_tokens: int
13
+
14
+ def to_claude(self) -> ThinkingConfigParam:
15
+ return {"type": "enabled", "budget_tokens": self.budget_tokens}
16
+
17
+
18
+ class ThinkingConfigDisabled(ExtraForbidModel):
19
+ type: Literal["disabled"]
20
+
21
+ def to_claude(self) -> ThinkingConfigParam:
22
+ return {"type": "disabled"}
23
+
24
+
25
+ class ClaudeConfiguration(ExtraForbidModel):
26
+ betas: List[AnthropicBetaParam] | None = Field(
27
+ default=None,
28
+ description="List of beta features to enable. Make sure to check if the given feature is supported by the Claude deployment you are using.",
29
+ )
30
+ enable_citations: bool = False
31
+
32
+
33
+ class ClaudeConfigurationWithThinking(ClaudeConfiguration):
34
+ # NOTE: once migrated to Pydantic v2 we can use TypeAdapter over
35
+ # the anthropic's ThinkingConfigParam class directly.
36
+ thinking: ThinkingConfigEnabled | ThinkingConfigDisabled | None = None
37
+
38
+
39
+ Configuration = ClaudeConfiguration | ClaudeConfigurationWithThinking
@@ -0,0 +1,303 @@
1
+ from typing import List, Literal, Optional, Sequence, Set, Tuple, assert_never
2
+
3
+ from aidial_sdk.chat_completion import FinishReason, Tool
4
+ from aidial_sdk.chat_completion import ToolChoice as DialToolChoice
5
+ from anthropic.types.beta import (
6
+ BetaCacheControlEphemeralParam as CacheControlEphemeralParam,
7
+ )
8
+ from anthropic.types.beta import BetaContentBlockParam as ContentBlockParam
9
+ from anthropic.types.beta import BetaMessageParam as MessageParam
10
+ from anthropic.types.beta import BetaStopReason as ClaudeStopReason
11
+ from anthropic.types.beta import BetaTextBlockParam as TextBlockParam
12
+ from anthropic.types.beta import BetaToolChoiceAnyParam as ToolChoiceAnyParam
13
+ from anthropic.types.beta import BetaToolChoiceAutoParam as ToolChoiceAutoParam
14
+ from anthropic.types.beta import BetaToolChoiceNoneParam as ToolChoiceNoneParam
15
+ from anthropic.types.beta import BetaToolChoiceParam as ToolChoice
16
+ from anthropic.types.beta import BetaToolChoiceToolParam as ToolChoiceToolParam
17
+ from anthropic.types.beta import BetaToolParam as ToolParam
18
+ from anthropic.types.beta import BetaUsage as Usage
19
+ from pydantic import BaseModel
20
+
21
+ from aidial_adapter_anthropic._utils.list import ListProjection, group_by
22
+ from aidial_adapter_anthropic.adapter._claude.blocks import (
23
+ create_text_block,
24
+ create_tool_result_block,
25
+ create_tool_use_block,
26
+ )
27
+ from aidial_adapter_anthropic.adapter._claude.config import Configuration
28
+ from aidial_adapter_anthropic.adapter._claude.state import (
29
+ get_message_content_from_state,
30
+ )
31
+ from aidial_adapter_anthropic.adapter._errors import ValidationError
32
+ from aidial_adapter_anthropic.dial._attachments import (
33
+ AttachmentProcessors,
34
+ WithResources,
35
+ )
36
+ from aidial_adapter_anthropic.dial._message import (
37
+ AIRegularMessage,
38
+ AIToolCallMessage,
39
+ BaseMessage,
40
+ HumanRegularMessage,
41
+ HumanToolResultMessage,
42
+ SystemMessage,
43
+ )
44
+ from aidial_adapter_anthropic.dial.token_usage import TokenUsage
45
+ from aidial_adapter_anthropic.dial.tools import ToolsConfig, ToolsMode
46
+
47
+ DialMessage = BaseMessage | HumanToolResultMessage | AIToolCallMessage
48
+
49
+ ClaudeMessage = WithResources[ContentBlockParam]
50
+
51
+ _claude_cache_breakpoint = CacheControlEphemeralParam(type="ephemeral")
52
+
53
+
54
+ def _add_cache_control(
55
+ message: DialMessage, claude_messages: Sequence[ContentBlockParam]
56
+ ) -> None:
57
+ if message.cache_breakpoint is not None:
58
+ for block in reversed(claude_messages):
59
+ if (
60
+ isinstance(block, dict)
61
+ and block["type"] != "thinking"
62
+ and block["type"] != "redacted_thinking"
63
+ ):
64
+ block["cache_control"] = _claude_cache_breakpoint
65
+ break
66
+
67
+
68
+ def _get_claude_message_role(
69
+ dial_message: (
70
+ AIRegularMessage
71
+ | AIToolCallMessage
72
+ | HumanRegularMessage
73
+ | HumanToolResultMessage
74
+ ),
75
+ ) -> Literal["assistant", "user"]:
76
+ match dial_message:
77
+ case AIRegularMessage() | AIToolCallMessage():
78
+ return "assistant"
79
+ case HumanRegularMessage() | HumanToolResultMessage():
80
+ return "user"
81
+ case _:
82
+ assert_never(dial_message)
83
+
84
+
85
+ _Elem = Tuple[WithResources[MessageParam], Set[int]]
86
+
87
+
88
+ def _merge_messages_with_same_role(
89
+ messages: ListProjection[WithResources[MessageParam]],
90
+ ) -> ListProjection[WithResources[MessageParam]]:
91
+
92
+ def _key(message: _Elem) -> str:
93
+ return message[0].payload["role"]
94
+
95
+ def _merge_message_param(
96
+ msg1: MessageParam, msg2: MessageParam
97
+ ) -> MessageParam:
98
+ content1 = msg1["content"]
99
+ content2 = msg2["content"]
100
+
101
+ if isinstance(content1, str):
102
+ content1 = [TextBlockParam(type="text", text=content1)]
103
+
104
+ if isinstance(content2, str):
105
+ content2 = [TextBlockParam(type="text", text=content2)]
106
+
107
+ return MessageParam(
108
+ role=msg1["role"],
109
+ content=list(content1) + list(content2),
110
+ )
111
+
112
+ def _merge(a: _Elem, b: _Elem) -> _Elem:
113
+ (msg1, set1), (msg2, set2) = a, b
114
+ payload = _merge_message_param(msg1.payload, msg2.payload)
115
+ resources = msg1.resources + msg2.resources
116
+ return (WithResources(payload, resources), set1 | set2)
117
+
118
+ return ListProjection(group_by(messages.list, _key, lambda x: x, _merge))
119
+
120
+
121
+ async def _get_claude_blocks(
122
+ handlers: AttachmentProcessors[
123
+ TextBlockParam, ContentBlockParam, Configuration
124
+ ],
125
+ message: (
126
+ HumanRegularMessage
127
+ | AIRegularMessage
128
+ | AIToolCallMessage
129
+ | HumanToolResultMessage
130
+ ),
131
+ message_idx: int,
132
+ ) -> WithResources[Sequence[ContentBlockParam]]:
133
+
134
+ match message:
135
+ case HumanRegularMessage():
136
+ return await handlers.process_attachments(message)
137
+
138
+ case HumanToolResultMessage():
139
+ blocks = [create_tool_result_block(message)]
140
+ return WithResources(payload=blocks)
141
+
142
+ case AIRegularMessage():
143
+ content = await handlers.process_attachments(message)
144
+
145
+ # Take the message content from the state if possible,
146
+ # since it may include certain content blocks that
147
+ # are missing from the DIAL message itself,
148
+ # such as thinking signatures and redacted thinking blocks.
149
+ if state := get_message_content_from_state(message_idx, message):
150
+ content.payload = state
151
+
152
+ return content
153
+
154
+ case AIToolCallMessage():
155
+ blocks = [create_tool_use_block(call) for call in message.calls]
156
+ if text_content := message.content:
157
+ blocks.insert(0, create_text_block(text_content))
158
+
159
+ content = WithResources(payload=blocks)
160
+ if state := get_message_content_from_state(message_idx, message):
161
+ content.payload = state
162
+
163
+ return content
164
+
165
+ case _:
166
+ assert_never(message)
167
+
168
+
169
+ async def to_claude_messages(
170
+ handlers: AttachmentProcessors[
171
+ TextBlockParam, ContentBlockParam, Configuration
172
+ ],
173
+ messages: List[DialMessage],
174
+ ) -> Tuple[List[TextBlockParam], ListProjection[WithResources[MessageParam]]]:
175
+
176
+ idx_offset: int = 0
177
+ system_messages: List[TextBlockParam] = []
178
+
179
+ for message in messages:
180
+ if not isinstance(message, SystemMessage):
181
+ break
182
+
183
+ idx_offset += 1
184
+ sys_content = await handlers.process_system_message(message)
185
+ _add_cache_control(message, sys_content)
186
+
187
+ system_messages.extend(sys_content)
188
+
189
+ claude_messages: ListProjection[WithResources[MessageParam]] = (
190
+ ListProjection()
191
+ )
192
+
193
+ for idx, message in enumerate(messages[idx_offset:], start=idx_offset):
194
+ if isinstance(message, SystemMessage):
195
+ raise ValidationError(
196
+ "System and developer messages are only allowed in the beginning of the conversation."
197
+ )
198
+
199
+ blocks = await _get_claude_blocks(handlers, message, idx)
200
+ _add_cache_control(message, blocks.payload)
201
+
202
+ role = _get_claude_message_role(message)
203
+ claude_message = WithResources(
204
+ payload=MessageParam(role=role, content=blocks.payload),
205
+ resources=blocks.resources,
206
+ )
207
+
208
+ claude_messages.append(claude_message, idx)
209
+
210
+ return system_messages, _merge_messages_with_same_role(claude_messages)
211
+
212
+
213
+ def to_dial_finish_reason(
214
+ finish_reason: Optional[ClaudeStopReason],
215
+ tools_mode: ToolsMode | None,
216
+ ) -> FinishReason:
217
+ if finish_reason is None:
218
+ return FinishReason.STOP
219
+
220
+ match finish_reason:
221
+ case "end_turn":
222
+ return FinishReason.STOP
223
+ case "max_tokens" | "model_context_window_exceeded":
224
+ return FinishReason.LENGTH
225
+ case "stop_sequence" | "pause_turn" | "refusal":
226
+ return FinishReason.STOP
227
+ case "tool_use":
228
+ match tools_mode:
229
+ case ToolsMode.TOOLS:
230
+ return FinishReason.TOOL_CALLS
231
+ case ToolsMode.FUNCTIONS:
232
+ return FinishReason.FUNCTION_CALL
233
+ case None:
234
+ raise ValidationError(
235
+ "A model has called a tool, but no tools were given to the model in the first place."
236
+ )
237
+ case _:
238
+ assert_never(tools_mode)
239
+
240
+ case _:
241
+ assert_never(finish_reason)
242
+
243
+
244
+ def to_dial_usage(usage: Usage) -> TokenUsage:
245
+ read = usage.cache_creation_input_tokens or 0
246
+ write = usage.cache_read_input_tokens or 0
247
+ return TokenUsage(
248
+ completion_tokens=usage.output_tokens,
249
+ prompt_tokens=usage.input_tokens + read + write,
250
+ cache_write_input_tokens=read,
251
+ cache_read_input_tokens=write,
252
+ )
253
+
254
+
255
+ def _to_claude_tool(tool: Tool) -> ToolParam:
256
+ function = tool.function
257
+ tool_param = ToolParam(
258
+ input_schema=function.parameters
259
+ or {"type": "object", "properties": {}},
260
+ name=function.name,
261
+ description=function.description or "",
262
+ )
263
+
264
+ if tool.custom_fields and tool.custom_fields.cache_breakpoint:
265
+ tool_param["cache_control"] = _claude_cache_breakpoint
266
+
267
+ return tool_param
268
+
269
+
270
+ def _to_claude_tool_choice(
271
+ tool_choice: Literal["auto", "none", "required"] | DialToolChoice,
272
+ ) -> ToolChoice:
273
+ # NOTE tool_choice.disable_parallel_tool_use=True option isn't supported
274
+ # by older Claude3 versions, so we limit the number of generated function calls
275
+ # to one in the adapter itself for the functions mode.
276
+
277
+ match tool_choice:
278
+ case DialToolChoice(function=function):
279
+ return ToolChoiceToolParam(type="tool", name=function.name)
280
+ case "required":
281
+ return ToolChoiceAnyParam(type="any")
282
+ case "auto":
283
+ return ToolChoiceAutoParam(type="auto")
284
+ case "none":
285
+ return ToolChoiceNoneParam(type="none")
286
+ case _:
287
+ assert_never(tool_choice)
288
+
289
+
290
+ class ClaudeToolsConfig(BaseModel):
291
+ tools: List[ToolParam]
292
+ tool_choice: ToolChoice
293
+
294
+
295
+ def to_claude_tool_config(
296
+ tools_config: ToolsConfig | None,
297
+ ) -> ClaudeToolsConfig | None:
298
+ if tools_config is None or not tools_config.tools:
299
+ return None
300
+
301
+ tools = [_to_claude_tool(tool) for tool in tools_config.tools]
302
+ tool_choice = _to_claude_tool_choice(tools_config.tool_choice)
303
+ return ClaudeToolsConfig(tools=tools, tool_choice=tool_choice)
@@ -0,0 +1,25 @@
1
+ from typing import List, TypedDict
2
+
3
+ from anthropic import Omit
4
+ from anthropic.types.anthropic_beta_param import AnthropicBetaParam
5
+ from anthropic.types.beta import BetaTextBlockParam as TextBlockParam
6
+ from anthropic.types.beta import BetaThinkingConfigParam as ThinkingConfigParam
7
+ from anthropic.types.beta import BetaToolChoiceParam as ToolChoice
8
+ from anthropic.types.beta import BetaToolParam as ToolParam
9
+
10
+
11
+ class ClaudeParameters(TypedDict):
12
+ """
13
+ Subset of parameters to Anthropic Messages API request:
14
+ https://github.com/anthropics/anthropic-sdk-python/blob/ff83982c44db0920f435916aadb37c3523083079/src/anthropic/resources/messages.py#L1827-L1847
15
+ """
16
+
17
+ max_tokens: int
18
+ stop_sequences: List[str] | Omit
19
+ system: str | List[TextBlockParam] | Omit
20
+ temperature: float | Omit
21
+ top_p: float | Omit
22
+ tools: List[ToolParam] | Omit
23
+ tool_choice: ToolChoice | Omit
24
+ thinking: ThinkingConfigParam | Omit
25
+ betas: List[AnthropicBetaParam] | Omit
@@ -0,0 +1,45 @@
1
+ import logging
2
+ from typing import List
3
+
4
+ import pydantic
5
+ from anthropic.types.beta import BetaContentBlock as ContentBlock
6
+ from anthropic.types.beta import BetaContentBlockParam as ContentBlockParam
7
+ from anthropic.types.beta.parsed_beta_message import (
8
+ ParsedBetaContentBlock as ParsedContentBlock,
9
+ )
10
+ from pydantic import BaseModel
11
+
12
+ from aidial_adapter_anthropic.dial._message import (
13
+ AIRegularMessage,
14
+ AIToolCallMessage,
15
+ )
16
+
17
+ _log = logging.getLogger(__name__)
18
+
19
+
20
+ class MessageState(BaseModel):
21
+ claude_message_content: List[ParsedContentBlock] | List[ContentBlock]
22
+
23
+ def to_dict(self) -> dict:
24
+ return self.dict(
25
+ # FIXME: a hack to exclude the private __json_buf field
26
+ exclude={"claude_message_content": {"__all__": {"__json_buf"}}},
27
+ # Excluding `citations: null`, since they could not be even parsed
28
+ # currently by the Bedrock.
29
+ exclude_none=True,
30
+ )
31
+
32
+
33
+ def get_message_content_from_state(
34
+ idx: int, message: AIRegularMessage | AIToolCallMessage
35
+ ) -> List[ContentBlockParam] | None:
36
+ if (cc := message.custom_content) and (state_dict := cc.state):
37
+ try:
38
+ state = MessageState.parse_obj(state_dict)
39
+ return [block.to_dict() for block in state.claude_message_content] # type: ignore
40
+ except pydantic.ValidationError as e:
41
+ _log.error(
42
+ f"Invalid state at the path 'messages[{idx}].custom_content.state': {e}"
43
+ )
44
+
45
+ return None
@@ -0,0 +1,10 @@
1
+ from .anthropic import AnthropicTokenizer
2
+ from .approximate import ApproximateTokenizer
3
+ from .base import ClaudeTokenizer, create_tokenizer
4
+
5
+ __all__ = [
6
+ "ClaudeTokenizer",
7
+ "create_tokenizer",
8
+ "AnthropicTokenizer",
9
+ "ApproximateTokenizer",
10
+ ]
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+ from aidial_sdk.exceptions import InternalServerError
5
+ from anthropic import (
6
+ AsyncAnthropic,
7
+ AsyncAnthropicBedrock,
8
+ AsyncAnthropicFoundry,
9
+ AsyncAnthropicVertex,
10
+ )
11
+ from anthropic._resource import AsyncAPIResource
12
+ from anthropic.resources.beta import AsyncMessages as FirstPartyAsyncMessagesAPI
13
+ from anthropic.types.beta import BetaMessageParam as ClaudeMessageParam
14
+
15
+ from aidial_adapter_anthropic.adapter._claude.params import ClaudeParameters
16
+
17
+ AnthropicClient = (
18
+ AsyncAnthropic
19
+ | AsyncAnthropicBedrock
20
+ | AsyncAnthropicVertex
21
+ | AsyncAnthropicFoundry
22
+ )
23
+
24
+
25
+ # Beta AsyncMessages doesn't provide the 'count_tokens' method,
26
+ # so we enabled it via the adapter.
27
+ class _AsyncMessagesAdapter(AsyncAPIResource):
28
+ count_tokens = FirstPartyAsyncMessagesAPI.count_tokens
29
+
30
+ def __init__(self, resource: AsyncAPIResource):
31
+ super().__init__(resource._client)
32
+
33
+
34
+ @dataclass
35
+ class AnthropicTokenizer:
36
+ deployment: str
37
+ client: AnthropicClient
38
+
39
+ def tokenize_text(self, text: str) -> int:
40
+ raise InternalServerError(
41
+ "Tokenization of strings is not supported by Anthropic API"
42
+ )
43
+
44
+ async def tokenize(
45
+ self, params: ClaudeParameters, messages: List[ClaudeMessageParam]
46
+ ) -> int:
47
+ return (
48
+ await _AsyncMessagesAdapter(self.client.beta.messages).count_tokens(
49
+ model=self.deployment,
50
+ messages=messages,
51
+ system=params["system"],
52
+ thinking=params["thinking"],
53
+ tools=params["tools"],
54
+ tool_choice=params["tool_choice"],
55
+ betas=params["betas"],
56
+ )
57
+ ).input_tokens