1use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
2use serde::Deserialize;
3
4pub const ANTHROPIC_DEFAULT_BASE: &str = "https://api.anthropic.com";
6pub const ANTHROPIC_VERSION: &str = "2023-06-01";
8pub const HDR_ANTHROPIC_VERSION: &str = "anthropic-version";
10pub const HDR_ANTHROPIC_BETA: &str = "anthropic-beta";
12pub const HDR_X_API_KEY: &str = "x-api-key";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum AnthropicAuth {
18 ApiKey(String),
20 Bearer(String),
22 Both {
24 api_key: String,
26 bearer: String,
28 },
29 None,
31}
32
33#[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 #[must_use]
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 #[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 #[must_use]
96 pub fn with_version(mut self, v: impl Into<String>) -> Self {
97 self.version = v.into();
98 self
99 }
100
101 #[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 #[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 #[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 #[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 #[must_use]
147 pub fn api_base(&self) -> &str {
148 &self.api_base
149 }
150
151 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 #[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
176pub trait Config: Send + Sync {
180 fn headers(&self) -> Result<HeaderMap, crate::error::AnthropicError>;
186
187 fn url(&self, path: &str) -> String;
189
190 fn query(&self) -> Vec<(&str, &str)>;
192
193 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#[derive(Debug, Clone, PartialEq, Eq)]
276pub enum BetaFeature {
277 PromptCaching20240731,
279 ExtendedCacheTtl20250411,
281 TokenCounting20241101,
283 StructuredOutputs20250917,
285 StructuredOutputs20251113,
287 StructuredOutputsLatest,
289 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}