Skip to main content

anthropic_async/
config.rs

1use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
2use serde::Deserialize;
3
4/// Default Anthropic API base URL
5pub const ANTHROPIC_DEFAULT_BASE: &str = "https://api.anthropic.com";
6/// Default Anthropic API version
7pub const ANTHROPIC_VERSION: &str = "2023-06-01";
8/// Header name for Anthropic version
9pub const HDR_ANTHROPIC_VERSION: &str = "anthropic-version";
10/// Header name for Anthropic beta features
11pub const HDR_ANTHROPIC_BETA: &str = "anthropic-beta";
12/// Header name for API key authentication
13pub const HDR_X_API_KEY: &str = "x-api-key";
14
15/// Authentication method for Anthropic API
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum AnthropicAuth {
18    /// API key authentication
19    ApiKey(String),
20    /// Bearer token authentication
21    Bearer(String),
22    /// Both API key and bearer token authentication
23    Both {
24        /// API key for x-api-key header
25        api_key: String,
26        /// Bearer token for Authorization header
27        bearer: String,
28    },
29    /// No authentication configured
30    None,
31}
32
33/// Configuration for the Anthropic client
34#[derive(Debug, Clone, Deserialize)]
35#[serde(default)]
36pub struct AnthropicConfig {
37    api_base: String,
38    version: String,
39    #[serde(skip)]
40    auth: AnthropicAuth,
41    #[serde(skip)]
42    beta: Vec<String>,
43}
44
45impl Default for AnthropicConfig {
46    fn default() -> Self {
47        let api_key = std::env::var("ANTHROPIC_API_KEY").ok();
48        let bearer = std::env::var("ANTHROPIC_AUTH_TOKEN").ok();
49        let api_base =
50            std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| ANTHROPIC_DEFAULT_BASE.into());
51
52        let auth = match (api_key, bearer) {
53            (Some(k), Some(t)) => AnthropicAuth::Both {
54                api_key: k,
55                bearer: t,
56            },
57            (Some(k), None) => AnthropicAuth::ApiKey(k),
58            (None, Some(t)) => AnthropicAuth::Bearer(t),
59            _ => AnthropicAuth::None,
60        };
61
62        Self {
63            api_base,
64            version: ANTHROPIC_VERSION.into(),
65            auth,
66            beta: vec![],
67        }
68    }
69}
70
71impl AnthropicConfig {
72    /// Creates a new configuration with default settings
73    ///
74    /// Attempts to read from environment variables:
75    /// - `ANTHROPIC_API_KEY` for API key authentication
76    /// - `ANTHROPIC_AUTH_TOKEN` for bearer token authentication
77    /// - `ANTHROPIC_BASE_URL` for custom API base URL (defaults to `https://api.anthropic.com`)
78    #[must_use]
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Sets the API base URL
84    ///
85    /// Default is `https://api.anthropic.com`
86    #[must_use]
87    pub fn with_api_base(mut self, base: impl Into<String>) -> Self {
88        self.api_base = base.into();
89        self
90    }
91
92    /// Sets the Anthropic API version
93    ///
94    /// Default is `2023-06-01`
95    #[must_use]
96    pub fn with_version(mut self, v: impl Into<String>) -> Self {
97        self.version = v.into();
98        self
99    }
100
101    /// Sets API key authentication
102    ///
103    /// This will use the `x-api-key` header for authentication.
104    #[must_use]
105    pub fn with_api_key(mut self, k: impl Into<String>) -> Self {
106        self.auth = AnthropicAuth::ApiKey(k.into());
107        self
108    }
109
110    /// Sets bearer token authentication
111    ///
112    /// This will use the `Authorization: Bearer` header for authentication.
113    #[must_use]
114    pub fn with_bearer(mut self, t: impl Into<String>) -> Self {
115        self.auth = AnthropicAuth::Bearer(t.into());
116        self
117    }
118
119    /// Sets both API key and bearer token authentication
120    ///
121    /// This will send both the `x-api-key` and `Authorization: Bearer` headers.
122    /// This matches the behavior of the official Python SDK when both credentials are present.
123    #[must_use]
124    pub fn with_both(mut self, api_key: impl Into<String>, bearer: impl Into<String>) -> Self {
125        self.auth = AnthropicAuth::Both {
126            api_key: api_key.into(),
127            bearer: bearer.into(),
128        };
129        self
130    }
131
132    /// Sets custom beta feature strings
133    ///
134    /// These will be sent in the `anthropic-beta` header as a comma-separated list.
135    #[must_use]
136    pub fn with_beta<I, S>(mut self, beta: I) -> Self
137    where
138        I: IntoIterator<Item = S>,
139        S: Into<String>,
140    {
141        self.beta = beta.into_iter().map(Into::into).collect();
142        self
143    }
144
145    /// Returns the configured API base URL
146    #[must_use]
147    pub fn api_base(&self) -> &str {
148        &self.api_base
149    }
150
151    /// Validates that authentication credentials are present.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if neither API key nor bearer token is configured.
156    pub fn validate_auth(&self) -> Result<(), crate::error::AnthropicError> {
157        match &self.auth {
158            AnthropicAuth::None => Err(crate::error::AnthropicError::Config(
159                "Missing Anthropic credentials: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN"
160                    .into(),
161            )),
162            _ => Ok(()),
163        }
164    }
165
166    /// Sets beta features using the `BetaFeature` enum
167    ///
168    /// This is a type-safe alternative to [`with_beta`](Self::with_beta).
169    #[must_use]
170    pub fn with_beta_features<I: IntoIterator<Item = BetaFeature>>(mut self, features: I) -> Self {
171        self.beta = features.into_iter().map(Into::<String>::into).collect();
172        self
173    }
174}
175
176/// Configuration trait for the Anthropic client
177///
178/// Implement this trait to provide custom authentication and API configuration.
179pub trait Config: Send + Sync {
180    /// Returns HTTP headers to include in requests
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if header values contain invalid characters.
185    fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError>;
186
187    /// Constructs the full URL for an API endpoint
188    fn url(&self, path: &str) -> String;
189
190    /// Returns query parameters to include in requests
191    fn query(&self) -> Vec<(&str, &str)>;
192
193    /// Validates that authentication credentials are present.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if authentication is not properly configured.
198    fn validate_auth(&self) -> Result<(), crate::error::AnthropicError>;
199}
200
201impl Config for AnthropicConfig {
202    fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError> {
203        use crate::error::AnthropicError;
204
205        let mut h = HeaderMap::new();
206
207        h.insert(
208            HDR_ANTHROPIC_VERSION,
209            HeaderValue::from_str(&self.version)
210                .map_err(|_| AnthropicError::Config("Invalid anthropic-version header".into()))?,
211        );
212
213        if !self.beta.is_empty() {
214            let v = self.beta.join(",");
215            h.insert(
216                HDR_ANTHROPIC_BETA,
217                HeaderValue::from_str(&v)
218                    .map_err(|_| AnthropicError::Config("Invalid anthropic-beta header".into()))?,
219            );
220        }
221
222        match &self.auth {
223            AnthropicAuth::ApiKey(k) => {
224                h.insert(
225                    HDR_X_API_KEY,
226                    HeaderValue::from_str(k)
227                        .map_err(|_| AnthropicError::Config("Invalid x-api-key value".into()))?,
228                );
229            }
230            AnthropicAuth::Bearer(t) => {
231                let v = format!("Bearer {t}");
232                h.insert(
233                    AUTHORIZATION,
234                    HeaderValue::from_str(&v).map_err(|_| {
235                        AnthropicError::Config("Invalid Authorization header".into())
236                    })?,
237                );
238            }
239            AnthropicAuth::Both { api_key, bearer } => {
240                h.insert(
241                    HDR_X_API_KEY,
242                    HeaderValue::from_str(api_key)
243                        .map_err(|_| AnthropicError::Config("Invalid x-api-key value".into()))?,
244                );
245                let v = format!("Bearer {bearer}");
246                h.insert(
247                    AUTHORIZATION,
248                    HeaderValue::from_str(&v).map_err(|_| {
249                        AnthropicError::Config("Invalid Authorization header".into())
250                    })?,
251                );
252            }
253            AnthropicAuth::None => {}
254        }
255
256        Ok(h)
257    }
258
259    fn url(&self, path: &str) -> String {
260        format!("{}{}", self.api_base, path)
261    }
262
263    fn query(&self) -> Vec<(&str, &str)> {
264        vec![]
265    }
266
267    fn validate_auth(&self) -> Result<(), crate::error::AnthropicError> {
268        self.validate_auth()
269    }
270}
271
272/// Known Anthropic beta features
273///
274/// See the [Anthropic API documentation](https://docs.anthropic.com/en/api) for details on each feature.
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub enum BetaFeature {
277    /// Prompt caching (2024-07-31)
278    PromptCaching20240731,
279    /// Extended cache TTL (2025-04-11)
280    ExtendedCacheTtl20250411,
281    /// Token counting (2024-11-01)
282    TokenCounting20241101,
283    /// Structured outputs (2025-09-17) — Python SDK version
284    StructuredOutputs20250917,
285    /// Structured outputs (2025-11-13) — TypeScript SDK version (recommended)
286    StructuredOutputs20251113,
287    /// Alias to the latest structured outputs beta (currently 2025-11-13)
288    StructuredOutputsLatest,
289    /// Custom beta feature string
290    Other(String),
291}
292
293impl From<BetaFeature> for String {
294    fn from(b: BetaFeature) -> Self {
295        match b {
296            BetaFeature::PromptCaching20240731 => "prompt-caching-2024-07-31".into(),
297            BetaFeature::ExtendedCacheTtl20250411 => "extended-cache-ttl-2025-04-11".into(),
298            BetaFeature::TokenCounting20241101 => "token-counting-2024-11-01".into(),
299            BetaFeature::StructuredOutputs20250917 => "structured-outputs-2025-09-17".into(),
300            BetaFeature::StructuredOutputs20251113 | BetaFeature::StructuredOutputsLatest => {
301                "structured-outputs-2025-11-13".into()
302            }
303            BetaFeature::Other(s) => s,
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_default_headers_exist() {
314        let cfg = AnthropicConfig::new();
315        let h = cfg.headers().unwrap();
316        assert!(h.contains_key(super::HDR_ANTHROPIC_VERSION));
317    }
318
319    #[test]
320    fn auth_api_key_header() {
321        let cfg = AnthropicConfig::new().with_api_key("k123");
322        let h = cfg.headers().unwrap();
323        assert!(h.contains_key(HDR_X_API_KEY));
324        assert!(!h.contains_key(reqwest::header::AUTHORIZATION));
325    }
326
327    #[test]
328    fn auth_bearer_header() {
329        let cfg = AnthropicConfig::new().with_bearer("t123");
330        let h = cfg.headers().unwrap();
331        assert!(h.contains_key(reqwest::header::AUTHORIZATION));
332        assert!(!h.contains_key(HDR_X_API_KEY));
333    }
334
335    #[test]
336    fn auth_both_headers() {
337        let cfg = AnthropicConfig::new().with_both("k123", "t123");
338        let h = cfg.headers().unwrap();
339        assert!(h.contains_key(HDR_X_API_KEY));
340        assert!(h.contains_key(reqwest::header::AUTHORIZATION));
341    }
342
343    #[test]
344    fn beta_header_join() {
345        let cfg = AnthropicConfig::new().with_beta(vec!["a", "b"]);
346        let h = cfg.headers().unwrap();
347        let v = h.get(HDR_ANTHROPIC_BETA).unwrap().to_str().unwrap();
348        assert_eq!(v, "a,b");
349    }
350
351    #[test]
352    fn invalid_header_values_error() {
353        let cfg = AnthropicConfig::new().with_api_key("bad\nkey");
354        match cfg.headers() {
355            Err(crate::error::AnthropicError::Config(msg)) => assert!(msg.contains("x-api-key")),
356            other => panic!("Expected Config error, got {other:?}"),
357        }
358    }
359
360    #[test]
361    fn validate_auth_missing() {
362        let cfg = AnthropicConfig {
363            api_base: "test".into(),
364            version: "test".into(),
365            auth: AnthropicAuth::None,
366            beta: vec![],
367        };
368        assert!(cfg.validate_auth().is_err());
369    }
370}