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.
- aidial_adapter_anthropic/_utils/json.py +116 -0
- aidial_adapter_anthropic/_utils/list.py +84 -0
- aidial_adapter_anthropic/_utils/pydantic.py +6 -0
- aidial_adapter_anthropic/_utils/resource.py +54 -0
- aidial_adapter_anthropic/_utils/text.py +4 -0
- aidial_adapter_anthropic/adapter/__init__.py +4 -0
- aidial_adapter_anthropic/adapter/_base.py +95 -0
- aidial_adapter_anthropic/adapter/_claude/adapter.py +549 -0
- aidial_adapter_anthropic/adapter/_claude/blocks.py +128 -0
- aidial_adapter_anthropic/adapter/_claude/citations.py +63 -0
- aidial_adapter_anthropic/adapter/_claude/config.py +39 -0
- aidial_adapter_anthropic/adapter/_claude/converters.py +303 -0
- aidial_adapter_anthropic/adapter/_claude/params.py +25 -0
- aidial_adapter_anthropic/adapter/_claude/state.py +45 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/__init__.py +10 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/anthropic.py +57 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/approximate.py +260 -0
- aidial_adapter_anthropic/adapter/_claude/tokenizer/base.py +26 -0
- aidial_adapter_anthropic/adapter/_claude/tools.py +98 -0
- aidial_adapter_anthropic/adapter/_decorator/base.py +53 -0
- aidial_adapter_anthropic/adapter/_decorator/preprocess.py +63 -0
- aidial_adapter_anthropic/adapter/_decorator/replicator.py +32 -0
- aidial_adapter_anthropic/adapter/_errors.py +71 -0
- aidial_adapter_anthropic/adapter/_tokenize.py +12 -0
- aidial_adapter_anthropic/adapter/_truncate_prompt.py +168 -0
- aidial_adapter_anthropic/adapter/claude.py +17 -0
- aidial_adapter_anthropic/dial/_attachments.py +238 -0
- aidial_adapter_anthropic/dial/_lazy_stage.py +40 -0
- aidial_adapter_anthropic/dial/_message.py +341 -0
- aidial_adapter_anthropic/dial/consumer.py +235 -0
- aidial_adapter_anthropic/dial/request.py +170 -0
- aidial_adapter_anthropic/dial/resource.py +189 -0
- aidial_adapter_anthropic/dial/storage.py +138 -0
- aidial_adapter_anthropic/dial/token_usage.py +19 -0
- aidial_adapter_anthropic/dial/tools.py +180 -0
- aidial_adapter_anthropic-0.1.0.dist-info/LICENSE +202 -0
- aidial_adapter_anthropic-0.1.0.dist-info/METADATA +121 -0
- aidial_adapter_anthropic-0.1.0.dist-info/RECORD +39 -0
- 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,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
|