Skip to main content

anthropic_async/
config.rs

1use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
2use secrecy::{ExposeSecret, SecretString};
3use serde::Deserialize;
4
5/// Default Anthropic API base URL
6pub const ANTHROPIC_DEFAULT_BASE: &str = "https://api.anthropic.com";
7/// Default Anthropic API version
8pub const ANTHROPIC_VERSION: &str = "2023-06-01";
9/// Header name for Anthropic version
10pub const HDR_ANTHROPIC_VERSION: &str = "anthropic-version";
11/// Header name for Anthropic beta features
12pub const HDR_ANTHROPIC_BETA: &str = "anthropic-beta";
13/// Header name for API key authentication
14pub const HDR_X_API_KEY: &str = "x-api-key";
15
16/// Authentication method for Anthropic API
17///
18/// Debug output automatically redacts credentials via [`SecretString`].
19#[derive(Clone, Debug)]
20pub enum AnthropicAuth {
21    /// API key authentication
22    ApiKey(SecretString),
23    /// Bearer token authentication
24    Bearer(SecretString),
25    /// Both API key and bearer token authentication
26    Both {
27        /// API key for x-api-key header
28        api_key: SecretString,
29        /// Bearer token for Authorization header
30        bearer: SecretString,
31    },
32    /// No authentication configured
33    None,
34}
35
36/// Configuration for the Anthropic client
37///
38/// Debug output automatically redacts credentials via [`SecretString`] in [`AnthropicAuth`].
39#[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
50/// Helper to read and normalize an env var (trim + filter empty).
51fn 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    /// Creates a new configuration with default settings
86    ///
87    /// Attempts to read from environment variables:
88    /// - `ANTHROPIC_API_KEY` for API key authentication
89    /// - `ANTHROPIC_AUTH_TOKEN` for bearer token authentication
90    /// - `ANTHROPIC_BASE_URL` for custom API base URL (defaults to `https://api.anthropic.com`)
91    #[must_use]
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Sets the API base URL
97    ///
98    /// Default is `https://api.anthropic.com`
99    #[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    /// Sets the Anthropic API version
106    ///
107    /// Default is `2023-06-01`
108    #[must_use]
109    pub fn with_version(mut self, v: impl Into<String>) -> Self {
110        self.version = v.into();
111        self
112    }
113
114    /// Sets API key authentication
115    ///
116    /// This will use the `x-api-key` header for authentication.
117    #[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    /// Sets bearer token authentication
124    ///
125    /// This will use the `Authorization: Bearer` header for authentication.
126    #[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    /// Sets both API key and bearer token authentication
133    ///
134    /// This will send both the `x-api-key` and `Authorization: Bearer` headers.
135    /// This matches the behavior of the official Python SDK when both credentials are present.
136    #[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    /// Sets custom beta feature strings
146    ///
147    /// These will be sent in the `anthropic-beta` header as a comma-separated list.
148    #[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    /// Returns the configured API base URL
159    #[must_use]
160    pub fn api_base(&self) -> &str {
161        &self.api_base
162    }
163
164    /// Validates that authentication credentials are present and non-empty.
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if neither API key nor bearer token is configured,
169    /// or if the configured credentials are empty/whitespace-only.
170    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    /// Sets beta features using the `BetaFeature` enum
190    ///
191    /// This is a type-safe alternative to [`with_beta`](Self::with_beta).
192    #[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
199/// Configuration trait for the Anthropic client
200///
201/// Implement this trait to provide custom authentication and API configuration.
202pub trait Config: Send + Sync {
203    /// Returns HTTP headers to include in requests
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if header values contain invalid characters.
208    fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError>;
209
210    /// Constructs the full URL for an API endpoint
211    fn url(&self, path: &str) -> String;
212
213    /// Returns query parameters to include in requests
214    fn query(&self) -> Vec<(&str, &str)>;
215
216    /// Validates that authentication credentials are present.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if authentication is not properly configured.
221    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/// Known Anthropic beta features
296///
297/// See the [Anthropic API documentation](https://docs.anthropic.com/en/api) for details on each feature.
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub enum BetaFeature {
300    /// Prompt caching (2024-07-31)
301    PromptCaching20240731,
302    /// Extended cache TTL (2025-04-11)
303    ExtendedCacheTtl20250411,
304    /// Token counting (2024-11-01)
305    TokenCounting20241101,
306    /// Structured outputs (2025-09-17) — Python SDK version
307    StructuredOutputs20250917,
308    /// Structured outputs (2025-11-13) — TypeScript SDK version (recommended)
309    StructuredOutputs20251113,
310    /// Alias to the latest structured outputs beta (currently 2025-11-13)
311    StructuredOutputsLatest,
312    /// Custom beta feature string
313    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        // SecretString uses [REDACTED] format
404        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        // SecretString uses [REDACTED] format
420        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        // SecretString uses [REDACTED] format
440        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        // Both empty
470        let cfg = AnthropicConfig::new().with_both("", "");
471        assert!(cfg.validate_auth().is_err());
472
473        // API key empty, bearer valid
474        let cfg = AnthropicConfig::new().with_both("", "valid-token");
475        assert!(cfg.validate_auth().is_err());
476
477        // API key valid, bearer empty
478        let cfg = AnthropicConfig::new().with_both("valid-key", "");
479        assert!(cfg.validate_auth().is_err());
480
481        // Both whitespace
482        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        // Valid with leading/trailing whitespace (trimmed internally)
498        let cfg = AnthropicConfig::new().with_api_key("  valid-key  ");
499        assert!(cfg.validate_auth().is_ok());
500    }
501}