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