agents_runtime/providers/
anthropic.rs1use agents_core::llm::{LanguageModel, LlmRequest, LlmResponse};
2use agents_core::messaging::{AgentMessage, MessageContent, MessageRole};
3use agents_core::tools::ToolSchema;
4use async_trait::async_trait;
5use reqwest::Client;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Clone)]
10pub struct AnthropicConfig {
11 pub api_key: String,
12 pub model: String,
13 pub max_output_tokens: u32,
14 pub api_url: Option<String>,
15 pub api_version: Option<String>,
16}
17
18pub struct AnthropicMessagesModel {
19 client: Client,
20 config: AnthropicConfig,
21}
22
23impl AnthropicMessagesModel {
24 pub fn new(config: AnthropicConfig) -> anyhow::Result<Self> {
25 Ok(Self {
26 client: Client::builder()
27 .user_agent("rust-deep-agents-sdk/0.1")
28 .build()?,
29 config,
30 })
31 }
32}
33
34#[derive(Serialize)]
35struct AnthropicRequest {
36 model: String,
37 max_tokens: u32,
38 system: String,
39 messages: Vec<AnthropicMessage>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 tools: Option<Vec<AnthropicTool>>,
42}
43
44#[derive(Serialize)]
45struct AnthropicTool {
46 name: String,
47 description: String,
48 input_schema: Value,
49}
50
51#[derive(Serialize)]
52struct AnthropicMessage {
53 role: String,
54 content: Vec<AnthropicContentBlock>,
55}
56
57#[derive(Serialize)]
58struct AnthropicContentBlock {
59 #[serde(rename = "type")]
60 kind: &'static str,
61 text: String,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 cache_control: Option<AnthropicCacheControl>,
64}
65
66#[derive(Serialize)]
67struct AnthropicCacheControl {
68 #[serde(rename = "type")]
69 cache_type: String,
70}
71
72#[derive(Deserialize)]
73struct AnthropicResponse {
74 content: Vec<AnthropicResponseBlock>,
75}
76
77#[derive(Deserialize)]
78struct AnthropicResponseBlock {
79 #[serde(rename = "type")]
80 kind: String,
81 text: Option<String>,
82 #[allow(dead_code)]
83 id: Option<String>,
84 name: Option<String>,
85 input: Option<Value>,
86}
87
88fn to_anthropic_messages(request: &LlmRequest) -> (String, Vec<AnthropicMessage>) {
89 let mut system_prompt = request.system_prompt.clone();
90 let mut messages = Vec::new();
91
92 for message in &request.messages {
93 let text = match &message.content {
94 MessageContent::Text(text) => text.clone(),
95 MessageContent::Json(value) => value.to_string(),
96 };
97
98 if matches!(message.role, MessageRole::System) {
100 if !system_prompt.is_empty() {
101 system_prompt.push_str("\n\n");
102 }
103 system_prompt.push_str(&text);
104 continue;
105 }
106
107 let role = match message.role {
108 MessageRole::User => "user",
109 MessageRole::Agent => "assistant",
110 MessageRole::Tool => "user",
111 MessageRole::System => unreachable!(), };
113
114 let cache_control = message
116 .metadata
117 .as_ref()
118 .and_then(|meta| meta.cache_control.as_ref())
119 .map(|cc| AnthropicCacheControl {
120 cache_type: cc.cache_type.clone(),
121 });
122
123 messages.push(AnthropicMessage {
124 role: role.to_string(),
125 content: vec![AnthropicContentBlock {
126 kind: "text",
127 text,
128 cache_control,
129 }],
130 });
131 }
132
133 (system_prompt, messages)
134}
135
136fn to_anthropic_tools(tools: &[ToolSchema]) -> Option<Vec<AnthropicTool>> {
138 if tools.is_empty() {
139 return None;
140 }
141
142 Some(
143 tools
144 .iter()
145 .map(|tool| AnthropicTool {
146 name: tool.name.clone(),
147 description: tool.description.clone(),
148 input_schema: serde_json::to_value(&tool.parameters)
149 .unwrap_or_else(|_| serde_json::json!({})),
150 })
151 .collect(),
152 )
153}
154
155#[async_trait]
156impl LanguageModel for AnthropicMessagesModel {
157 async fn generate(&self, request: LlmRequest) -> anyhow::Result<LlmResponse> {
158 let (system_prompt, messages) = to_anthropic_messages(&request);
159 let tools = to_anthropic_tools(&request.tools);
160
161 tracing::debug!(
163 "Anthropic request: model={}, messages={}, tools={}",
164 self.config.model,
165 messages.len(),
166 tools.as_ref().map(|t| t.len()).unwrap_or(0)
167 );
168
169 let body = AnthropicRequest {
170 model: self.config.model.clone(),
171 max_tokens: self.config.max_output_tokens,
172 system: system_prompt,
173 messages,
174 tools,
175 };
176
177 let url = self
178 .config
179 .api_url
180 .as_deref()
181 .unwrap_or("https://api.anthropic.com/v1/messages");
182 let version = self.config.api_version.as_deref().unwrap_or("2023-06-01");
183
184 let response = self
185 .client
186 .post(url)
187 .header("x-api-key", &self.config.api_key)
188 .header("anthropic-version", version)
189 .json(&body)
190 .send()
191 .await?
192 .error_for_status()?;
193
194 let data: AnthropicResponse = response.json().await?;
195
196 let tool_uses: Vec<_> = data
198 .content
199 .iter()
200 .filter(|block| block.kind == "tool_use")
201 .collect();
202
203 if !tool_uses.is_empty() {
204 let tool_calls: Vec<_> = tool_uses
206 .iter()
207 .filter_map(|block| {
208 Some(serde_json::json!({
209 "name": block.name.as_ref()?,
210 "args": block.input.as_ref()?
211 }))
212 })
213 .collect();
214
215 tracing::debug!("Anthropic response contains {} tool uses", tool_calls.len());
216
217 return Ok(LlmResponse {
218 message: AgentMessage {
219 role: MessageRole::Agent,
220 content: MessageContent::Json(serde_json::json!({
221 "tool_calls": tool_calls
222 })),
223 metadata: None,
224 },
225 });
226 }
227
228 let text = data
230 .content
231 .into_iter()
232 .find_map(|block| (block.kind == "text").then(|| block.text.unwrap_or_default()))
233 .unwrap_or_default();
234
235 Ok(LlmResponse {
236 message: AgentMessage {
237 role: MessageRole::Agent,
238 content: MessageContent::Text(text),
239 metadata: None,
240 },
241 })
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn anthropic_message_conversion_includes_system_prompt() {
251 let request = LlmRequest::new(
252 "You are helpful",
253 vec![AgentMessage {
254 role: MessageRole::User,
255 content: MessageContent::Text("Hello".into()),
256 metadata: None,
257 }],
258 );
259 let (system, messages) = to_anthropic_messages(&request);
260 assert_eq!(system, "You are helpful");
261 assert_eq!(messages.len(), 1);
262 assert_eq!(messages[0].role, "user");
263 assert_eq!(messages[0].content[0].text, "Hello");
264 }
265}