1use reqwest::header::AUTHORIZATION;
2use reqwest::header::HeaderMap;
3use reqwest::header::HeaderValue;
4use secrecy::ExposeSecret;
5use secrecy::SecretString;
6use serde::Deserialize;
7
8pub const ANTHROPIC_DEFAULT_BASE: &str = "https://api.anthropic.com";
10pub const ANTHROPIC_VERSION: &str = "2023-06-01";
12pub const HDR_ANTHROPIC_VERSION: &str = "anthropic-version";
14pub const HDR_ANTHROPIC_BETA: &str = "anthropic-beta";
16pub const HDR_X_API_KEY: &str = "x-api-key";
18
19#[derive(Clone, Debug)]
23pub enum AnthropicAuth {
24 ApiKey(SecretString),
26 Bearer(SecretString),
28 Both {
30 api_key: SecretString,
32 bearer: SecretString,
34 },
35 None,
37}
38
39#[derive(Clone, Debug, Deserialize)]
43#[serde(default)]
44pub struct AnthropicConfig {
45 api_base: String,
46 version: String,
47 #[serde(skip)]
48 auth: AnthropicAuth,
49 #[serde(skip)]
50 beta: Vec<String>,
51 #[serde(skip)]
53 dangerously_skip_auth: bool,
54}
55
56fn env_trimmed(name: &str) -> Option<String> {
58 std::env::var(name)
59 .ok()
60 .map(|v| v.trim().to_string())
61 .filter(|v| !v.is_empty())
62}
63
64impl Default for AnthropicConfig {
65 fn default() -> Self {
66 let api_key = env_trimmed("ANTHROPIC_API_KEY").map(SecretString::from);
67 let bearer = env_trimmed("ANTHROPIC_AUTH_TOKEN").map(SecretString::from);
68 let api_base =
69 env_trimmed("ANTHROPIC_BASE_URL").unwrap_or_else(|| ANTHROPIC_DEFAULT_BASE.into());
70
71 let auth = match (api_key, bearer) {
72 (Some(k), Some(t)) => AnthropicAuth::Both {
73 api_key: k,
74 bearer: t,
75 },
76 (Some(k), None) => AnthropicAuth::ApiKey(k),
77 (None, Some(t)) => AnthropicAuth::Bearer(t),
78 _ => AnthropicAuth::None,
79 };
80
81 Self {
82 api_base,
83 version: ANTHROPIC_VERSION.into(),
84 auth,
85 beta: vec![],
86 dangerously_skip_auth: false,
87 }
88 }
89}
90
91impl AnthropicConfig {
92 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use]
107 pub fn with_api_base(mut self, base: impl Into<String>) -> Self {
108 self.api_base = base.into();
109 self
110 }
111
112 #[must_use]
116 pub fn with_version(mut self, v: impl Into<String>) -> Self {
117 self.version = v.into();
118 self
119 }
120
121 #[must_use]
125 pub fn with_api_key(mut self, k: impl Into<String>) -> Self {
126 self.auth = AnthropicAuth::ApiKey(SecretString::from(k.into()));
127 self
128 }
129
130 #[must_use]
134 pub fn with_bearer(mut self, t: impl Into<String>) -> Self {
135 self.auth = AnthropicAuth::Bearer(SecretString::from(t.into()));
136 self
137 }
138
139 #[must_use]
144 pub fn with_both(mut self, api_key: impl Into<String>, bearer: impl Into<String>) -> Self {
145 self.auth = AnthropicAuth::Both {
146 api_key: SecretString::from(api_key.into()),
147 bearer: SecretString::from(bearer.into()),
148 };
149 self
150 }
151
152 #[must_use]
156 pub fn with_beta<I, S>(mut self, beta: I) -> Self
157 where
158 I: IntoIterator<Item = S>,
159 S: Into<String>,
160 {
161 self.beta = beta.into_iter().map(Into::into).collect();
162 self
163 }
164
165 #[must_use]
170 pub fn dangerously_skip_auth(mut self) -> Self {
171 self.dangerously_skip_auth = true;
172 self.auth = AnthropicAuth::None;
173 self
174 }
175
176 #[must_use]
178 pub fn api_base(&self) -> &str {
179 &self.api_base
180 }
181
182 pub fn validate_auth(&self) -> Result<(), crate::error::AnthropicError> {
190 use crate::error::AnthropicError;
191
192 if self.dangerously_skip_auth {
194 return Ok(());
195 }
196
197 match &self.auth {
198 AnthropicAuth::ApiKey(k) if !k.expose_secret().trim().is_empty() => Ok(()),
199 AnthropicAuth::Bearer(t) if !t.expose_secret().trim().is_empty() => Ok(()),
200 AnthropicAuth::Both { api_key, bearer }
201 if !api_key.expose_secret().trim().is_empty()
202 && !bearer.expose_secret().trim().is_empty() =>
203 {
204 Ok(())
205 }
206 _ => Err(AnthropicError::Config(
207 "Missing Anthropic credentials: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN"
208 .into(),
209 )),
210 }
211 }
212
213 #[must_use]
217 pub fn with_beta_features<I: IntoIterator<Item = BetaFeature>>(mut self, features: I) -> Self {
218 self.beta = features.into_iter().map(Into::<String>::into).collect();
219 self
220 }
221}
222
223pub trait Config: Send + Sync {
227 fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError>;
233
234 fn url(&self, path: &str) -> String;
236
237 fn query(&self) -> Vec<(&str, &str)>;
239
240 fn validate_auth(&self) -> Result<(), crate::error::AnthropicError>;
246}
247
248impl Config for AnthropicConfig {
249 fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError> {
250 use crate::error::AnthropicError;
251
252 let mut h = HeaderMap::new();
253
254 h.insert(
255 HDR_ANTHROPIC_VERSION,
256 HeaderValue::from_str(&self.version)
257 .map_err(|_| AnthropicError::Config("Invalid anthropic-version header".into()))?,
258 );
259
260 if !self.beta.is_empty() {
261 let v = self.beta.join(",");
262 h.insert(
263 HDR_ANTHROPIC_BETA,
264 HeaderValue::from_str(&v)
265 .map_err(|_| AnthropicError::Config("Invalid anthropic-beta header".into()))?,
266 );
267 }
268
269 if !self.dangerously_skip_auth {
271 match &self.auth {
272 AnthropicAuth::ApiKey(k) => {
273 h.insert(
274 HDR_X_API_KEY,
275 HeaderValue::from_str(k.expose_secret()).map_err(|_| {
276 AnthropicError::Config("Invalid x-api-key value".into())
277 })?,
278 );
279 }
280 AnthropicAuth::Bearer(t) => {
281 let v = format!("Bearer {}", t.expose_secret());
282 h.insert(
283 AUTHORIZATION,
284 HeaderValue::from_str(&v).map_err(|_| {
285 AnthropicError::Config("Invalid Authorization header".into())
286 })?,
287 );
288 }
289 AnthropicAuth::Both { api_key, bearer } => {
290 h.insert(
291 HDR_X_API_KEY,
292 HeaderValue::from_str(api_key.expose_secret()).map_err(|_| {
293 AnthropicError::Config("Invalid x-api-key value".into())
294 })?,
295 );
296 let v = format!("Bearer {}", bearer.expose_secret());
297 h.insert(
298 AUTHORIZATION,
299 HeaderValue::from_str(&v).map_err(|_| {
300 AnthropicError::Config("Invalid Authorization header".into())
301 })?,
302 );
303 }
304 AnthropicAuth::None => {}
305 }
306 }
307
308 Ok(h)
309 }
310
311 fn url(&self, path: &str) -> String {
312 format!("{}{}", self.api_base, path)
313 }
314
315 fn query(&self) -> Vec<(&str, &str)> {
316 vec![]
317 }
318
319 fn validate_auth(&self) -> Result<(), crate::error::AnthropicError> {
320 self.validate_auth()
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
328pub enum BetaFeature {
329 PromptCaching20240731,
331 ExtendedCacheTtl20250411,
333 TokenCounting20241101,
335 StructuredOutputs20250917,
337 StructuredOutputs20251113,
339 StructuredOutputsLatest,
341 Other(String),
343}
344
345impl From<BetaFeature> for String {
346 fn from(b: BetaFeature) -> Self {
347 match b {
348 BetaFeature::PromptCaching20240731 => "prompt-caching-2024-07-31".into(),
349 BetaFeature::ExtendedCacheTtl20250411 => "extended-cache-ttl-2025-04-11".into(),
350 BetaFeature::TokenCounting20241101 => "token-counting-2024-11-01".into(),
351 BetaFeature::StructuredOutputs20250917 => "structured-outputs-2025-09-17".into(),
352 BetaFeature::StructuredOutputs20251113 | BetaFeature::StructuredOutputsLatest => {
353 "structured-outputs-2025-11-13".into()
354 }
355 BetaFeature::Other(s) => s,
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_default_headers_exist() {
366 let cfg = AnthropicConfig::new();
367 let h = cfg.headers().unwrap();
368 assert!(h.contains_key(super::HDR_ANTHROPIC_VERSION));
369 }
370
371 #[test]
372 fn auth_api_key_header() {
373 let cfg = AnthropicConfig::new().with_api_key("k123");
374 let h = cfg.headers().unwrap();
375 assert!(h.contains_key(HDR_X_API_KEY));
376 assert!(!h.contains_key(reqwest::header::AUTHORIZATION));
377 }
378
379 #[test]
380 fn auth_bearer_header() {
381 let cfg = AnthropicConfig::new().with_bearer("t123");
382 let h = cfg.headers().unwrap();
383 assert!(h.contains_key(reqwest::header::AUTHORIZATION));
384 assert!(!h.contains_key(HDR_X_API_KEY));
385 }
386
387 #[test]
388 fn auth_both_headers() {
389 let cfg = AnthropicConfig::new().with_both("k123", "t123");
390 let h = cfg.headers().unwrap();
391 assert!(h.contains_key(HDR_X_API_KEY));
392 assert!(h.contains_key(reqwest::header::AUTHORIZATION));
393 }
394
395 #[test]
396 fn beta_header_join() {
397 let cfg = AnthropicConfig::new().with_beta(vec!["a", "b"]);
398 let h = cfg.headers().unwrap();
399 let v = h.get(HDR_ANTHROPIC_BETA).unwrap().to_str().unwrap();
400 assert_eq!(v, "a,b");
401 }
402
403 #[test]
404 fn invalid_header_values_error() {
405 let cfg = AnthropicConfig::new().with_api_key("bad\nkey");
406 match cfg.headers() {
407 Err(crate::error::AnthropicError::Config(msg)) => assert!(msg.contains("x-api-key")),
408 other => panic!("Expected Config error, got {other:?}"),
409 }
410 }
411
412 #[test]
413 fn validate_auth_missing() {
414 let cfg = AnthropicConfig {
415 api_base: "test".into(),
416 version: "test".into(),
417 auth: AnthropicAuth::None,
418 beta: vec![],
419 dangerously_skip_auth: false,
420 };
421 assert!(cfg.validate_auth().is_err());
422 }
423
424 #[test]
425 fn debug_output_redacts_api_key() {
426 let cfg = AnthropicConfig::new().with_api_key("super-secret-key-12345");
427 let debug_str = format!("{cfg:?}");
428
429 assert!(
430 !debug_str.contains("super-secret-key-12345"),
431 "Debug output should not contain the API key"
432 );
433 assert!(
435 debug_str.contains("[REDACTED]"),
436 "Debug output should contain '[REDACTED]', got: {debug_str}"
437 );
438 }
439
440 #[test]
441 fn debug_output_redacts_bearer() {
442 let cfg = AnthropicConfig::new().with_bearer("super-secret-token-12345");
443 let debug_str = format!("{cfg:?}");
444
445 assert!(
446 !debug_str.contains("super-secret-token-12345"),
447 "Debug output should not contain the bearer token"
448 );
449 assert!(
451 debug_str.contains("[REDACTED]"),
452 "Debug output should contain '[REDACTED]', got: {debug_str}"
453 );
454 }
455
456 #[test]
457 fn debug_output_redacts_both() {
458 let cfg = AnthropicConfig::new().with_both("secret-api-key", "secret-bearer-token");
459 let debug_str = format!("{cfg:?}");
460
461 assert!(
462 !debug_str.contains("secret-api-key"),
463 "Debug output should not contain the API key"
464 );
465 assert!(
466 !debug_str.contains("secret-bearer-token"),
467 "Debug output should not contain the bearer token"
468 );
469 assert!(
471 debug_str.contains("[REDACTED]"),
472 "Debug output should contain '[REDACTED]', got: {debug_str}"
473 );
474 }
475
476 #[test]
477 fn validate_auth_rejects_empty_api_key() {
478 let cfg = AnthropicConfig::new().with_api_key("");
479 assert!(cfg.validate_auth().is_err());
480
481 let cfg = AnthropicConfig::new().with_api_key(" ");
482 assert!(cfg.validate_auth().is_err());
483
484 let cfg = AnthropicConfig::new().with_api_key("\n");
485 assert!(cfg.validate_auth().is_err());
486 }
487
488 #[test]
489 fn validate_auth_rejects_empty_bearer() {
490 let cfg = AnthropicConfig::new().with_bearer("");
491 assert!(cfg.validate_auth().is_err());
492
493 let cfg = AnthropicConfig::new().with_bearer(" ");
494 assert!(cfg.validate_auth().is_err());
495 }
496
497 #[test]
498 fn validate_auth_rejects_empty_both() {
499 let cfg = AnthropicConfig::new().with_both("", "");
501 assert!(cfg.validate_auth().is_err());
502
503 let cfg = AnthropicConfig::new().with_both("", "valid-token");
505 assert!(cfg.validate_auth().is_err());
506
507 let cfg = AnthropicConfig::new().with_both("valid-key", "");
509 assert!(cfg.validate_auth().is_err());
510
511 let cfg = AnthropicConfig::new().with_both(" ", " ");
513 assert!(cfg.validate_auth().is_err());
514 }
515
516 #[test]
517 fn validate_auth_accepts_valid_credentials() {
518 let cfg = AnthropicConfig::new().with_api_key("valid-key");
519 assert!(cfg.validate_auth().is_ok());
520
521 let cfg = AnthropicConfig::new().with_bearer("valid-token");
522 assert!(cfg.validate_auth().is_ok());
523
524 let cfg = AnthropicConfig::new().with_both("valid-key", "valid-token");
525 assert!(cfg.validate_auth().is_ok());
526
527 let cfg = AnthropicConfig::new().with_api_key(" valid-key ");
529 assert!(cfg.validate_auth().is_ok());
530 }
531
532 #[test]
533 fn dangerously_skip_auth_bypasses_validation() {
534 let cfg_normal = AnthropicConfig {
536 api_base: "test".into(),
537 version: "test".into(),
538 auth: AnthropicAuth::None,
539 beta: vec![],
540 dangerously_skip_auth: false,
541 };
542 assert!(cfg_normal.validate_auth().is_err());
543
544 let cfg_skip = AnthropicConfig::new().dangerously_skip_auth();
546 assert!(
547 cfg_skip.validate_auth().is_ok(),
548 "validate_auth must succeed when dangerously_skip_auth is set"
549 );
550 }
551
552 #[test]
553 fn dangerously_skip_auth_omits_headers() {
554 let cfg = AnthropicConfig::new()
556 .with_both("test-key", "test-token")
557 .dangerously_skip_auth();
558
559 let headers = cfg.headers().unwrap();
560
561 assert!(
563 !headers.contains_key(HDR_X_API_KEY),
564 "x-api-key must not be present when dangerously_skip_auth is set"
565 );
566 assert!(
567 !headers.contains_key(reqwest::header::AUTHORIZATION),
568 "Authorization must not be present when dangerously_skip_auth is set"
569 );
570
571 assert!(headers.contains_key(HDR_ANTHROPIC_VERSION));
573 }
574}