claude_agent/client/adapter/
anthropic.rs

1//! Anthropic Direct API adapter.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use tokio::sync::RwLock;
7
8use super::config::ProviderConfig;
9use super::traits::ProviderAdapter;
10use crate::auth::{Credential, CredentialProvider, OAuthConfig};
11use crate::client::messages::{
12    CountTokensRequest, CountTokensResponse, CreateMessageRequest, ErrorResponse,
13};
14use crate::types::ApiResponse;
15use crate::{Error, Result};
16
17const BASE_URL: &str = "https://api.anthropic.com";
18
19#[derive(Debug, Clone)]
20enum AuthMethod {
21    ApiKey(String),
22    OAuth { token: String, config: OAuthConfig },
23}
24
25impl AuthMethod {
26    fn from_credential(credential: &Credential, oauth_config: Option<OAuthConfig>) -> Self {
27        match credential {
28            Credential::ApiKey(key) => Self::ApiKey(key.clone()),
29            Credential::OAuth(oauth) => Self::OAuth {
30                token: oauth.access_token.clone(),
31                config: oauth_config.unwrap_or_default(),
32            },
33        }
34    }
35
36    fn update_token(&mut self, credential: &Credential) {
37        match credential {
38            Credential::ApiKey(key) => *self = Self::ApiKey(key.clone()),
39            Credential::OAuth(oauth) => {
40                if let Self::OAuth { token, .. } = self {
41                    *token = oauth.access_token.clone();
42                } else {
43                    *self = Self::OAuth {
44                        token: oauth.access_token.clone(),
45                        config: OAuthConfig::default(),
46                    };
47                }
48            }
49        }
50    }
51}
52
53pub struct AnthropicAdapter {
54    config: ProviderConfig,
55    base_url: String,
56    auth: RwLock<AuthMethod>,
57    credential_provider: Option<Arc<dyn CredentialProvider>>,
58}
59
60impl std::fmt::Debug for AnthropicAdapter {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("AnthropicAdapter")
63            .field("config", &self.config)
64            .field("base_url", &self.base_url)
65            .finish()
66    }
67}
68
69impl AnthropicAdapter {
70    pub fn new(config: ProviderConfig) -> Self {
71        Self {
72            config,
73            base_url: Self::base_url_from_env(),
74            auth: RwLock::new(AuthMethod::ApiKey(Self::api_key_from_env())),
75            credential_provider: None,
76        }
77    }
78
79    fn api_key_from_env() -> String {
80        std::env::var("ANTHROPIC_API_KEY").unwrap_or_default()
81    }
82
83    fn base_url_from_env() -> String {
84        std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| BASE_URL.into())
85    }
86
87    pub fn from_credential(
88        config: ProviderConfig,
89        credential: &Credential,
90        oauth_config: Option<OAuthConfig>,
91    ) -> Self {
92        Self {
93            config,
94            base_url: Self::base_url_from_env(),
95            auth: RwLock::new(AuthMethod::from_credential(credential, oauth_config)),
96            credential_provider: None,
97        }
98    }
99
100    pub fn from_credential_provider(
101        config: ProviderConfig,
102        credential: &Credential,
103        oauth_config: Option<OAuthConfig>,
104        provider: Arc<dyn CredentialProvider>,
105    ) -> Self {
106        Self {
107            config,
108            base_url: Self::base_url_from_env(),
109            auth: RwLock::new(AuthMethod::from_credential(credential, oauth_config)),
110            credential_provider: Some(provider),
111        }
112    }
113
114    pub fn with_api_key(self, key: impl Into<String>) -> Self {
115        Self {
116            auth: RwLock::new(AuthMethod::ApiKey(key.into())),
117            ..self
118        }
119    }
120
121    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
122        self.base_url = url.into();
123        self
124    }
125
126    fn build_endpoint_url(&self, auth: &AuthMethod, endpoint: &str) -> String {
127        match auth {
128            AuthMethod::OAuth { config, .. } => config.build_url(&self.base_url, endpoint),
129            AuthMethod::ApiKey(_) => format!("{}{}", self.base_url, endpoint),
130        }
131    }
132
133    fn build_headers(
134        &self,
135        req: reqwest::RequestBuilder,
136        auth: &AuthMethod,
137    ) -> reqwest::RequestBuilder {
138        let mut r = match auth {
139            AuthMethod::ApiKey(key) => req
140                .header("x-api-key", key)
141                .header("anthropic-version", &self.config.api_version)
142                .header("content-type", "application/json"),
143            AuthMethod::OAuth { token, config } => {
144                config.apply_headers(req, token, &self.config.api_version, &self.config.beta)
145            }
146        };
147
148        if let AuthMethod::ApiKey(_) = auth
149            && let Some(beta) = self.config.beta.header_value()
150        {
151            r = r.header("anthropic-beta", beta);
152        }
153
154        for (k, v) in &self.config.extra_headers {
155            r = r.header(k.as_str(), v.as_str());
156        }
157
158        r
159    }
160
161    fn prepare_request_with_auth(
162        &self,
163        mut request: CreateMessageRequest,
164        auth: &AuthMethod,
165    ) -> CreateMessageRequest {
166        if let AuthMethod::OAuth { .. } = auth {
167            use crate::prompts::CLI_IDENTITY;
168
169            let mut blocks = vec![crate::types::SystemBlock::uncached(CLI_IDENTITY)];
170
171            match &request.system {
172                Some(crate::types::SystemPrompt::Text(existing)) if !existing.is_empty() => {
173                    if !existing.starts_with(CLI_IDENTITY) {
174                        blocks.push(crate::types::SystemBlock::uncached(existing));
175                    }
176                }
177                Some(crate::types::SystemPrompt::Blocks(existing_blocks))
178                    if !existing_blocks.is_empty() =>
179                {
180                    for block in existing_blocks {
181                        if block.text != CLI_IDENTITY {
182                            blocks.push(block.clone());
183                        }
184                    }
185                }
186                _ => {}
187            }
188
189            request.system = Some(crate::types::SystemPrompt::Blocks(blocks));
190        }
191        request
192    }
193
194    async fn do_refresh(&self) -> Result<()> {
195        if let Some(ref provider) = self.credential_provider {
196            let new_credential = provider.refresh().await?;
197            self.auth.write().await.update_token(&new_credential);
198        }
199        Ok(())
200    }
201
202    pub fn credential_provider(&self) -> Option<&Arc<dyn CredentialProvider>> {
203        self.credential_provider.as_ref()
204    }
205}
206
207#[async_trait]
208impl ProviderAdapter for AnthropicAdapter {
209    fn config(&self) -> &ProviderConfig {
210        &self.config
211    }
212
213    fn name(&self) -> &'static str {
214        "anthropic"
215    }
216
217    async fn build_url(&self, _model: &str, _stream: bool) -> String {
218        let auth = self.auth.read().await;
219        self.build_endpoint_url(&auth, "/v1/messages")
220    }
221
222    async fn transform_request(&self, request: CreateMessageRequest) -> Result<serde_json::Value> {
223        let auth = self.auth.read().await;
224        let prepared = self.prepare_request_with_auth(request, &auth);
225        serde_json::to_value(&prepared).map_err(|e| Error::InvalidRequest(e.to_string()))
226    }
227
228    fn transform_response(&self, response: serde_json::Value) -> Result<ApiResponse> {
229        serde_json::from_value(response).map_err(|e| Error::Parse(e.to_string()))
230    }
231
232    async fn apply_auth_headers(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
233        let auth = self.auth.read().await;
234        self.build_headers(req, &auth)
235    }
236
237    async fn send(
238        &self,
239        http: &reqwest::Client,
240        request: CreateMessageRequest,
241    ) -> Result<ApiResponse> {
242        let (url, body) = {
243            let auth = self.auth.read().await;
244            let url = self.build_endpoint_url(&auth, "/v1/messages");
245            let prepared = self.prepare_request_with_auth(request, &auth);
246            (url, serde_json::to_value(&prepared)?)
247        };
248
249        let response = self
250            .apply_auth_headers(http.post(&url))
251            .await
252            .json(&body)
253            .send()
254            .await?;
255
256        if !response.status().is_success() {
257            let status = response.status().as_u16();
258            let error: ErrorResponse = response.json().await?;
259            return Err(error.into_error(status));
260        }
261
262        let json: serde_json::Value = response.json().await?;
263        self.transform_response(json)
264    }
265
266    async fn send_stream(
267        &self,
268        http: &reqwest::Client,
269        mut request: CreateMessageRequest,
270    ) -> Result<reqwest::Response> {
271        request.stream = Some(true);
272
273        let (url, body) = {
274            let auth = self.auth.read().await;
275            let url = self.build_endpoint_url(&auth, "/v1/messages");
276            let prepared = self.prepare_request_with_auth(request, &auth);
277            (url, serde_json::to_value(&prepared)?)
278        };
279
280        let response = self
281            .apply_auth_headers(http.post(&url))
282            .await
283            .json(&body)
284            .send()
285            .await?;
286
287        if !response.status().is_success() {
288            let status = response.status().as_u16();
289            let error: ErrorResponse = response.json().await?;
290            return Err(error.into_error(status));
291        }
292
293        Ok(response)
294    }
295
296    fn supports_credential_refresh(&self) -> bool {
297        self.credential_provider
298            .as_ref()
299            .is_some_and(|p| p.supports_refresh())
300    }
301
302    async fn ensure_fresh_credentials(&self) -> Result<()> {
303        if let Some(ref provider) = self.credential_provider {
304            let current = provider.resolve().await?;
305            if current.needs_refresh() && provider.supports_refresh() {
306                let new_cred = provider.refresh().await?;
307                self.auth.write().await.update_token(&new_cred);
308            }
309        }
310        Ok(())
311    }
312
313    async fn refresh_credentials(&self) -> Result<()> {
314        self.do_refresh().await
315    }
316
317    async fn count_tokens(
318        &self,
319        http: &reqwest::Client,
320        request: CountTokensRequest,
321    ) -> Result<CountTokensResponse> {
322        let (url, body) = {
323            let auth = self.auth.read().await;
324            let url = self.build_endpoint_url(&auth, "/v1/messages/count_tokens");
325            (url, serde_json::to_value(&request)?)
326        };
327
328        let response = self
329            .apply_auth_headers(http.post(&url))
330            .await
331            .json(&body)
332            .send()
333            .await?;
334
335        if !response.status().is_success() {
336            let status = response.status().as_u16();
337            let error: ErrorResponse = response.json().await?;
338            return Err(error.into_error(status));
339        }
340
341        Ok(response.json().await?)
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::client::adapter::{BetaConfig, BetaFeature, ModelConfig};
349    use crate::types::Message;
350
351    #[tokio::test]
352    async fn test_build_url() {
353        let adapter = AnthropicAdapter::new(ProviderConfig::new(ModelConfig::anthropic()));
354        let url = adapter.build_url("claude-sonnet-4-5", false).await;
355        assert!(url.contains("/v1/messages"));
356    }
357
358    #[tokio::test]
359    async fn test_transform_request() {
360        let adapter = AnthropicAdapter::new(ProviderConfig::new(ModelConfig::anthropic()));
361        let request = CreateMessageRequest::new("claude-sonnet-4-5", vec![Message::user("Hello")]);
362        let body = adapter.transform_request(request).await.unwrap();
363        assert!(body.get("model").is_some());
364        assert!(body.get("messages").is_some());
365    }
366
367    #[tokio::test]
368    async fn test_oauth_url_params() {
369        let credential = Credential::oauth("test-token");
370        let adapter = AnthropicAdapter::from_credential(
371            ProviderConfig::new(ModelConfig::anthropic()),
372            &credential,
373            None,
374        );
375        let url = adapter.build_url("model", false).await;
376        assert!(url.contains("beta=true"));
377    }
378
379    #[tokio::test]
380    async fn test_oauth_system_prompt() {
381        let credential = Credential::oauth("test-token");
382        let adapter = AnthropicAdapter::from_credential(
383            ProviderConfig::new(ModelConfig::anthropic()),
384            &credential,
385            None,
386        );
387        let request = CreateMessageRequest::new("model", vec![Message::user("Hi")]);
388        let body = adapter.transform_request(request).await.unwrap();
389        let system_blocks = body
390            .get("system")
391            .and_then(|v| v.as_array())
392            .expect("OAuth should produce system blocks");
393        let first_text = system_blocks[0]
394            .get("text")
395            .and_then(|v| v.as_str())
396            .unwrap_or("");
397        assert!(first_text.contains("Claude Code"));
398    }
399
400    #[test]
401    fn test_api_key_with_beta() {
402        let config = ProviderConfig::new(ModelConfig::anthropic())
403            .with_beta(BetaFeature::InterleavedThinking)
404            .with_beta(BetaFeature::ContextManagement);
405
406        let adapter = AnthropicAdapter::new(config);
407        assert!(adapter.config.beta.has(BetaFeature::InterleavedThinking));
408        assert!(adapter.config.beta.has(BetaFeature::ContextManagement));
409    }
410
411    #[test]
412    fn test_api_key_with_custom_beta() {
413        let beta = BetaConfig::new().with_custom("new-feature-2026-01-01");
414        let config = ProviderConfig::new(ModelConfig::anthropic()).with_beta_config(beta);
415
416        let adapter = AnthropicAdapter::new(config);
417        let header = adapter.config.beta.header_value().unwrap();
418        assert!(header.contains("new-feature-2026-01-01"));
419    }
420
421    #[tokio::test]
422    async fn test_oauth_prepends_cli_identity_to_system_prompt() {
423        let credential = Credential::oauth("test-token");
424        let adapter = AnthropicAdapter::from_credential(
425            ProviderConfig::new(ModelConfig::anthropic()),
426            &credential,
427            None,
428        );
429
430        let request = CreateMessageRequest::new("model", vec![Message::user("Hi")])
431            .with_system("Custom user system prompt");
432
433        let body = adapter.transform_request(request).await.unwrap();
434        let system_blocks = body
435            .get("system")
436            .and_then(|v| v.as_array())
437            .expect("OAuth should produce system blocks");
438
439        assert!(system_blocks.len() >= 2, "Should have at least 2 blocks");
440
441        let first_text = system_blocks[0]
442            .get("text")
443            .and_then(|v| v.as_str())
444            .unwrap_or("");
445        assert!(
446            first_text.starts_with("You are Claude Code"),
447            "First block should be Claude Code identity: {}",
448            first_text
449        );
450
451        let second_text = system_blocks[1]
452            .get("text")
453            .and_then(|v| v.as_str())
454            .unwrap_or("");
455        assert_eq!(
456            second_text, "Custom user system prompt",
457            "Second block should preserve original"
458        );
459    }
460
461    #[tokio::test]
462    async fn test_api_key_does_not_modify_system_prompt() {
463        let adapter = AnthropicAdapter::new(ProviderConfig::new(ModelConfig::anthropic()))
464            .with_api_key("sk-test");
465
466        // Create request with existing system prompt
467        let request = CreateMessageRequest::new("model", vec![Message::user("Hi")])
468            .with_system("Custom user system prompt");
469
470        let body = adapter.transform_request(request).await.unwrap();
471        let system = body.get("system").and_then(|v| v.as_str()).unwrap_or("");
472
473        // For API key auth, system prompt should be unchanged
474        assert_eq!(
475            system, "Custom user system prompt",
476            "API key auth should not modify system prompt"
477        );
478        // Should NOT contain CLI identity
479        assert!(
480            !system.contains("Claude Code"),
481            "API key auth should not add CLI identity: {}",
482            system
483        );
484    }
485}