1mod auth;
2mod stream;
3mod types;
4
5use auth::{AnthropicAuth, AuthResolved, refresh_oauth_token};
6use stream::process_sse_stream;
7use types::{AnthropicRequest, convert_messages, convert_tools};
8
9use std::{
10 future::Future,
11 pin::Pin,
12 time::{SystemTime, UNIX_EPOCH},
13};
14
15use anyhow::Context;
16use tokio::sync::{mpsc, mpsc::UnboundedReceiver};
17use tracing::warn;
18
19use crate::provider::{Message, Provider, StreamEvent, StreamEventType, ToolDefinition};
20
21pub struct AnthropicProvider {
22 client: reqwest::Client,
23 model: String,
24 auth: tokio::sync::Mutex<AnthropicAuth>,
25 cached_models: std::sync::Mutex<Option<Vec<String>>>,
26}
27
28impl AnthropicProvider {
29 pub fn new_with_api_key(api_key: impl Into<String>, model: impl Into<String>) -> Self {
30 Self {
31 client: reqwest::Client::builder()
32 .user_agent("dot/0.1.0")
33 .build()
34 .expect("Failed to build reqwest client"),
35 model: model.into(),
36 auth: tokio::sync::Mutex::new(AnthropicAuth::ApiKey(api_key.into())),
37 cached_models: std::sync::Mutex::new(None),
38 }
39 }
40
41 pub fn new_with_oauth(
42 access_token: impl Into<String>,
43 refresh_token: impl Into<String>,
44 expires_at: i64,
45 model: impl Into<String>,
46 ) -> Self {
47 Self {
48 client: reqwest::Client::builder()
49 .user_agent("claude-code/2.1.49 (external, cli)")
50 .build()
51 .expect("Failed to build reqwest client"),
52 model: model.into(),
53 auth: tokio::sync::Mutex::new(AnthropicAuth::OAuth {
54 access_token: access_token.into(),
55 refresh_token: refresh_token.into(),
56 expires_at,
57 }),
58 cached_models: std::sync::Mutex::new(None),
59 }
60 }
61
62 async fn fetch_model_context_window(&self, model: &str) -> Option<u32> {
63 let auth = self.resolve_auth().await.ok()?;
64 let url = format!("https://api.anthropic.com/v1/models/{model}");
65 let mut req = self
66 .client
67 .get(&url)
68 .header(&auth.header_name, &auth.header_value)
69 .header("anthropic-version", "2023-06-01");
70 if auth.is_oauth {
71 req = req
72 .header(
73 "anthropic-beta",
74 "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14",
75 )
76 .header("user-agent", "claude-code/2.1.49 (external, cli)");
77 }
78 let data: serde_json::Value = req.send().await.ok()?.json().await.ok()?;
79 data["context_window"].as_u64().map(|v| v as u32)
80 }
81
82 fn model_context_window(model: &str) -> u32 {
83 if model.contains("claude") {
84 return 200_000;
85 }
86 0
87 }
88
89 async fn resolve_auth(&self) -> anyhow::Result<AuthResolved> {
90 let mut auth = self.auth.lock().await;
91 match &*auth {
92 AnthropicAuth::ApiKey(key) => Ok(AuthResolved {
93 header_name: "x-api-key".to_string(),
94 header_value: key.clone(),
95 is_oauth: false,
96 }),
97 AnthropicAuth::OAuth {
98 access_token,
99 refresh_token,
100 expires_at,
101 } => {
102 let now = SystemTime::now()
103 .duration_since(UNIX_EPOCH)
104 .unwrap_or_default()
105 .as_secs() as i64;
106 let expires_at_secs = if *expires_at > 1_000_000_000_000 {
108 *expires_at / 1000
109 } else {
110 *expires_at
111 };
112
113 let token = if now >= expires_at_secs - 60 {
114 let rt = refresh_token.clone();
115 match refresh_oauth_token(&self.client, &rt).await {
116 Ok((new_token, new_expires_at, new_refresh)) => {
117 if let AnthropicAuth::OAuth {
118 access_token,
119 refresh_token,
120 expires_at,
121 } = &mut *auth
122 {
123 *access_token = new_token.clone();
124 *expires_at = new_expires_at;
125 if let Some(ref rt) = new_refresh {
126 *refresh_token = rt.clone();
127 }
128 }
129 if let Ok(mut creds) = crate::auth::Credentials::load() {
130 let cred = crate::auth::ProviderCredential::OAuth {
131 access_token: new_token.clone(),
132 refresh_token: new_refresh,
133 expires_at: Some(new_expires_at),
134 api_key: None,
135 };
136 creds.set("anthropic", cred);
137 let _ = creds.save();
138 }
139 new_token
140 }
141 Err(e) => {
142 warn!("OAuth token refresh failed: {e}");
143 access_token.clone()
144 }
145 }
146 } else {
147 access_token.clone()
148 };
149
150 Ok(AuthResolved {
151 header_name: "Authorization".to_string(),
152 header_value: format!("Bearer {token}"),
153 is_oauth: true,
154 })
155 }
156 }
157 }
158}
159
160impl Provider for AnthropicProvider {
161 fn name(&self) -> &str {
162 "anthropic"
163 }
164
165 fn model(&self) -> &str {
166 &self.model
167 }
168
169 fn set_model(&mut self, model: String) {
170 self.model = model;
171 }
172
173 fn available_models(&self) -> Vec<String> {
174 let cache = self.cached_models.lock().unwrap();
175 cache.clone().unwrap_or_default()
176 }
177
178 fn context_window(&self) -> u32 {
179 Self::model_context_window(&self.model)
180 }
181
182 fn supports_server_compaction(&self) -> bool {
183 true
184 }
185
186 fn fetch_context_window(
187 &self,
188 ) -> Pin<Box<dyn Future<Output = anyhow::Result<u32>> + Send + '_>> {
189 Box::pin(async move {
190 if let Some(cw) = self.fetch_model_context_window(&self.model).await {
191 return Ok(cw);
192 }
193 Ok(Self::model_context_window(&self.model))
194 })
195 }
196
197 fn fetch_models(
198 &self,
199 ) -> Pin<Box<dyn Future<Output = anyhow::Result<Vec<String>>> + Send + '_>> {
200 Box::pin(async move {
201 {
202 let cache = self.cached_models.lock().unwrap();
203 if let Some(ref models) = *cache {
204 return Ok(models.clone());
205 }
206 }
207 let auth = self.resolve_auth().await?;
208 let mut all_models: Vec<String> = Vec::new();
209 let mut after_id: Option<String> = None;
210
211 loop {
212 let mut url = "https://api.anthropic.com/v1/models?limit=1000".to_string();
213 if let Some(ref cursor) = after_id {
214 url.push_str(&format!("&after_id={cursor}"));
215 }
216
217 let mut req = self
218 .client
219 .get(&url)
220 .header(&auth.header_name, &auth.header_value)
221 .header("anthropic-version", "2023-06-01");
222
223 if auth.is_oauth {
224 req = req
225 .header(
226 "anthropic-beta",
227 "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14",
228 )
229 .header("user-agent", "claude-code/2.1.49 (external, cli)");
230 }
231
232 let resp = req
233 .send()
234 .await
235 .context("Failed to fetch Anthropic models")?;
236
237 if !resp.status().is_success() {
238 let status = resp.status();
239 let body = resp.text().await.unwrap_or_default();
240 return Err(anyhow::anyhow!(
241 "Anthropic models API error {status}: {body}"
242 ));
243 }
244
245 let data: serde_json::Value = resp
246 .json()
247 .await
248 .context("Failed to parse Anthropic models response")?;
249
250 if let Some(arr) = data["data"].as_array() {
251 for m in arr {
252 if let Some(id) = m["id"].as_str() {
253 all_models.push(id.to_string());
254 }
255 }
256 }
257
258 let has_more = data["has_more"].as_bool().unwrap_or(false);
259 if !has_more {
260 break;
261 }
262
263 match data["last_id"].as_str() {
264 Some(last) => after_id = Some(last.to_string()),
265 None => break,
266 }
267 }
268
269 if all_models.is_empty() {
270 return Err(anyhow::anyhow!("Anthropic models API returned empty list"));
271 }
272
273 all_models.sort();
274 let mut cache = self.cached_models.lock().unwrap();
275 *cache = Some(all_models.clone());
276
277 Ok(all_models)
278 })
279 }
280
281 fn stream(
282 &self,
283 messages: &[Message],
284 system: Option<&str>,
285 tools: &[ToolDefinition],
286 max_tokens: u32,
287 thinking_budget: u32,
288 ) -> Pin<Box<dyn Future<Output = anyhow::Result<UnboundedReceiver<StreamEvent>>> + Send + '_>>
289 {
290 self.stream_with_model(
291 &self.model,
292 messages,
293 system,
294 tools,
295 max_tokens,
296 thinking_budget,
297 )
298 }
299
300 fn stream_with_model(
301 &self,
302 model: &str,
303 messages: &[Message],
304 system: Option<&str>,
305 tools: &[ToolDefinition],
306 max_tokens: u32,
307 thinking_budget: u32,
308 ) -> Pin<Box<dyn Future<Output = anyhow::Result<UnboundedReceiver<StreamEvent>>> + Send + '_>>
309 {
310 let messages = messages.to_vec();
311 let system = system.map(String::from);
312 let tools = tools.to_vec();
313 let model = model.to_string();
314
315 Box::pin(async move {
316 let auth = self.resolve_auth().await?;
317
318 let url = if auth.is_oauth {
319 "https://api.anthropic.com/v1/messages?beta=true".to_string()
320 } else {
321 "https://api.anthropic.com/v1/messages".to_string()
322 };
323
324 let thinking = if thinking_budget >= 1024 {
325 Some(serde_json::json!({
326 "type": "enabled",
327 "budget_tokens": thinking_budget,
328 }))
329 } else {
330 None
331 };
332
333 let effective_max_tokens = if thinking_budget >= 1024 {
334 max_tokens.max(thinking_budget.saturating_add(4096))
335 } else {
336 max_tokens
337 };
338
339 let context_management = Some(serde_json::json!({
340 "edits": [{ "type": "compact_20260112" }]
341 }));
342
343 let system_value = if auth.is_oauth {
344 let identity = serde_json::json!({
345 "type": "text",
346 "text": "You are Claude Code, Anthropic's official CLI for Claude.",
347 "cache_control": { "type": "ephemeral" }
348 });
349 match system {
350 Some(s) => Some(serde_json::json!([
351 identity,
352 { "type": "text", "text": s }
353 ])),
354 None => Some(serde_json::json!([identity])),
355 }
356 } else {
357 system.map(serde_json::Value::String)
358 };
359
360 let body = AnthropicRequest {
361 model: &model,
362 messages: convert_messages(&messages),
363 max_tokens: effective_max_tokens,
364 stream: true,
365 system: system_value,
366 tools: convert_tools(&tools),
367 temperature: 1.0,
368 thinking,
369 context_management,
370 };
371
372 let mut req_builder = self
373 .client
374 .post(&url)
375 .header(&auth.header_name, &auth.header_value)
376 .header("anthropic-version", "2023-06-01")
377 .header("content-type", "application/json");
378
379 let compact_beta = "compact-2026-01-12";
380 if auth.is_oauth {
381 req_builder = req_builder
382 .header(
383 "anthropic-beta",
384 format!("claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,{compact_beta}"),
385 )
386 .header("user-agent", "claude-code/2.1.49 (external, cli)")
387 .header("anthropic-dangerous-direct-browser-access", "true")
388 .header("x-app", "cli");
389 } else if thinking_budget >= 1024 {
390 req_builder = req_builder.header(
391 "anthropic-beta",
392 format!("interleaved-thinking-2025-05-14,{compact_beta}"),
393 );
394 } else {
395 req_builder = req_builder.header("anthropic-beta", compact_beta);
396 }
397
398 let response = req_builder
399 .json(&body)
400 .send()
401 .await
402 .context("Failed to connect to Anthropic API")?;
403
404 if !response.status().is_success() {
405 let status = response.status();
406 let body_text = response.text().await.unwrap_or_default();
407 return Err(anyhow::anyhow!("Anthropic API error {status}: {body_text}"));
408 }
409
410 let (tx, rx) = mpsc::unbounded_channel::<StreamEvent>();
411 let tx_clone = tx.clone();
412
413 tokio::spawn(async move {
414 if let Err(e) = process_sse_stream(response, tx_clone.clone()).await {
415 let _ = tx_clone.send(StreamEvent {
416 event_type: StreamEventType::Error(e.to_string()),
417 });
418 }
419 });
420
421 Ok(rx)
422 })
423 }
424}