apollo/providers/
anthropic.rs1use async_trait::async_trait;
5use serde_json::Value;
6
7use super::retry::send_with_retry;
8use super::traits::*;
9use crate::cost::{CostTracker, TokenUsage};
10use crate::text::truncate_chars;
11use crate::tools::ToolSpec;
12
13pub struct AnthropicProvider {
14 api_key: String,
15 base_url: String,
16 cost_tracker: Option<std::sync::Arc<CostTracker>>,
17}
18
19impl AnthropicProvider {
20 pub fn new(api_key: impl Into<String>) -> Self {
21 Self {
22 api_key: api_key.into(),
23 base_url: "https://api.anthropic.com/v1".to_string(),
24 cost_tracker: None,
25 }
26 }
27
28 pub fn from_env_or_oauth() -> anyhow::Result<Self> {
30 if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") {
32 return Ok(Self::new(key));
33 }
34
35 if let Ok((token, _, _)) = super::oauth::load_oauth_token_from_file() {
37 return Ok(Self::new(token));
38 }
39
40 Err(anyhow::anyhow!(
41 "No ANTHROPIC_API_KEY found. Set env var or install Claude for Desktop with OAuth token."
42 ))
43 }
44
45 pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
46 self.base_url = url.into();
47 self
48 }
49
50 pub fn with_cost_tracker(mut self, tracker: std::sync::Arc<CostTracker>) -> Self {
51 self.cost_tracker = Some(tracker);
52 self
53 }
54
55 fn build_anthropic_messages(&self, messages: &[ChatMessage]) -> Vec<Value> {
58 let mut result: Vec<Value> = Vec::new();
59
60 for msg in messages {
61 match msg.role.as_str() {
62 "system" => continue, "user" => {
64 result.push(serde_json::json!({
65 "role": "user",
66 "content": &msg.content,
67 }));
68 }
69 "assistant" => {
70 result.push(serde_json::json!({
71 "role": "assistant",
72 "content": &msg.content,
73 }));
74 }
75 "assistant_tool_use" => {
76 if let Some(tool_json) = &msg.tool_use_id {
79 if let Ok(blocks) = serde_json::from_str::<Vec<Value>>(tool_json) {
80 result.push(serde_json::json!({
81 "role": "assistant",
82 "content": blocks,
83 }));
84 }
85 }
86 }
87 "tool_result" => {
88 if let Some(tool_use_id) = &msg.tool_use_id {
90 result.push(serde_json::json!({
91 "role": "user",
92 "content": [{
93 "type": "tool_result",
94 "tool_use_id": tool_use_id,
95 "content": &msg.content,
96 }],
97 }));
98 }
99 }
100 other => {
101 result.push(serde_json::json!({
103 "role": other,
104 "content": &msg.content,
105 }));
106 }
107 }
108 }
109
110 result
111 }
112
113 fn build_tools_payload(&self, tools: &[ToolSpec]) -> Vec<Value> {
114 tools
115 .iter()
116 .map(|t| {
117 serde_json::json!({
118 "name": t.name,
119 "description": t.description,
120 "input_schema": t.parameters,
121 })
122 })
123 .collect()
124 }
125
126 async fn record_usage(&self, data: &Value, model: &str) {
128 if let Some(tracker) = &self.cost_tracker {
129 if let Some(usage_obj) = data.get("usage").and_then(|v| v.as_object()) {
130 let input_tokens = usage_obj
131 .get("input_tokens")
132 .and_then(|v| v.as_u64())
133 .unwrap_or(0) as usize;
134 let output_tokens = usage_obj
135 .get("output_tokens")
136 .and_then(|v| v.as_u64())
137 .unwrap_or(0) as usize;
138
139 let usage = TokenUsage {
140 input_tokens,
141 output_tokens,
142 total_tokens: input_tokens + output_tokens,
143 };
144
145 if let Err(e) = tracker.record(model, usage).await {
146 tracing::warn!("Failed to record cost: {}", e);
147 }
148 }
149 }
150 }
151}
152
153#[async_trait]
154impl Provider for AnthropicProvider {
155 fn name(&self) -> &str {
156 "anthropic"
157 }
158
159 fn capabilities(&self) -> ProviderCapabilities {
160 ProviderCapabilities {
161 native_tools: true,
162 streaming: true,
163 vision: true,
164 max_context: 200_000,
165 }
166 }
167
168 async fn chat(&self, request: &ChatRequest<'_>) -> anyhow::Result<ChatResponse> {
169 let client = reqwest::Client::builder()
171 .timeout(std::time::Duration::from_secs(120))
172 .build()?;
173
174 let system: Option<Value> = {
177 let sys_parts: Vec<&str> = request
178 .messages
179 .iter()
180 .filter(|m| m.role == "system")
181 .map(|m| m.content.as_str())
182 .collect();
183 if sys_parts.is_empty() {
184 None
185 } else {
186 Some(serde_json::json!([{
188 "type": "text",
189 "text": sys_parts.join("\n\n---\n\n"),
190 "cache_control": {"type": "ephemeral"}
191 }]))
192 }
193 };
194
195 let messages: Vec<Value> = self.build_anthropic_messages(request.messages);
197
198 let mut body = serde_json::json!({
199 "model": request.model,
200 "messages": messages,
201 "max_tokens": request.max_tokens.unwrap_or(8192),
202 "temperature": request.temperature,
203 });
204
205 if let Some(sys) = system {
206 body["system"] = sys;
207 }
208
209 if let Some(tools) = request.tools {
210 if !tools.is_empty() {
211 body["tools"] = Value::Array(self.build_tools_payload(tools));
212 }
213 }
214
215 let is_oauth = self.api_key.contains("sk-ant-oat");
217
218 if is_oauth {
220 let prefix = serde_json::json!({
221 "type": "text",
222 "text": "You are Claude Code, Anthropic's official CLI for Claude.",
223 "cache_control": {"type": "ephemeral"}
224 });
225 match body.get("system") {
226 Some(Value::Array(blocks)) => {
227 let mut new_blocks = vec![prefix];
228 new_blocks.extend(blocks.iter().cloned());
229 body["system"] = Value::Array(new_blocks);
230 }
231 Some(Value::String(s)) => {
232 body["system"] = serde_json::json!([
233 prefix,
234 {"type": "text", "text": s, "cache_control": {"type": "ephemeral"}}
235 ]);
236 }
237 None => {
238 body["system"] = serde_json::json!([prefix]);
239 }
240 _ => {}
241 }
242 }
243
244 let mut req_builder = client
245 .post(format!("{}/messages", self.base_url))
246 .header("content-type", "application/json")
247 .header("anthropic-version", "2023-06-01");
248
249 if is_oauth {
250 req_builder = req_builder
251 .header("Authorization", format!("Bearer {}", self.api_key))
252 .header(
253 "anthropic-beta",
254 "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14",
255 )
256 .header("anthropic-dangerous-direct-browser-access", "true");
257 } else {
258 req_builder = req_builder
259 .header("x-api-key", &self.api_key)
260 .header("anthropic-beta", "prompt-caching-2024-07-31");
261 }
262
263 let resp = send_with_retry(req_builder.json(&body), self.name()).await?;
264
265 if !resp.status().is_success() {
266 let status = resp.status();
267 let text = resp.text().await.unwrap_or_default();
268 anyhow::bail!(
269 "Anthropic API error {}: {}",
270 status,
271 truncate_chars(&text, 200)
272 );
273 }
274
275 let headers = resp.headers().clone();
277
278 let data: Value = resp.json().await?;
279
280 self.record_usage(&data, request.model).await;
282
283 if let Some(tracker) = &self.cost_tracker {
285 tracker.update_rate_limits(&headers).await;
286 }
287
288 let mut text_parts = Vec::new();
289 let mut tool_calls = Vec::new();
290
291 if let Some(content) = data["content"].as_array() {
292 for block in content {
293 match block["type"].as_str() {
294 Some("text") => {
295 if let Some(t) = block["text"].as_str() {
296 text_parts.push(t.to_string());
297 }
298 }
299 Some("tool_use") => {
300 tool_calls.push(ToolCall {
301 id: block["id"].as_str().unwrap_or("").to_string(),
302 name: block["name"].as_str().unwrap_or("").to_string(),
303 arguments: block["input"].to_string(),
304 });
305 }
306 _ => {}
307 }
308 }
309 }
310
311 let usage = data["usage"].as_object().map(|u| Usage {
312 input_tokens: u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
313 output_tokens: u.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
314 });
315
316 Ok(ChatResponse {
317 text: if text_parts.is_empty() {
318 None
319 } else {
320 Some(text_parts.join(""))
321 },
322 tool_calls,
323 usage,
324 })
325 }
326}