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