Skip to main content

rig_core/providers/anthropic/
client.rs

1//! Anthropic client api implementation
2use http::{HeaderName, HeaderValue};
3
4use super::completion::{ANTHROPIC_VERSION_LATEST, CompletionModel};
5use crate::{
6    client::{self, ApiKey, DebugExt, Provider, ProviderBuilder},
7    http_client::{self, HttpClientExt},
8    providers::anthropic::model_listing::AnthropicModelLister,
9};
10
11// ================================================================
12// Main Anthropic Client
13// ================================================================
14#[derive(Debug, Default, Clone)]
15pub struct AnthropicExt;
16
17impl Provider for AnthropicExt {
18    type Builder = AnthropicBuilder;
19    const VERIFY_PATH: &'static str = "/v1/models";
20}
21
22client::impl_capabilities!(
23    AnthropicExt,
24    completion = CompletionModel<H>,
25    model_listing = AnthropicModelLister<H>,
26);
27
28#[derive(Debug, Clone)]
29pub struct AnthropicBuilder {
30    pub(crate) anthropic_version: String,
31    pub(crate) anthropic_betas: Vec<String>,
32}
33
34#[derive(Debug, Clone)]
35pub struct AnthropicKey(String);
36
37impl<S> From<S> for AnthropicKey
38where
39    S: Into<String>,
40{
41    fn from(value: S) -> Self {
42        Self(value.into())
43    }
44}
45
46impl ApiKey for AnthropicKey {
47    fn into_header(self) -> Option<http_client::Result<(http::HeaderName, HeaderValue)>> {
48        Some(
49            HeaderValue::from_str(&self.0)
50                .map(|val| (HeaderName::from_static("x-api-key"), val))
51                .map_err(Into::into),
52        )
53    }
54}
55
56pub type Client<H = reqwest::Client> = client::Client<AnthropicExt, H>;
57pub type ClientBuilder<H = crate::markers::Missing> =
58    client::ClientBuilder<AnthropicBuilder, AnthropicKey, H>;
59
60impl Default for AnthropicBuilder {
61    fn default() -> Self {
62        Self {
63            anthropic_version: ANTHROPIC_VERSION_LATEST.into(),
64            anthropic_betas: Vec::new(),
65        }
66    }
67}
68
69impl ProviderBuilder for AnthropicBuilder {
70    type Extension<H>
71        = AnthropicExt
72    where
73        H: HttpClientExt;
74    type ApiKey = AnthropicKey;
75
76    const BASE_URL: &'static str = "https://api.anthropic.com";
77
78    fn build<H>(
79        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
80    ) -> http_client::Result<Self::Extension<H>>
81    where
82        H: HttpClientExt,
83    {
84        Ok(AnthropicExt)
85    }
86
87    fn finish<H>(
88        &self,
89        builder: client::ClientBuilder<Self, AnthropicKey, H>,
90    ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
91        finish_anthropic_builder(self, builder)
92    }
93}
94
95impl DebugExt for AnthropicExt {}
96
97client::impl_provider_client!(
98    Client,
99    input = String,
100    api_key_env = "ANTHROPIC_API_KEY",
101    base_url_env_first = "ANTHROPIC_BASE_URL",
102);
103
104/// Create a new anthropic client using the builder
105///
106/// # Example
107/// ```no_run
108/// use rig_core::providers::anthropic::{Client, self};
109/// use rig_core::providers::anthropic::completion::ANTHROPIC_VERSION_LATEST;
110///
111/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
112/// // Initialize the Anthropic client
113/// let anthropic_client = Client::builder()
114///    .api_key("your-claude-api-key")
115///    .anthropic_version(ANTHROPIC_VERSION_LATEST)
116///    .anthropic_beta("prompt-caching-2024-07-31")
117///    .build()?;
118/// # Ok(())
119/// # }
120/// ```
121impl<H> ClientBuilder<H> {
122    pub fn anthropic_version(self, anthropic_version: &str) -> Self {
123        self.over_ext(|ext| AnthropicBuilder {
124            anthropic_version: anthropic_version.into(),
125            ..ext
126        })
127    }
128
129    pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
130        self.over_ext(|mut ext| {
131            ext.anthropic_betas
132                .extend(anthropic_betas.iter().copied().map(String::from));
133
134            ext
135        })
136    }
137
138    pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
139        self.over_ext(|mut ext| {
140            ext.anthropic_betas.push(anthropic_beta.into());
141
142            ext
143        })
144    }
145}
146
147pub fn normalize_anthropic_base_url(base_url: &str) -> String {
148    let trimmed = base_url.trim_end_matches('/');
149
150    if let Some(stripped) = trimmed.strip_suffix("/v1/messages") {
151        stripped.to_string()
152    } else if let Some(stripped) = trimmed.strip_suffix("/messages") {
153        stripped.to_string()
154    } else if let Some(stripped) = trimmed.strip_suffix("/v1") {
155        stripped.to_string()
156    } else {
157        trimmed.to_string()
158    }
159}
160
161pub fn finish_anthropic_builder<ExtBuilder, H>(
162    ext: &AnthropicBuilder,
163    mut builder: client::ClientBuilder<ExtBuilder, AnthropicKey, H>,
164) -> http_client::Result<client::ClientBuilder<ExtBuilder, AnthropicKey, H>>
165where
166    ExtBuilder: Clone,
167{
168    let normalized_base_url = normalize_anthropic_base_url(builder.get_base_url());
169    builder = builder.base_url(normalized_base_url);
170
171    builder.headers_mut().insert(
172        "anthropic-version",
173        HeaderValue::from_str(&ext.anthropic_version)?,
174    );
175
176    if !ext.anthropic_betas.is_empty() {
177        builder.headers_mut().insert(
178            "anthropic-beta",
179            HeaderValue::from_str(&ext.anthropic_betas.join(","))?,
180        );
181    }
182
183    Ok(builder)
184}
185
186// The remaining compatible-client repetition is inherent builder methods and
187// a ProviderBuilder implementation, neither of which ordinary functions can
188// generate. Keep the actual header behavior in `finish_anthropic_builder` and
189// generate only this type-level plumbing.
190macro_rules! impl_anthropic_compatible_builder {
191    ($builder:ty => $extension:ty, base_url = $base_url:expr $(,)?) => {
192        $crate::client::impl_default_provider_builder!(
193            $builder => $extension,
194            api_key = $crate::providers::anthropic::client::AnthropicKey,
195            base_url = $base_url,
196            finish = $crate::providers::anthropic::client::finish_anthropic_builder,
197            state = anthropic,
198        );
199
200        impl<H>
201            $crate::client::ClientBuilder<
202                $builder,
203                $crate::providers::anthropic::client::AnthropicKey,
204                H,
205            >
206        {
207            pub fn anthropic_version(self, anthropic_version: &str) -> Self {
208                self.over_ext(|mut ext| {
209                    ext.anthropic.anthropic_version = anthropic_version.into();
210                    ext
211                })
212            }
213
214            pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
215                self.over_ext(|mut ext| {
216                    ext.anthropic
217                        .anthropic_betas
218                        .extend(anthropic_betas.iter().copied().map(String::from));
219                    ext
220                })
221            }
222
223            pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
224                self.over_ext(|mut ext| {
225                    ext.anthropic.anthropic_betas.push(anthropic_beta.into());
226                    ext
227                })
228            }
229        }
230    };
231}
232pub(crate) use impl_anthropic_compatible_builder;
233
234#[cfg(test)]
235mod tests {
236    use std::sync::Mutex;
237
238    use crate::client::ProviderClient;
239
240    static ENV_LOCK: Mutex<()> = Mutex::new(());
241
242    struct EnvVarGuard {
243        key: &'static str,
244        original: Option<String>,
245    }
246
247    impl EnvVarGuard {
248        fn set(key: &'static str, value: &str) -> Self {
249            let original = std::env::var(key).ok();
250            // SAFETY: Tests in this module hold ENV_LOCK while mutating process
251            // environment and restore the original value before releasing it.
252            unsafe { std::env::set_var(key, value) };
253
254            Self { key, original }
255        }
256
257        fn remove(key: &'static str) -> Self {
258            let original = std::env::var(key).ok();
259            // SAFETY: Tests in this module hold ENV_LOCK while mutating process
260            // environment and restore the original value before releasing it.
261            unsafe { std::env::remove_var(key) };
262
263            Self { key, original }
264        }
265    }
266
267    impl Drop for EnvVarGuard {
268        fn drop(&mut self) {
269            // SAFETY: Tests in this module hold ENV_LOCK while mutating process
270            // environment and restore the original value before releasing it.
271            unsafe {
272                match &self.original {
273                    Some(value) => std::env::set_var(self.key, value),
274                    None => std::env::remove_var(self.key),
275                }
276            }
277        }
278    }
279
280    #[test]
281    fn test_client_initialization() {
282        let _client =
283            crate::providers::anthropic::Client::new("dummy-key").expect("Client::new() failed");
284        let _client_from_builder = crate::providers::anthropic::Client::builder()
285            .api_key("dummy-key")
286            .build()
287            .expect("Client::builder() failed");
288    }
289
290    #[test]
291    fn from_env_uses_anthropic_base_url() {
292        let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
293        let _api_key = EnvVarGuard::set("ANTHROPIC_API_KEY", "dummy-key");
294        let _base_url = EnvVarGuard::set(
295            "ANTHROPIC_BASE_URL",
296            "https://anthropic-compatible.example/v1/messages",
297        );
298
299        let client = crate::providers::anthropic::Client::from_env()
300            .expect("Client::from_env should build with ANTHROPIC_BASE_URL");
301
302        assert_eq!(
303            client.base_url(),
304            "https://anthropic-compatible.example",
305            "from_env should apply ANTHROPIC_BASE_URL and the existing Anthropic base URL normalization"
306        );
307    }
308
309    #[test]
310    fn from_env_uses_default_base_url_when_anthropic_base_url_is_unset() {
311        let _guard = ENV_LOCK.lock().expect("env lock should not be poisoned");
312        let _api_key = EnvVarGuard::set("ANTHROPIC_API_KEY", "dummy-key");
313        let _base_url = EnvVarGuard::remove("ANTHROPIC_BASE_URL");
314
315        let client = crate::providers::anthropic::Client::from_env()
316            .expect("Client::from_env should build without ANTHROPIC_BASE_URL");
317
318        assert_eq!(client.base_url(), "https://api.anthropic.com");
319    }
320}