Skip to main content

anthropic_async/
client.rs

1use backon::{ExponentialBuilder, Retryable};
2use serde::{Serialize, de::DeserializeOwned};
3
4use crate::{config::Config, error::AnthropicError, retry};
5
6/// Anthropic API client
7///
8/// The client is generic over a [`Config`] implementation that provides authentication
9/// and API configuration.
10#[derive(Debug, Clone)]
11pub struct Client<C: Config> {
12    http: reqwest::Client,
13    config: C,
14    backoff: ExponentialBuilder,
15}
16
17impl Client<crate::config::AnthropicConfig> {
18    /// Creates a new client with default configuration
19    ///
20    /// Uses environment variables for authentication:
21    /// - `ANTHROPIC_API_KEY` for API key authentication
22    /// - `ANTHROPIC_AUTH_TOKEN` for bearer token authentication
23    #[must_use]
24    pub fn new() -> Self {
25        Self::with_config(crate::config::AnthropicConfig::new())
26    }
27}
28
29impl<C: Config + Default> Default for Client<C> {
30    fn default() -> Self {
31        Self::with_config(C::default())
32    }
33}
34
35impl<C: Config> Client<C> {
36    /// Creates a new client with the given configuration.
37    ///
38    /// # Panics
39    ///
40    /// Panics if the reqwest client cannot be built.
41    #[must_use]
42    #[expect(
43        clippy::expect_used,
44        reason = "reqwest client build failure is rare (TLS/resolver init) and fatal; matches reqwest::Client::new() pattern"
45    )]
46    pub fn with_config(config: C) -> Self {
47        Self {
48            http: reqwest::Client::builder()
49                .connect_timeout(std::time::Duration::from_secs(5))
50                .timeout(std::time::Duration::from_secs(600))
51                .build()
52                .expect("reqwest client"),
53            config,
54            backoff: retry::default_backoff_builder(),
55        }
56    }
57
58    /// Replaces the HTTP client with a custom one
59    ///
60    /// Useful for setting custom timeouts, proxies, or other HTTP configuration.
61    #[must_use]
62    pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
63        self.http = http;
64        self
65    }
66
67    /// Replaces the backoff configuration for retry logic
68    ///
69    /// By default, the client uses exponential backoff with jitter.
70    #[must_use]
71    pub const fn with_backoff(mut self, backoff: ExponentialBuilder) -> Self {
72        self.backoff = backoff;
73        self
74    }
75
76    /// Returns a reference to the client's configuration
77    #[must_use]
78    pub const fn config(&self) -> &C {
79        &self.config
80    }
81
82    pub(crate) async fn get<O: DeserializeOwned>(&self, path: &str) -> Result<O, AnthropicError> {
83        let mk = || async {
84            let headers = self.config.headers()?;
85            Ok(self
86                .http
87                .get(self.config.url(path))
88                .headers(headers)
89                .query(&self.config.query())
90                .build()?)
91        };
92        self.execute(mk).await
93    }
94
95    pub(crate) async fn get_with_query<Q, O>(
96        &self,
97        path: &str,
98        query: &Q,
99    ) -> Result<O, AnthropicError>
100    where
101        Q: Serialize + Sync + ?Sized,
102        O: DeserializeOwned,
103    {
104        let mk = || async {
105            let headers = self.config.headers()?;
106            Ok(self
107                .http
108                .get(self.config.url(path))
109                .headers(headers)
110                .query(&self.config.query())
111                .query(query)
112                .build()?)
113        };
114        self.execute(mk).await
115    }
116
117    pub(crate) async fn post<I, O>(&self, path: &str, body: I) -> Result<O, AnthropicError>
118    where
119        I: Serialize + Send + Sync,
120        O: DeserializeOwned,
121    {
122        let mk = || async {
123            let headers = self.config.headers()?;
124            Ok(self
125                .http
126                .post(self.config.url(path))
127                .headers(headers)
128                .query(&self.config.query())
129                .json(&body)
130                .build()?)
131        };
132        self.execute(mk).await
133    }
134
135    /// Sends a POST request and returns the raw response for streaming.
136    ///
137    /// This method does not retry on error, as streaming responses cannot be replayed.
138    #[cfg(feature = "streaming")]
139    pub(crate) async fn post_stream<I: Serialize + Send + Sync>(
140        &self,
141        path: &str,
142        body: I,
143    ) -> Result<reqwest::Response, AnthropicError> {
144        // Validate auth before any request
145        self.config.validate_auth()?;
146
147        let headers = self.config.headers()?;
148        let request = self
149            .http
150            .post(self.config.url(path))
151            .headers(headers)
152            .query(&self.config.query())
153            .json(&body)
154            .build()?;
155
156        let response = self
157            .http
158            .execute(request)
159            .await
160            .map_err(AnthropicError::Reqwest)?;
161
162        let status = response.status();
163        if status.is_success() {
164            Ok(response)
165        } else {
166            let bytes = response.bytes().await.map_err(AnthropicError::Reqwest)?;
167            Err(crate::error::deserialize_api_error(status, &bytes))
168        }
169    }
170
171    async fn execute<O, M, Fut>(&self, mk: M) -> Result<O, AnthropicError>
172    where
173        O: DeserializeOwned,
174        M: Fn() -> Fut + Send + Sync,
175        Fut: core::future::Future<Output = Result<reqwest::Request, AnthropicError>> + Send,
176    {
177        // Validate auth before any request
178        self.config.validate_auth()?;
179
180        let bytes = self.execute_raw(mk).await?;
181        let resp: O =
182            serde_json::from_slice(&bytes).map_err(|e| crate::error::map_deser(&e, &bytes))?;
183        Ok(resp)
184    }
185
186    async fn execute_raw<M, Fut>(&self, mk: M) -> Result<bytes::Bytes, AnthropicError>
187    where
188        M: Fn() -> Fut + Send + Sync,
189        Fut: core::future::Future<Output = Result<reqwest::Request, AnthropicError>> + Send,
190    {
191        let http_client = self.http.clone();
192
193        (|| async {
194            let request = mk().await?;
195            let response = http_client
196                .execute(request)
197                .await
198                .map_err(AnthropicError::Reqwest)?;
199
200            let status = response.status();
201            let bytes = response.bytes().await.map_err(AnthropicError::Reqwest)?;
202
203            if status.is_success() {
204                return Ok(bytes);
205            }
206
207            Err(crate::error::deserialize_api_error(status, &bytes))
208        })
209        .retry(self.backoff)
210        .when(AnthropicError::is_retryable)
211        .await
212    }
213}