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}
49
50fn env_trimmed(name: &str) -> Option<String> {
52 std::env::var(name)
53 .ok()
54 .map(|v| v.trim().to_string())
55 .filter(|v| !v.is_empty())
56}
57
58impl Default for AnthropicConfig {
59 fn default() -> Self {
60 let api_key = env_trimmed("ANTHROPIC_API_KEY").map(SecretString::from);
61 let bearer = env_trimmed("ANTHROPIC_AUTH_TOKEN").map(SecretString::from);
62 let api_base =
63 env_trimmed("ANTHROPIC_BASE_URL").unwrap_or_else(|| ANTHROPIC_DEFAULT_BASE.into());
64
65 let auth = match (api_key, bearer) {
66 (Some(k), Some(t)) => AnthropicAuth::Both {
67 api_key: k,
68 bearer: t,
69 },
70 (Some(k), None) => AnthropicAuth::ApiKey(k),
71 (None, Some(t)) => AnthropicAuth::Bearer(t),
72 _ => AnthropicAuth::None,
73 };
74
75 Self {
76 api_base,
77 version: ANTHROPIC_VERSION.into(),
78 auth,
79 beta: vec![],
80 }
81 }
82}
83
84impl AnthropicConfig {
85 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
100 pub fn with_api_base(mut self, base: impl Into<String>) -> Self {
101 self.api_base = base.into();
102 self
103 }
104
105 #[must_use]
109 pub fn with_version(mut self, v: impl Into<String>) -> Self {
110 self.version = v.into();
111 self
112 }
113
114 #[must_use]
118 pub fn with_api_key(mut self, k: impl Into<String>) -> Self {
119 self.auth = AnthropicAuth::ApiKey(SecretString::from(k.into()));
120 self
121 }
122
123 #[must_use]
127 pub fn with_bearer(mut self, t: impl Into<String>) -> Self {
128 self.auth = AnthropicAuth::Bearer(SecretString::from(t.into()));
129 self
130 }
131
132 #[must_use]
137 pub fn with_both(mut self, api_key: impl Into<String>, bearer: impl Into<String>) -> Self {
138 self.auth = AnthropicAuth::Both {
139 api_key: SecretString::from(api_key.into()),
140 bearer: SecretString::from(bearer.into()),
141 };
142 self
143 }
144
145 #[must_use]
149 pub fn with_beta<I, S>(mut self, beta: I) -> Self
150 where
151 I: IntoIterator<Item = S>,
152 S: Into<String>,
153 {
154 self.beta = beta.into_iter().map(Into::into).collect();
155 self
156 }
157
158 #[must_use]
160 pub fn api_base(&self) -> &str {
161 &self.api_base
162 }
163
164 pub fn validate_auth(&self) -> Result<(), crate::error::AnthropicError> {
171 use crate::error::AnthropicError;
172
173 match &self.auth {
174 AnthropicAuth::ApiKey(k) if !k.expose_secret().trim().is_empty() => Ok(()),
175 AnthropicAuth::Bearer(t) if !t.expose_secret().trim().is_empty() => Ok(()),
176 AnthropicAuth::Both { api_key, bearer }
177 if !api_key.expose_secret().trim().is_empty()
178 && !bearer.expose_secret().trim().is_empty() =>
179 {
180 Ok(())
181 }
182 _ => Err(AnthropicError::Config(
183 "Missing Anthropic credentials: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN"
184 .into(),
185 )),
186 }
187 }
188
189 #[must_use]
193 pub fn with_beta_features<I: IntoIterator<Item = BetaFeature>>(mut self, features: I) -> Self {
194 self.beta = features.into_iter().map(Into::<String>::into).collect();
195 self
196 }
197}
198
199pub trait Config: Send + Sync {
203 fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError>;
209
210 fn url(&self, path: &str) -> String;
212
213 fn query(&self) -> Vec<(&str, &str)>;
215
216 fn validate_auth(&self) -> Result<(), crate::error::AnthropicError>;
222}
223
224impl Config for AnthropicConfig {
225 fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError> {
226 use crate::error::AnthropicError;
227
228 let mut h = HeaderMap::new();
229
230 h.insert(
231 HDR_ANTHROPIC_VERSION,
232 HeaderValue::from_str(&self.version)
233 .map_err(|_| AnthropicError::Config("Invalid anthropic-version header".into()))?,
234 );
235
236 if !self.beta.is_empty() {
237 let v = self.beta.join(",");
238 h.insert(
239 HDR_ANTHROPIC_BETA,
240 HeaderValue::from_str(&v)
241 .map_err(|_| AnthropicError::Config("Invalid anthropic-beta header".into()))?,
242 );
243 }
244
245 match &self.auth {
246 AnthropicAuth::ApiKey(k) => {
247 h.insert(
248 HDR_X_API_KEY,
249 HeaderValue::from_str(k.expose_secret())
250 .map_err(|_| AnthropicError::Config("Invalid x-api-key value".into()))?,
251 );
252 }
253 AnthropicAuth::Bearer(t) => {
254 let v = format!("Bearer {}", t.expose_secret());
255 h.insert(
256 AUTHORIZATION,
257 HeaderValue::from_str(&v).map_err(|_| {
258 AnthropicError::Config("Invalid Authorization header".into())
259 })?,
260 );
261 }
262 AnthropicAuth::Both { api_key, bearer } => {
263 h.insert(
264 HDR_X_API_KEY,
265 HeaderValue::from_str(api_key.expose_secret())
266 .map_err(|_| AnthropicError::Config("Invalid x-api-key value".into()))?,
267 );
268 let v = format!("Bearer {}", bearer.expose_secret());
269 h.insert(
270 AUTHORIZATION,
271 HeaderValue::from_str(&v).map_err(|_| {
272 AnthropicError::Config("Invalid Authorization header".into())
273 })?,
274 );
275 }
276 AnthropicAuth::None => {}
277 }
278
279 Ok(h)
280 }
281
282 fn url(&self, path: &str) -> String {
283 format!("{}{}", self.api_base, path)
284 }
285
286 fn query(&self) -> Vec<(&str, &str)> {
287 vec![]
288 }
289
290 fn validate_auth(&self) -> Result<(), crate::error::AnthropicError> {
291 self.validate_auth()
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum BetaFeature {
300 PromptCaching20240731,
302 ExtendedCacheTtl20250411,
304 TokenCounting20241101,
306 StructuredOutputs20250917,
308 StructuredOutputs20251113,
310 StructuredOutputsLatest,
312 Other(String),
314}
315
316impl From<BetaFeature> for String {
317 fn from(b: BetaFeature) -> Self {
318 match b {
319 BetaFeature::PromptCaching20240731 => "prompt-caching-2024-07-31".into(),
320 BetaFeature::ExtendedCacheTtl20250411 => "extended-cache-ttl-2025-04-11".into(),
321 BetaFeature::TokenCounting20241101 => "token-counting-2024-11-01".into(),
322 BetaFeature::StructuredOutputs20250917 => "structured-outputs-2025-09-17".into(),
323 BetaFeature::StructuredOutputs20251113 | BetaFeature::StructuredOutputsLatest => {
324 "structured-outputs-2025-11-13".into()
325 }
326 BetaFeature::Other(s) => s,
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_default_headers_exist() {
337 let cfg = AnthropicConfig::new();
338 let h = cfg.headers().unwrap();
339 assert!(h.contains_key(super::HDR_ANTHROPIC_VERSION));
340 }
341
342 #[test]
343 fn auth_api_key_header() {
344 let cfg = AnthropicConfig::new().with_api_key("k123");
345 let h = cfg.headers().unwrap();
346 assert!(h.contains_key(HDR_X_API_KEY));
347 assert!(!h.contains_key(reqwest::header::AUTHORIZATION));
348 }
349
350 #[test]
351 fn auth_bearer_header() {
352 let cfg = AnthropicConfig::new().with_bearer("t123");
353 let h = cfg.headers().unwrap();
354 assert!(h.contains_key(reqwest::header::AUTHORIZATION));
355 assert!(!h.contains_key(HDR_X_API_KEY));
356 }
357
358 #[test]
359 fn auth_both_headers() {
360 let cfg = AnthropicConfig::new().with_both("k123", "t123");
361 let h = cfg.headers().unwrap();
362 assert!(h.contains_key(HDR_X_API_KEY));
363 assert!(h.contains_key(reqwest::header::AUTHORIZATION));
364 }
365
366 #[test]
367 fn beta_header_join() {
368 let cfg = AnthropicConfig::new().with_beta(vec!["a", "b"]);
369 let h = cfg.headers().unwrap();
370 let v = h.get(HDR_ANTHROPIC_BETA).unwrap().to_str().unwrap();
371 assert_eq!(v, "a,b");
372 }
373
374 #[test]
375 fn invalid_header_values_error() {
376 let cfg = AnthropicConfig::new().with_api_key("bad\nkey");
377 match cfg.headers() {
378 Err(crate::error::AnthropicError::Config(msg)) => assert!(msg.contains("x-api-key")),
379 other => panic!("Expected Config error, got {other:?}"),
380 }
381 }
382
383 #[test]
384 fn validate_auth_missing() {
385 let cfg = AnthropicConfig {
386 api_base: "test".into(),
387 version: "test".into(),
388 auth: AnthropicAuth::None,
389 beta: vec![],
390 };
391 assert!(cfg.validate_auth().is_err());
392 }
393
394 #[test]
395 fn debug_output_redacts_api_key() {
396 let cfg = AnthropicConfig::new().with_api_key("super-secret-key-12345");
397 let debug_str = format!("{cfg:?}");
398
399 assert!(
400 !debug_str.contains("super-secret-key-12345"),
401 "Debug output should not contain the API key"
402 );
403 assert!(
405 debug_str.contains("[REDACTED]"),
406 "Debug output should contain '[REDACTED]', got: {debug_str}"
407 );
408 }
409
410 #[test]
411 fn debug_output_redacts_bearer() {
412 let cfg = AnthropicConfig::new().with_bearer("super-secret-token-12345");
413 let debug_str = format!("{cfg:?}");
414
415 assert!(
416 !debug_str.contains("super-secret-token-12345"),
417 "Debug output should not contain the bearer token"
418 );
419 assert!(
421 debug_str.contains("[REDACTED]"),
422 "Debug output should contain '[REDACTED]', got: {debug_str}"
423 );
424 }
425
426 #[test]
427 fn debug_output_redacts_both() {
428 let cfg = AnthropicConfig::new().with_both("secret-api-key", "secret-bearer-token");
429 let debug_str = format!("{cfg:?}");
430
431 assert!(
432 !debug_str.contains("secret-api-key"),
433 "Debug output should not contain the API key"
434 );
435 assert!(
436 !debug_str.contains("secret-bearer-token"),
437 "Debug output should not contain the bearer token"
438 );
439 assert!(
441 debug_str.contains("[REDACTED]"),
442 "Debug output should contain '[REDACTED]', got: {debug_str}"
443 );
444 }
445
446 #[test]
447 fn validate_auth_rejects_empty_api_key() {
448 let cfg = AnthropicConfig::new().with_api_key("");
449 assert!(cfg.validate_auth().is_err());
450
451 let cfg = AnthropicConfig::new().with_api_key(" ");
452 assert!(cfg.validate_auth().is_err());
453
454 let cfg = AnthropicConfig::new().with_api_key("\n");
455 assert!(cfg.validate_auth().is_err());
456 }
457
458 #[test]
459 fn validate_auth_rejects_empty_bearer() {
460 let cfg = AnthropicConfig::new().with_bearer("");
461 assert!(cfg.validate_auth().is_err());
462
463 let cfg = AnthropicConfig::new().with_bearer(" ");
464 assert!(cfg.validate_auth().is_err());
465 }
466
467 #[test]
468 fn validate_auth_rejects_empty_both() {
469 let cfg = AnthropicConfig::new().with_both("", "");
471 assert!(cfg.validate_auth().is_err());
472
473 let cfg = AnthropicConfig::new().with_both("", "valid-token");
475 assert!(cfg.validate_auth().is_err());
476
477 let cfg = AnthropicConfig::new().with_both("valid-key", "");
479 assert!(cfg.validate_auth().is_err());
480
481 let cfg = AnthropicConfig::new().with_both(" ", " ");
483 assert!(cfg.validate_auth().is_err());
484 }
485
486 #[test]
487 fn validate_auth_accepts_valid_credentials() {
488 let cfg = AnthropicConfig::new().with_api_key("valid-key");
489 assert!(cfg.validate_auth().is_ok());
490
491 let cfg = AnthropicConfig::new().with_bearer("valid-token");
492 assert!(cfg.validate_auth().is_ok());
493
494 let cfg = AnthropicConfig::new().with_both("valid-key", "valid-token");
495 assert!(cfg.validate_auth().is_ok());
496
497 let cfg = AnthropicConfig::new().with_api_key(" valid-key ");
499 assert!(cfg.validate_auth().is_ok());
500 }
501}