8000 models - anthropic (#12) · Unshure/sdk-python@b213e34 · GitHub
[go: up one dir, main page]

Skip to content

Commit b213e34

Browse files
yonib05pgrayyrlatntjr
authored
models - anthropic (strands-agents#12)
Co-authored-by: Patrick Gray <pgrayy@amazon.com> Co-authored-by: Jason Kim <jkim@anthropic.com>
1 parent b3890bb commit b213e34

File tree

4 files changed

+1079
-2
lines changed

4 files changed

+1079
-2
lines changed

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ Documentation = "https://strandsagents.com"
4747
packages = ["src/strands"]
4848

4949
[project.optional-dependencies]
50+
anthropic = [
51+
"anthropic>=0.21.0,<1.0.0",
52+
]
5053
dev = [
5154
"commitizen>=4.4.0,<5.0.0",
5255
"hatch>=1.0.0,<2.0.0",
@@ -71,7 +74,7 @@ ollama = [
7174
]
7275

7376
[tool.hatch.envs.hatch-static-analysis]
74-
features = ["litellm", "ollama"]
77+
features = ["anthropic", "litellm", "ollama"]
7578
dependencies = [
7679
"mypy>=1.15.0,<2.0.0",
7780
"ruff>=0.11.6,<0.12.0",
@@ -94,7 +97,7 @@ lint-fix = [
9497
]
9598

9699
[tool.hatch.envs.hatch-test]
97-
features = ["litellm", "ollama"]
100+
features = ["anthropic", "litellm", "ollama"]
98101
extra-dependencies = [
99102
"moto>=5.1.0,<6.0.0",
100103
"pytest>=8.0.0,<9.0.0",

src/strands/models/anthropic.py

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
"""Anthropic Claude model provider.
2+
3+
- Docs: https://docs.anthropic.com/claude/reference/getting-started-with-the-api
4+
"""
5+
6+
import base64
7+
import json
8+
import logging
9+
import mimetypes
10+
from typing import Any, Iterable, Optional, TypedDict, cast
11+
12+
import anthropic
13+
from typing_extensions import Required, Unpack, override
14+
15+
from ..types.content import ContentBlock, Messages
16+
from ..types.exceptions import ContextWindowOverflowException, ModelThrottledException
17+
from ..types.models import Model
18+
from ..types.streaming import StreamEvent
19+
from ..types.tools import ToolSpec
20+
21+
logger = logging.getLogger(__name__)
22+
23+
24+
class AnthropicModel(Model):
25+
"""Anthropic model provider implementation."""
26+
27+
EVENT_TYPES = {
28+
"message_start",
29+
"content_block_start",
30+
"content_block_delta",
31+
"content_block_stop",
32+
"message_stop",
33+
}
34+
35+
OVERFLOW_MESSAGES = {
36+
"input is too long",
37+
"input length exceeds context window",
38+
"input and output tokens exceed your context limit",
39+
}
40+
41+
class AnthropicConfig(TypedDict, total=False):
42+
"""Configuration options for Anthropic models.
43+
44+
Attributes:
45+
max_tokens: Maximum number of tokens to generate.
46+
model_id: Calude model ID (e.g., "claude-3-7-sonnet-latest").
47+
For a complete list of supported models, see
48+
https://docs.anthropic.com/en/docs/about-claude/models/all-models.
49+
params: Additional model parameters (e.g., temperature).
50+
For a complete list of supported parameters, see https://docs.anthropic.com/en/api/messages.
51+
"""
52+
53+
max_tokens: Required[str]
54+
model_id: Required[str]
55+
params: Optional[dict[str, Any]]
56+
57+
def __init__(self, *, client_args: Optional[dict[str, Any]] = None, **model_config: Unpack[AnthropicConfig]):
58+
"""Initialize provider instance.
59+
60+
Args:
61+
client_args: Arguments for the underlying Anthropic client (e.g., api_key).
62+
For a complete list of supported arguments, see https://docs.anthropic.com/en/api/client-sdks.
63+
**model_config: Configuration options for the Anthropic model.
64+
"""
65+
self.config = AnthropicModel.AnthropicConfig(**model_config)
66+
67+
logger.debug("config=<%s> | initializing", self.config)
68+
69+
client_args = client_args or {}
70+
self.client = anthropic.Anthropic(**client_args)
71+
72+
@override
73+
def update_config(self, **model_config: Unpack[AnthropicConfig]) -> None: # type: ignore[override]
74+
"""Update the Anthropic model configuration with the provided arguments.
75+
76+
Args:
77+
**model_config: Configuration overrides.
78+
"""
79+
self.config.update(model_config)
80+
81+
@override
82+
def get_config(self) -> AnthropicConfig:
83+
"""Get the Anthropic model configuration.
84+
85+
Returns:
86+
The Anthropic model configuration.
87+
"""
88+
return self.config
89+
90+
def _format_request_message_content(self, content: ContentBlock) -> dict[str, Any]:
91+
"""Format an Anthropic content block.
92+
93+
Args:
94+
content: Message content.
95+
96+
Returns:
97+
Anthropic formatted content block.
98+
"""
99+
if "document" in content:
100+
return {
101+
"source": {
102+
"data": base64.b64encode(content["document"]["source"]["bytes"]).decode("utf-8"),
103+
"media_type": mimetypes.types_map.get(
104+
f".{content['document']['format']}", "application/octet-stream"
105+
),
106+
"type": "base64",
107+
},
108+
"title": content["document"]["name"],
109+
"type": "document",
110+
}
111+
112+
if "image" in content:
113+
return {
114+
"source": {
115+
"data": base64.b64encode(content["image"]["source"]["bytes"]).decode("utf-8"),
116+
"media_type": mimetypes.types_map.get(f".{content['image']['format']}", "application/octet-stream"),
117+
"type": "base64",
118+
},
119+
"type": "image",
120+
}
121+
122+
if "reasoningContent" in content:
123+
return {
124+
"signature": content["reasoningContent"]["reasoningText"]["signature"],
125+
"thinking": content["reasoningContent"]["reasoningText"]["text"],
126+
"type": "thinking",
127+
}
128+
129+
if "text" in content:
130+
return {"text": content["text"], "type": "text"}
131+
132+
if "toolUse" in content:
133+
return {
134+
"id": content["toolUse"]["toolUseId"],
135+
"input": content["toolUse"]["input"],
136+
"name": content["toolUse"]["name"],
137+
"type": "tool_use",
138+
}
139+
140+
if "toolResult" in content:
141+
return {
142+
"content": [
143+
self._format_request_message_content(cast(ContentBlock, tool_result_content))
144+
for tool_result_content in content["toolResult"]["content"]
145+
],
146+
"is_error": content["toolResult"]["status"] == "error",
147+
"tool_use_id": content["toolResult"]["toolUseId"],
148+
"type": "tool_result",
149+
}
150+
151+
return {"text": json.dumps(content), "type": "text"}
152+
153+
def _format_request_messages(self, messages: Messages) -> list[dict[str, Any]]:
154+
"""Format an Anthropic messages array.
155+
156+
Args:
157+
messages: List of message objects to be processed by the model.
158+
159+
Returns:
160+
An Anthropic messages array.
161+
"""
162+
formatted_messages = []
163+
164+
for message in messages:
165+
formatted_contents: list[dict[str, Any]] = []
166+
167+
for content in message["content"]:
168+
if "cachePoint" in content:
169+
formatted_contents[-1]["cache_control"] = {"type": "ephemeral"}
170+
continue
171+
172+
formatted_contents.append(self._format_request_message_content(content))
173+
174+
if formatted_contents:
175+
formatted_messages.append({"content": formatted_contents, "role": message["role"]})
176+
177+
return formatted_messages
178+
179+
@override
180+
def format_request(
181+
self, messages: Messages, tool_specs: Optional[list[ToolSpec]] = None, system_prompt: Optional[str] = None
182+
) -> dict[str, Any]:
183+
"""Format an Anthropic streaming request.
184+
185+
Args:
186+
messages: List of message objects to be processed by the model.
187+
tool_specs: List of tool specifications to make available to the model.
188+
system_prompt: System prompt to provide context to the model.
189+
190+
Returns:
191+
An Anthropic streaming request.
192+
"""
193+
return {
194+
"max_tokens": self.config["max_tokens"],
195+
"messages": self._format_request_messages(messages),
196+
"model": self.config["model_id"],
197+
"tools": [
198+
{
199+
"name": tool_spec["name"],
200+
"description": tool_spec["description"],
201+
"input_schema": tool_spec["inputSchema"]["json"],
202+
}
203+
for tool_spec in tool_specs or []
204+
],
205+
**({"system": system_prompt} if system_prompt else {}),
206+
**(self.config.get("params") or {}),
207+
}
208+
209+
@override
210+
def format_chunk(self, event: dict[str, Any]) -> StreamEvent:
211+
"""Format the Anthropic response events into standardized message chunks.
212+
213+
Args:
214+
event: A response event from the Anthropic model.
215+
216+
Returns:
217+
The formatted chunk.
218+
219+
Raises:
220+
RuntimeError: If chunk_type is not recognized.
221+
This error should never be encountered as we control chunk_type in the stream method.
222+
"""
223+
match event["type"]:
224+
case "message_start":
225+
return {"messageStart": {"role": "assistant"}}
226+
227+
case "content_block_start":
228+
content = event["content_block"]
229+
230+
if content["type"] == "tool_use":
231+
return {
232+
"contentBlockStart": {
233+
"contentBlockIndex": event["index"],
234+
"start": {
235+
"toolUse": {
236+
"name": content["name"],
237+
"toolUseId": content["id"],
238+
}
239+
},
240+
}
241+
}
242+
243+
return {"contentBlockStart": {"contentBlockIndex": event["index"], "start": {}}}
244+
245+
case "content_block_delta":
246+
delta = event["delta"]
247+
248+
match delta["type"]:
249+
case "signature_delta":
250+
return {
251+
"contentBlockDelta": {
252+
"contentBlockIndex": event["index"],
253+
"delta": {
254+
"reasoningContent": {
255+
"signature": delta["signature"],
256+
},
257+
},
258+
},
259+
}
260+
261+
case "thinking_delta":
262+
return {
263+
"contentBlockDelta": {
264+
"contentBlockIndex": event["index"],
265+
"delta": {
266+
"reasoningContent": {
267+
"text": delta["thinking"],
268+
},
269+
},
270+
},
271+
}
272+
273+
case "input_json_delta":
274+
return {
275+
"contentBlockDelta": {
276+
"contentBlockIndex": event["index"],
277+
"delta": {
278+
"toolUse": {
279+
"input": delta["partial_json"],
280+
},
281+
},
282+
},
283+
}
284+
285+
case "text_delta":
286+
return {
287+
"contentBlockDelta": {
288+
"contentBlockIndex": event["index"],
289+
"delta": {
290+
"text": delta["text"],
291+
},
292+
},
293+
}
294+
295+
case _:
296+
raise RuntimeError(
297+
f"event_type=<content_block_delta>, delta_type=<{delta['type']}> | unknown type"
298+
)
299+
300+
case "content_block_stop":
301+
return {"contentBlockStop": {"contentBlockIndex": event["index"]}}
302+
303+
case "message_stop":
304+
message = event["message"]
305+
306+
return {"messageStop": {"stopReason": message["stop_reason"]}}
307+
308+
case "metadata":
309+
usage = event["usage"]
310+
311+
return {
312+
"metadata": {
313+
"usage": {
314+
"inputTokens": usage["input_tokens"],
315+
"outputTokens": usage["output_tokens"],
316+
"totalTokens": usage["input_tokens"] + usage["output_tokens"],
317+
},
318+
"metrics": {
319+
"latencyMs": 0, # TODO
320+
},
321+
}
322+
}
323+
324+
case _:
325+
raise RuntimeError(f"event_type=<{event['type']} | unknown type")
326+
327+
@override
328+
def stream(self, request: dict[str, Any]) -> Iterable[dict[str, Any]]:
329+
"""Send the request to the Anthropic model and get the streaming response.
330+
331+
Args:
332+
request: The formatted request to send to the Anthropic model.
333+
334+
Returns:
335+
An iterable of response events from the Anthropic model.
336+
337+
Raises:
338+
ContextWindowOverflowException: If the input exceeds the model's context window.
339+
ModelThrottledException: If the request is throttled by Anthropic.
340+
"""
341+
try:
342+
with self.client.messages.stream(**request) as stream:
343+
for event in stream:
344+
if event.type in AnthropicModel.EVENT_TYPES:
345+
yield event.dict()
346+
347+
usage = event.message.usage # type: ignore
348+
yield {"type": "metadata", "usage": usage.dict()}
349+
350+
except anthropic.RateLimitError as error:
351+
raise ModelThrottledException(str(error)) from error
352+
353+
except anthropic.BadRequestError as error:
354+
if any(overflow_message in str(error).lower() for overflow_message in AnthropicModel.OVERFLOW_MESSAGES):
355+
raise ContextWindowOverflowException(str(error)) from error
356+
357+
raise error

0 commit comments

Comments
 (0)
0