claude_agent/client/adapter/
anthropic.rs1use 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 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 assert_eq!(
475 system, "Custom user system prompt",
476 "API key auth should not modify system prompt"
477 );
478 assert!(
480 !system.contains("Claude Code"),
481 "API key auth should not add CLI identity: {}",
482 system
483 );
484 }
485}