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,549 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from functools import cached_property
4
+ from logging import DEBUG
5
+ from typing import List, Optional, Tuple, Type, assert_never
6
+
7
+ from aidial_sdk.chat_completion import Message as DialMessage
8
+ from anthropic import (
9
+ AsyncAnthropic,
10
+ AsyncAnthropicBedrock,
11
+ AsyncAnthropicFoundry,
12
+ AsyncAnthropicVertex,
13
+ Omit,
14
+ omit,
15
+ )
16
+ from anthropic._resource import AsyncAPIResource
17
+ from anthropic.lib.streaming import BetaInputJsonEvent as InputJsonEvent
18
+ from anthropic.lib.streaming import BetaTextEvent as TextEvent
19
+ from anthropic.lib.streaming import (
20
+ ParsedBetaContentBlockStopEvent as ParsedContentBlockStopEvent,
21
+ )
22
+ from anthropic.lib.streaming._beta_types import (
23
+ BetaCitationEvent as CitationEvent,
24
+ )
25
+ from anthropic.lib.streaming._beta_types import (
26
+ BetaSignatureEvent as SignatureEvent,
27
+ )
28
+ from anthropic.lib.streaming._beta_types import (
29
+ BetaThinkingEvent as ThinkingEvent,
30
+ )
31
+ from anthropic.lib.streaming._beta_types import (
32
+ ParsedBetaMessageStopEvent as ParsedMessageStopEvent,
33
+ )
34
+ from anthropic.resources.beta import AsyncMessages as FirstPartyAsyncMessagesAPI
35
+ from anthropic.types.beta import (
36
+ BetaBashCodeExecutionToolResultBlock as BashCodeExecutionToolResultBlock,
37
+ )
38
+ from anthropic.types.beta import (
39
+ BetaCodeExecutionToolResultBlock as CodeExecutionToolResultBlock,
40
+ )
41
+ from anthropic.types.beta import (
42
+ BetaContainerUploadBlock as ContainerUploadBlock,
43
+ )
44
+ from anthropic.types.beta import BetaMCPToolResultBlock as MCPToolResultBlock
45
+ from anthropic.types.beta import BetaMCPToolUseBlock as MCPToolUseBlock
46
+ from anthropic.types.beta import BetaMessage as ClaudeResponseMessage
47
+ from anthropic.types.beta import BetaMessageParam as ClaudeMessageParam
48
+ from anthropic.types.beta import (
49
+ BetaRawContentBlockDeltaEvent as ContentBlockDeltaEvent,
50
+ )
51
+ from anthropic.types.beta import (
52
+ BetaRawContentBlockStartEvent as ContentBlockStartEvent,
53
+ )
54
+ from anthropic.types.beta import BetaRawMessageDeltaEvent as MessageDeltaEvent
55
+ from anthropic.types.beta import BetaRawMessageStartEvent as MessageStartEvent
56
+ from anthropic.types.beta import (
57
+ BetaRedactedThinkingBlock as RedactedThinkingBlock,
58
+ )
59
+ from anthropic.types.beta import BetaServerToolUseBlock as ServerToolUseBlock
60
+ from anthropic.types.beta import BetaTextBlock as TextBlock
61
+ from anthropic.types.beta import (
62
+ BetaTextEditorCodeExecutionToolResultBlock as TextEditorCodeExecutionToolResultBlock,
63
+ )
64
+ from anthropic.types.beta import BetaThinkingBlock as ThinkingBlock
65
+ from anthropic.types.beta import BetaThinkingConfigParam as ThinkingConfigParam
66
+ from anthropic.types.beta import (
67
+ BetaToolSearchToolResultBlock as ToolSearchToolResultBlock,
68
+ )
69
+ from anthropic.types.beta import BetaToolUseBlock as ToolUseBlock
70
+ from anthropic.types.beta import (
71
+ BetaWebFetchToolResultBlock as WebFetchToolResultBlock,
72
+ )
73
+ from anthropic.types.beta import (
74
+ BetaWebSearchToolResultBlock as WebSearchToolResultBlock,
75
+ )
76
+ from anthropic.types.beta.parsed_beta_message import (
77
+ ParsedBetaTextBlock as ParsedTextBlock,
78
+ )
79
+
80
+ from aidial_adapter_anthropic._utils.json import json_dumps_short
81
+ from aidial_adapter_anthropic._utils.list import ListProjection
82
+ from aidial_adapter_anthropic.adapter._base import (
83
+ ChatCompletionAdapter,
84
+ default_preprocess_messages,
85
+ keep_last,
86
+ turn_based_partitioner,
87
+ )
88
+ from aidial_adapter_anthropic.adapter._claude.blocks import (
89
+ IMAGE_ATTACHMENT_PROCESSOR,
90
+ PDF_ATTACHMENT_PROCESSOR,
91
+ PLAIN_TEXT_ATTACHMENT_PROCESSOR,
92
+ create_text_block,
93
+ )
94
+ from aidial_adapter_anthropic.adapter._claude.citations import create_citations
95
+ from aidial_adapter_anthropic.adapter._claude.config import (
96
+ ClaudeConfiguration,
97
+ ClaudeConfigurationWithThinking,
98
+ )
99
+ from aidial_adapter_anthropic.adapter._claude.converters import (
100
+ to_claude_messages,
101
+ to_claude_tool_config,
102
+ to_dial_finish_reason,
103
+ to_dial_usage,
104
+ )
105
+ from aidial_adapter_anthropic.adapter._claude.params import ClaudeParameters
106
+ from aidial_adapter_anthropic.adapter._claude.state import MessageState
107
+ from aidial_adapter_anthropic.adapter._claude.tokenizer import (
108
+ AnthropicTokenizer,
109
+ ClaudeTokenizer,
110
+ create_tokenizer,
111
+ )
112
+ from aidial_adapter_anthropic.adapter._claude.tools import (
113
+ function_to_tool_messages,
114
+ process_tools_block,
115
+ )
116
+ from aidial_adapter_anthropic.adapter._decorator.base import compose_decorators
117
+ from aidial_adapter_anthropic.adapter._decorator.preprocess import (
118
+ preprocess_messages_decorator,
119
+ )
120
+ from aidial_adapter_anthropic.adapter._decorator.replicator import (
121
+ replicator_decorator,
122
+ )
123
+ from aidial_adapter_anthropic.adapter._errors import ValidationError
124
+ from aidial_adapter_anthropic.adapter._truncate_prompt import (
125
+ DiscardedMessages,
126
+ truncate_prompt,
127
+ )
128
+ from aidial_adapter_anthropic.dial._attachments import (
129
+ AttachmentProcessors,
130
+ WithResources,
131
+ )
132
+ from aidial_adapter_anthropic.dial._message import parse_dial_message
133
+ from aidial_adapter_anthropic.dial.consumer import Consumer, ToolUseMessage
134
+ from aidial_adapter_anthropic.dial.request import (
135
+ ModelParameters as DialParameters,
136
+ )
137
+ from aidial_adapter_anthropic.dial.resource import DialResource
138
+ from aidial_adapter_anthropic.dial.storage import FileStorage
139
+ from aidial_adapter_anthropic.dial.tools import ToolsMode
140
+
141
+ _log = logging.getLogger(__name__)
142
+
143
+
144
+ # Beta AsyncMessages doesn't provide the 'stream' method,
145
+ # so we enabled it via the adapter.
146
+ class _AsyncMessagesAdapter(AsyncAPIResource):
147
+ create = FirstPartyAsyncMessagesAPI.create
148
+ stream = FirstPartyAsyncMessagesAPI.stream
149
+
150
+ def __init__(self, resource: AsyncAPIResource):
151
+ super().__init__(resource._client)
152
+
153
+
154
+ # NOTE: it's not pydantic BaseModel, because
155
+ # anthropic.types.MessageParam.content is of Iterable type and
156
+ # pydantic automatically converts lists into
157
+ # list iterators following the type.
158
+ # See https://github.com/anthropics/anthropic-sdk-python/issues/656 for details.
159
+ @dataclass
160
+ class ClaudeRequest:
161
+ params: ClaudeParameters
162
+ messages: ListProjection[WithResources[ClaudeMessageParam]]
163
+
164
+ @property
165
+ def claude_messages(self) -> List[ClaudeMessageParam]:
166
+ return [res.payload for res in self.messages.raw_list]
167
+
168
+ @cached_property
169
+ def resources(self) -> List[DialResource]:
170
+ return [r for res in self.messages.raw_list for r in res.resources]
171
+
172
+ def get_resource(self, index: int) -> DialResource | None:
173
+ if 0 <= index < len(self.resources):
174
+ return self.resources[index]
175
+ return None
176
+
177
+
178
+ AnthropicClient = (
179
+ AsyncAnthropic
180
+ | AsyncAnthropicBedrock
181
+ | AsyncAnthropicVertex
182
+ | AsyncAnthropicFoundry
183
+ )
184
+
185
+
186
+ async def create_adapter(
187
+ *,
188
+ deployment: str,
189
+ storage: FileStorage | None,
190
+ client: AnthropicClient,
191
+ default_max_tokens: int,
192
+ supports_thinking: bool,
193
+ supports_documents: bool,
194
+ custom_tokenizer: ClaudeTokenizer | None = None,
195
+ ) -> ChatCompletionAdapter:
196
+ tokenizer = custom_tokenizer or AnthropicTokenizer(deployment, client)
197
+ model = Adapter(
198
+ deployment=deployment,
199
+ storage=storage,
200
+ client=client,
201
+ tokenizer=tokenizer,
202
+ default_max_tokens=default_max_tokens,
203
+ supports_documents=supports_documents,
204
+ supports_thinking=supports_thinking,
205
+ )
206
+
207
+ return compose_decorators(
208
+ preprocess_messages_decorator(default_preprocess_messages),
209
+ replicator_decorator(),
210
+ )(model)
211
+
212
+
213
+ class Adapter(ChatCompletionAdapter):
214
+ deployment: str
215
+ storage: Optional[FileStorage]
216
+ client: AnthropicClient
217
+
218
+ tokenizer: ClaudeTokenizer
219
+ default_max_tokens: int
220
+ supports_thinking: bool
221
+ supports_documents: bool
222
+
223
+ async def configuration(self) -> Type[ClaudeConfiguration]:
224
+ return ClaudeConfigurationWithThinking
225
+
226
+ @property
227
+ def attachment_processors(self) -> AttachmentProcessors:
228
+ # Document support: https://docs.anthropic.com/en/docs/build-with-claude/pdf-support#supported-platforms-and-models
229
+ document_processors = (
230
+ [PDF_ATTACHMENT_PROCESSOR, PLAIN_TEXT_ATTACHMENT_PROCESSOR]
231
+ if self.supports_documents
232
+ else []
233
+ )
234
+ return AttachmentProcessors(
235
+ text_handler=create_text_block,
236
+ attachment_processors=(
237
+ [IMAGE_ATTACHMENT_PROCESSOR] + document_processors
238
+ ),
239
+ file_storage=self.storage,
240
+ )
241
+
242
+ async def _prepare_claude_request(
243
+ self, params: DialParameters, messages: List[DialMessage]
244
+ ) -> ClaudeRequest:
245
+ configuration = params.parse_configuration(await self.configuration())
246
+
247
+ if len(messages) == 0:
248
+ raise ValidationError("List of messages must not be empty")
249
+
250
+ tools_config = to_claude_tool_config(params.tool_config)
251
+
252
+ parsed_messages = [
253
+ function_to_tool_messages(parse_dial_message(m)) for m in messages
254
+ ]
255
+
256
+ system_prompt, claude_messages = await to_claude_messages(
257
+ self.attachment_processors, parsed_messages
258
+ )
259
+
260
+ thinking: ThinkingConfigParam | Omit = omit
261
+ if (
262
+ isinstance(configuration, ClaudeConfigurationWithThinking)
263
+ and configuration.thinking is not None
264
+ ):
265
+ thinking = configuration.thinking.to_claude()
266
+
267
+ temperature = omit
268
+ if params.temperature is not None:
269
+ # Mapping OpenAI temp [0,2] range to Anthropic temp [0,1] range
270
+ temperature = params.temperature / 2
271
+
272
+ if not isinstance(thinking, Omit) and thinking["type"] == "enabled":
273
+ # Thinking isn’t compatible with temperature, top_p, or top_k
274
+ # modifications as well as forced tool use.
275
+ temperature = omit
276
+
277
+ max_tokens = params.max_tokens or self.default_max_tokens
278
+
279
+ claude_params = ClaudeParameters(
280
+ max_tokens=max_tokens,
281
+ stop_sequences=params.stop,
282
+ system=system_prompt or omit,
283
+ temperature=temperature,
284
+ top_p=params.top_p or omit,
285
+ tools=(tools_config and tools_config.tools) or omit,
286
+ tool_choice=(tools_config and tools_config.tool_choice) or omit,
287
+ thinking=thinking,
288
+ betas=configuration.betas or omit,
289
+ )
290
+
291
+ return ClaudeRequest(params=claude_params, messages=claude_messages)
292
+
293
+ async def _compute_discarded_messages(
294
+ self, request: ClaudeRequest, max_prompt_tokens: int | None
295
+ ) -> Tuple[DiscardedMessages | None, ClaudeRequest]:
296
+ if max_prompt_tokens is None:
297
+ return None, request
298
+
299
+ discarded_messages, messages = await truncate_prompt(
300
+ messages=request.messages.list,
301
+ tokenizer=create_tokenizer(self.tokenizer, request.params),
302
+ keep_message=keep_last,
303
+ partitioner=turn_based_partitioner,
304
+ model_limit=None,
305
+ user_limit=max_prompt_tokens,
306
+ )
307
+
308
+ claude_messages = ListProjection(messages)
309
+
310
+ discarded_messages = list(
311
+ request.messages.to_original_indices(discarded_messages)
312
+ )
313
+
314
+ return discarded_messages, ClaudeRequest(
315
+ params=request.params,
316
+ messages=claude_messages,
317
+ )
318
+
319
+ async def chat(
320
+ self,
321
+ consumer: Consumer,
322
+ params: DialParameters,
323
+ messages: List[DialMessage],
324
+ ):
325
+ request = await self._prepare_claude_request(params, messages)
326
+
327
+ discarded_messages, request = await self._compute_discarded_messages(
328
+ request, params.max_prompt_tokens
329
+ )
330
+
331
+ if params.stream:
332
+ await self.invoke_streaming(
333
+ consumer,
334
+ params.tools_mode,
335
+ request,
336
+ discarded_messages,
337
+ )
338
+ else:
339
+ await self.invoke_non_streaming(
340
+ consumer,
341
+ params.tools_mode,
342
+ request,
343
+ discarded_messages,
344
+ )
345
+
346
+ async def count_prompt_tokens(
347
+ self, params: DialParameters, messages: List[DialMessage]
348
+ ) -> int:
349
+ request = await self._prepare_claude_request(params, messages)
350
+ tokenizer = create_tokenizer(self.tokenizer, request.params)
351
+ return await tokenizer(request.messages.list)
352
+
353
+ async def count_completion_tokens(self, string: str) -> int:
354
+ return self.tokenizer.tokenize_text(string)
355
+
356
+ async def compute_discarded_messages(
357
+ self, params: DialParameters, messages: List[DialMessage]
358
+ ) -> DiscardedMessages | None:
359
+ request = await self._prepare_claude_request(params, messages)
360
+ discarded_messages, _request = await self._compute_discarded_messages(
361
+ request, params.max_prompt_tokens
362
+ )
363
+ return discarded_messages
364
+
365
+ async def invoke_streaming(
366
+ self,
367
+ consumer: Consumer,
368
+ tools_mode: ToolsMode | None,
369
+ request: ClaudeRequest,
370
+ discarded_messages: DiscardedMessages | None,
371
+ ):
372
+ if _log.isEnabledFor(DEBUG):
373
+ msg = json_dumps_short(
374
+ {"deployment": self.deployment, "request": request}
375
+ )
376
+ _log.debug(f"request: {msg}")
377
+
378
+ async with (
379
+ _AsyncMessagesAdapter(self.client.beta.messages).stream(
380
+ messages=request.claude_messages,
381
+ model=self.deployment,
382
+ **request.params,
383
+ ) as stream,
384
+ consumer.create_stage("Thinking") as thinking_stage,
385
+ ):
386
+ stop_reason = None
387
+ tool: ToolUseMessage | None = None
388
+
389
+ async for event in stream:
390
+ if _log.isEnabledFor(DEBUG):
391
+ _log.debug(f"response event: {json_dumps_short(event)}")
392
+
393
+ match event:
394
+ case MessageStartEvent():
395
+ pass
396
+ case TextEvent(text=text):
397
+ consumer.append_content(text)
398
+
399
+ case ThinkingEvent(thinking=thinking):
400
+ thinking_stage.append_content(thinking)
401
+
402
+ case SignatureEvent() | MessageDeltaEvent():
403
+ pass
404
+
405
+ case ContentBlockStartEvent(content_block=content_block):
406
+ if isinstance(content_block, ToolUseBlock):
407
+ tool = process_tools_block(
408
+ consumer,
409
+ content_block,
410
+ tools_mode,
411
+ streaming=True,
412
+ )
413
+
414
+ case InputJsonEvent(partial_json=partial_json):
415
+ if tool:
416
+ tool.append_arguments(partial_json)
417
+ else:
418
+ _log.warning(
419
+ "The model generated tool input before start using it"
420
+ )
421
+
422
+ case ParsedContentBlockStopEvent(
423
+ content_block=content_block
424
+ ):
425
+ match content_block:
426
+ case TextBlock(citations=citations):
427
+ # The text content is already handled in TextEvent handler.
428
+ for citation in citations or []:
429
+ create_citations(
430
+ consumer, request.get_resource, citation
431
+ )
432
+ case ToolUseBlock():
433
+ # Tool Use is processed in ContentBlockStartEvent and InputJsonEvent handlers
434
+ pass
435
+ case ThinkingBlock() | RedactedThinkingBlock():
436
+ # Thinking is processed in ThinkingEvent
437
+ pass
438
+ case (
439
+ ServerToolUseBlock()
440
+ | WebSearchToolResultBlock()
441
+ | CodeExecutionToolResultBlock()
442
+ | MCPToolUseBlock()
443
+ | MCPToolResultBlock()
444
+ | ContainerUploadBlock()
445
+ | BashCodeExecutionToolResultBlock()
446
+ | TextEditorCodeExecutionToolResultBlock()
447
+ | WebFetchToolResultBlock()
448
+ | ParsedTextBlock()
449
+ | BashCodeExecutionToolResultBlock()
450
+ | TextEditorCodeExecutionToolResultBlock()
451
+ ):
452
+ _log.error(
453
+ f"Content block of type {content_block.type} isn't supported"
454
+ )
455
+ case _:
456
+ assert_never(content_block)
457
+
458
+ case ParsedMessageStopEvent(message=message):
459
+ consumer.add_usage(to_dial_usage(message.usage))
460
+ stop_reason = message.stop_reason
461
+ if self.supports_thinking:
462
+ consumer.choice.set_state(
463
+ MessageState(
464
+ claude_message_content=message.content
465
+ ).to_dict()
466
+ )
467
+
468
+ case ContentBlockDeltaEvent() | CitationEvent():
469
+ pass
470
+
471
+ case _:
472
+ assert_never(event)
473
+
474
+ consumer.close_content(
475
+ to_dial_finish_reason(stop_reason, tools_mode)
476
+ )
477
+
478
+ consumer.set_discarded_messages(discarded_messages)
479
+
480
+ async def invoke_non_streaming(
481
+ self,
482
+ consumer: Consumer,
483
+ tools_mode: ToolsMode | None,
484
+ request: ClaudeRequest,
485
+ discarded_messages: DiscardedMessages | None,
486
+ ):
487
+
488
+ if _log.isEnabledFor(DEBUG):
489
+ msg = json_dumps_short(
490
+ {"deployment": self.deployment, "request": request}
491
+ )
492
+ _log.debug(f"request: {msg}")
493
+
494
+ message: ClaudeResponseMessage = await self.client.beta.messages.create(
495
+ messages=request.claude_messages,
496
+ model=self.deployment,
497
+ **request.params,
498
+ stream=False,
499
+ )
500
+
501
+ if _log.isEnabledFor(DEBUG):
502
+ _log.debug(f"response: {json_dumps_short(message)}")
503
+
504
+ for content in message.content:
505
+ match content:
506
+ case TextBlock(text=text, citations=citations):
507
+ consumer.append_content(text)
508
+ for citation in citations or []:
509
+ create_citations(
510
+ consumer, request.get_resource, citation
511
+ )
512
+ case ToolUseBlock():
513
+ process_tools_block(
514
+ consumer, content, tools_mode, streaming=False
515
+ )
516
+ case ThinkingBlock(thinking=thinking):
517
+ with consumer.create_stage("Thinking") as stage:
518
+ stage.append_content(thinking)
519
+ case RedactedThinkingBlock():
520
+ pass
521
+ case (
522
+ ServerToolUseBlock()
523
+ | WebSearchToolResultBlock()
524
+ | CodeExecutionToolResultBlock()
525
+ | MCPToolUseBlock()
526
+ | MCPToolResultBlock()
527
+ | ContainerUploadBlock()
528
+ | BashCodeExecutionToolResultBlock()
529
+ | TextEditorCodeExecutionToolResultBlock()
530
+ | WebFetchToolResultBlock()
531
+ | ToolSearchToolResultBlock()
532
+ ):
533
+ _log.error(
534
+ f"Content block of type {content.type} isn't supported"
535
+ )
536
+ case _:
537
+ assert_never(content)
538
+
539
+ if self.supports_thinking:
540
+ consumer.choice.set_state(
541
+ MessageState(claude_message_content=message.content).to_dict()
542
+ )
543
+
544
+ consumer.close_content(
545
+ to_dial_finish_reason(message.stop_reason, tools_mode)
546
+ )
547
+
548
+ consumer.add_usage(to_dial_usage(message.usage))
549
+ consumer.set_discarded_messages(discarded_messages)
@@ -0,0 +1,128 @@
1
+ import json
2
+
3
+ from aidial_sdk.chat_completion import ToolCall
4
+ from anthropic.types.beta import (
5
+ BetaBase64PDFSourceParam as Base64PDFSourceParam,
6
+ )
7
+ from anthropic.types.beta import (
8
+ BetaCitationsConfigParam as CitationsConfigParam,
9
+ )
10
+ from anthropic.types.beta import BetaContentBlockParam as ContentBlockParam
11
+ from anthropic.types.beta import BetaImageBlockParam as ImageBlockParam
12
+ from anthropic.types.beta import (
13
+ BetaPlainTextSourceParam as PlainTextSourceParam,
14
+ )
15
+ from anthropic.types.beta import (
16
+ BetaRequestDocumentBlockParam as RequestDocumentBlockParam,
17
+ )
18
+ from anthropic.types.beta import BetaTextBlockParam as TextBlockParam
19
+ from anthropic.types.beta import (
20
+ BetaToolResultBlockParam as ToolResultBlockParam,
21
+ )
22
+ from anthropic.types.beta import BetaToolUseBlockParam as ToolUseBlockParam
23
+ from anthropic.types.beta.beta_base64_image_source_param import (
24
+ BetaBase64ImageSourceParam as Base64ImageSourceParam,
25
+ )
26
+
27
+ from aidial_adapter_anthropic._utils.resource import Resource
28
+ from aidial_adapter_anthropic.dial._attachments import AttachmentProcessor
29
+ from aidial_adapter_anthropic.dial._message import HumanToolResultMessage
30
+
31
+
32
+ def create_text_block(text: str) -> TextBlockParam:
33
+ return TextBlockParam(text=text, type="text")
34
+
35
+
36
+ def create_image_block(resource: Resource) -> ImageBlockParam:
37
+ return ImageBlockParam(
38
+ source=Base64ImageSourceParam(
39
+ data=resource.data_base64,
40
+ media_type=resource.type, # type: ignore
41
+ type="base64",
42
+ ),
43
+ type="image",
44
+ )
45
+
46
+
47
+ def create_text_document_block(
48
+ resource: Resource, *, enable_citations: bool = False
49
+ ) -> RequestDocumentBlockParam:
50
+ return RequestDocumentBlockParam(
51
+ source=PlainTextSourceParam(
52
+ data=resource.data.decode("utf-8"),
53
+ media_type="text/plain",
54
+ type="text",
55
+ ),
56
+ type="document",
57
+ citations=CitationsConfigParam(enabled=enable_citations),
58
+ )
59
+
60
+
61
+ def create_pdf_document_block(
62
+ resource: Resource, *, enable_citations: bool = False
63
+ ) -> RequestDocumentBlockParam:
64
+ return RequestDocumentBlockParam(
65
+ source=Base64PDFSourceParam(
66
+ data=resource.data_base64,
67
+ media_type="application/pdf",
68
+ type="base64",
69
+ ),
70
+ type="document",
71
+ citations=CitationsConfigParam(enabled=enable_citations),
72
+ )
73
+
74
+
75
+ def create_tool_use_block(call: ToolCall) -> ContentBlockParam:
76
+ return ToolUseBlockParam(
77
+ id=call.id,
78
+ name=call.function.name,
79
+ input=json.loads(call.function.arguments),
80
+ type="tool_use",
81
+ )
82
+
83
+
84
+ def create_tool_result_block(
85
+ message: HumanToolResultMessage,
86
+ ) -> ToolResultBlockParam:
87
+ return ToolResultBlockParam(
88
+ tool_use_id=message.id,
89
+ type="tool_result",
90
+ content=[create_text_block(message.content)],
91
+ )
92
+
93
+
94
+ IMAGE_ATTACHMENT_PROCESSOR = AttachmentProcessor(
95
+ supported_types={
96
+ "image/png": {"png"},
97
+ "image/jpeg": {"jpeg", "jpg"},
98
+ "image/gif": {"gif"},
99
+ "image/webp": {"webp"},
100
+ },
101
+ handler=create_image_block,
102
+ )
103
+
104
+ PDF_ATTACHMENT_PROCESSOR = AttachmentProcessor(
105
+ supported_types={"application/pdf": {"pdf"}},
106
+ handler=create_pdf_document_block,
107
+ )
108
+
109
+ PLAIN_TEXT_ATTACHMENT_PROCESSOR = AttachmentProcessor(
110
+ supported_types={
111
+ "text/plain": {"txt"},
112
+ "text/html": {"html", "htm"},
113
+ "text/css": {"css"},
114
+ "text/javascript": {"js"},
115
+ "application/x-javascript": {"js"},
116
+ "text/x-typescript": {"ts"},
117
+ "application/x-typescript": {"ts"},
118
+ "text/csv": {"csv"},
119
+ "text/markdown": {"md"},
120
+ "text/x-python": {"py"},
121
+ "application/x-python-code": {"py"},
122
+ "application/json": {"json"},
123
+ "text/xml": {"xml"},
124
+ "application/rtf": {"rtf"},
125
+ "text/rtf": {"rtf"},
126
+ },
127
+ handler=create_text_document_block,
128
+ )