agent_io/llm/anthropic/
mod.rs1mod request;
4mod response;
5mod types;
6
7use async_trait::async_trait;
8use derive_builder::Builder;
9use futures::StreamExt;
10use reqwest::Client;
11use std::time::Duration;
12
13use crate::llm::{
14 BaseChatModel, ChatCompletion, ChatStream, LlmError, Message, ToolChoice, ToolDefinition,
15};
16
17use types::*;
18
19const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
20
21#[derive(Builder, Clone)]
23#[builder(pattern = "owned", build_fn(skip))]
24pub struct ChatAnthropic {
25 #[builder(setter(into))]
27 pub(super) model: String,
28 pub(super) api_key: String,
30 #[builder(setter(into, strip_option), default = "None")]
32 pub(super) base_url: Option<String>,
33 #[builder(default = "8192")]
35 pub(super) max_tokens: u64,
36 #[builder(default = "0.2")]
38 pub(super) temperature: f32,
39 #[builder(default = r#"Some("prompt-caching-2024-07-31".to_string())"#)]
41 pub(super) prompt_cache_beta: Option<String>,
42 #[builder(default = "false")]
44 pub(super) thinking: bool,
45 #[builder(default = "Some(1024)")]
47 pub(super) thinking_budget: Option<u64>,
48 #[builder(setter(skip))]
50 pub(super) client: Client,
51 #[builder(setter(skip))]
53 pub(super) context_window: u64,
54}
55
56impl ChatAnthropic {
57 pub fn new(model: impl Into<String>) -> Result<Self, LlmError> {
59 let api_key = std::env::var("ANTHROPIC_API_KEY")
60 .map_err(|_| LlmError::Config("ANTHROPIC_API_KEY not set".into()))?;
61
62 Self::builder().model(model).api_key(api_key).build()
63 }
64
65 pub fn builder() -> ChatAnthropicBuilder {
67 ChatAnthropicBuilder::default()
68 }
69
70 fn api_url(&self) -> &str {
72 self.base_url.as_deref().unwrap_or(ANTHROPIC_API_URL)
73 }
74
75 fn build_client() -> Client {
77 Client::builder()
78 .timeout(Duration::from_secs(120))
79 .build()
80 .expect("Failed to create HTTP client")
81 }
82
83 fn get_context_window(_model: &str) -> u64 {
85 200_000
87 }
88
89 fn supports_thinking(&self) -> bool {
91 let model_lower = self.model.to_lowercase();
92 model_lower.contains("claude-3-7-sonnet")
93 || model_lower.contains("claude-3.7")
94 || model_lower.contains("claude-4")
95 }
96}
97
98impl ChatAnthropicBuilder {
99 pub fn build(&self) -> Result<ChatAnthropic, LlmError> {
100 let model = self
101 .model
102 .clone()
103 .ok_or_else(|| LlmError::Config("model is required".into()))?;
104 let api_key = self
105 .api_key
106 .clone()
107 .ok_or_else(|| LlmError::Config("api_key is required".into()))?;
108
109 Ok(ChatAnthropic {
110 context_window: ChatAnthropic::get_context_window(&model),
111 client: ChatAnthropic::build_client(),
112 model,
113 api_key,
114 base_url: self.base_url.clone().flatten(),
115 max_tokens: self.max_tokens.unwrap_or(8192),
116 temperature: self.temperature.unwrap_or(0.2),
117 prompt_cache_beta: self.prompt_cache_beta.clone().flatten(),
118 thinking: self.thinking.unwrap_or(false),
119 thinking_budget: self.thinking_budget.flatten(),
120 })
121 }
122}
123
124#[async_trait]
125impl BaseChatModel for ChatAnthropic {
126 fn model(&self) -> &str {
127 &self.model
128 }
129
130 fn provider(&self) -> &str {
131 "anthropic"
132 }
133
134 fn context_window(&self) -> Option<u64> {
135 Some(self.context_window)
136 }
137
138 async fn invoke(
139 &self,
140 messages: Vec<Message>,
141 tools: Option<Vec<ToolDefinition>>,
142 tool_choice: Option<ToolChoice>,
143 ) -> Result<ChatCompletion, LlmError> {
144 let request = self.build_request(messages, tools, tool_choice, false)?;
145
146 let mut req = self
147 .client
148 .post(self.api_url())
149 .header("x-api-key", &self.api_key)
150 .header("anthropic-version", "2023-06-01")
151 .header("Content-Type", "application/json");
152
153 if let Some(ref beta) = self.prompt_cache_beta {
154 req = req.header("anthropic-beta", beta.as_str());
155 }
156
157 let response = req.json(&request).send().await?;
158
159 if !response.status().is_success() {
160 let status = response.status();
161 let body = response.text().await.unwrap_or_default();
162 return Err(LlmError::Api(format!(
163 "Anthropic API error ({}): {}",
164 status, body
165 )));
166 }
167
168 let completion: AnthropicResponse = response.json().await?;
169 Ok(self.parse_response(completion))
170 }
171
172 async fn invoke_stream(
173 &self,
174 messages: Vec<Message>,
175 tools: Option<Vec<ToolDefinition>>,
176 tool_choice: Option<ToolChoice>,
177 ) -> Result<ChatStream, LlmError> {
178 let request = self.build_request(messages, tools, tool_choice, true)?;
179
180 let mut req = self
181 .client
182 .post(self.api_url())
183 .header("x-api-key", &self.api_key)
184 .header("anthropic-version", "2023-06-01")
185 .header("Content-Type", "application/json");
186
187 if let Some(ref beta) = self.prompt_cache_beta {
188 req = req.header("anthropic-beta", beta.as_str());
189 }
190
191 let response = req.json(&request).send().await?;
192
193 if !response.status().is_success() {
194 let status = response.status();
195 let body = response.text().await.unwrap_or_default();
196 return Err(LlmError::Api(format!(
197 "Anthropic API error ({}): {}",
198 status, body
199 )));
200 }
201
202 let stream = response.bytes_stream().filter_map(|result| async move {
204 match result {
205 Ok(bytes) => {
206 let text = String::from_utf8_lossy(&bytes);
207 Self::parse_sse_event(&text)
208 }
209 Err(e) => Some(Err(LlmError::Stream(e.to_string()))),
210 }
211 });
212
213 Ok(Box::pin(stream))
214 }
215
216 fn supports_vision(&self) -> bool {
217 true
219 }
220}