claude_api/blocking/
client.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use serde::de::DeserializeOwned;
7
8use crate::auth::ApiKey;
9use crate::error::{Error, Result};
10use crate::retry::RetryPolicy;
11
12#[derive(Debug, Clone)]
17pub struct Client {
18 inner: Arc<Inner>,
19}
20
21#[derive(Debug)]
22struct Inner {
23 api_key: ApiKey,
24 base_url: String,
25 http: reqwest::blocking::Client,
26 user_agent: String,
27 betas: Vec<String>,
28 retry: RetryPolicy,
29}
30
31impl Client {
32 pub fn new(api_key: impl Into<String>) -> Self {
40 Self::builder()
41 .api_key(api_key)
42 .build()
43 .expect("default builder should succeed when an api key is provided")
44 }
45
46 pub fn builder() -> ClientBuilder {
48 ClientBuilder::default()
49 }
50
51 pub fn messages(&self) -> super::Messages<'_> {
53 super::Messages::new(self)
54 }
55
56 pub fn models(&self) -> super::Models<'_> {
58 super::Models::new(self)
59 }
60
61 pub(crate) fn request_builder(
64 &self,
65 method: reqwest::Method,
66 path: &str,
67 ) -> reqwest::blocking::RequestBuilder {
68 let url = format!("{}{}", self.inner.base_url, path);
69 self.inner
70 .http
71 .request(method, url)
72 .header("x-api-key", self.inner.api_key.as_str())
73 .header("anthropic-version", crate::ANTHROPIC_VERSION)
74 .header(reqwest::header::USER_AGENT, &self.inner.user_agent)
75 }
76
77 pub(crate) fn execute<R: DeserializeOwned>(
81 &self,
82 mut builder: reqwest::blocking::RequestBuilder,
83 per_request_betas: &[&str],
84 ) -> Result<R> {
85 if let Some(joined) = merge_betas(&self.inner.betas, per_request_betas) {
86 builder = builder.header("anthropic-beta", joined);
87 }
88
89 let response = builder.send()?;
90 let status = response.status();
91 let request_id = response
92 .headers()
93 .get("request-id")
94 .and_then(|v| v.to_str().ok())
95 .map(String::from);
96 let retry_after_header = response
97 .headers()
98 .get("retry-after")
99 .and_then(|v| v.to_str().ok())
100 .map(String::from);
101
102 let bytes = response.bytes()?;
103
104 if !status.is_success() {
105 tracing::warn!(
106 status = status.as_u16(),
107 request_id = ?request_id,
108 "claude-api: error response"
109 );
110 return Err(Error::from_response(
111 http::StatusCode::from_u16(status.as_u16())
112 .unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
113 request_id,
114 retry_after_header.as_deref(),
115 &bytes,
116 ));
117 }
118
119 Ok(serde_json::from_slice(&bytes)?)
120 }
121
122 pub(crate) fn execute_with_retry<R, F>(
125 &self,
126 mut make_request: F,
127 per_request_betas: &[&str],
128 ) -> Result<R>
129 where
130 R: DeserializeOwned,
131 F: FnMut() -> reqwest::blocking::RequestBuilder,
132 {
133 let policy = &self.inner.retry;
134 let mut attempt: u32 = 1;
135 loop {
136 let builder = make_request();
137 match self.execute(builder, per_request_betas) {
138 Ok(r) => return Ok(r),
139 Err(e) => {
140 if !e.is_retryable() || attempt >= policy.max_attempts {
141 return Err(e);
142 }
143 let backoff = policy.compute_backoff(attempt, e.retry_after());
144 tracing::warn!(
145 attempt,
146 retry_in_ms = u64::try_from(backoff.as_millis()).unwrap_or(u64::MAX),
147 request_id = ?e.request_id(),
148 status = ?e.status().map(|s| s.as_u16()),
149 "claude-api: retrying after error"
150 );
151 std::thread::sleep(backoff);
152 attempt += 1;
153 }
154 }
155 }
156 }
157
158 #[cfg(test)]
159 pub(crate) fn betas(&self) -> &[String] {
160 &self.inner.betas
161 }
162}
163
164fn merge_betas(client_betas: &[String], per_request_betas: &[&str]) -> Option<String> {
168 let trimmed: Vec<&str> = client_betas
169 .iter()
170 .map(String::as_str)
171 .chain(per_request_betas.iter().copied())
172 .map(str::trim)
173 .filter(|s| !s.is_empty())
174 .collect();
175 if trimmed.is_empty() {
176 None
177 } else {
178 Some(trimmed.join(","))
179 }
180}
181
182#[derive(Debug, Default)]
184pub struct ClientBuilder {
185 api_key: Option<String>,
186 base_url: Option<String>,
187 user_agent: Option<String>,
188 timeout: Option<Duration>,
189 betas: Vec<String>,
190 retry: Option<RetryPolicy>,
191 http: Option<reqwest::blocking::Client>,
192}
193
194impl ClientBuilder {
195 #[must_use]
197 pub fn api_key(mut self, k: impl Into<String>) -> Self {
198 self.api_key = Some(k.into());
199 self
200 }
201
202 #[must_use]
204 pub fn base_url(mut self, url: impl Into<String>) -> Self {
205 self.base_url = Some(url.into());
206 self
207 }
208
209 #[must_use]
211 pub fn beta(mut self, header_value: impl Into<String>) -> Self {
212 self.betas.push(header_value.into());
213 self
214 }
215
216 #[must_use]
219 pub fn timeout(mut self, d: Duration) -> Self {
220 self.timeout = Some(d);
221 self
222 }
223
224 #[must_use]
226 pub fn retry(mut self, policy: RetryPolicy) -> Self {
227 self.retry = Some(policy);
228 self
229 }
230
231 #[must_use]
233 pub fn http_client(mut self, c: reqwest::blocking::Client) -> Self {
234 self.http = Some(c);
235 self
236 }
237
238 #[must_use]
240 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
241 self.user_agent = Some(ua.into());
242 self
243 }
244
245 pub fn build(self) -> Result<Client> {
248 let api_key = self
249 .api_key
250 .ok_or_else(|| Error::InvalidConfig("api_key is required".into()))?;
251
252 let http = if let Some(c) = self.http {
253 c
254 } else {
255 let mut builder = reqwest::blocking::Client::builder();
256 if let Some(t) = self.timeout {
257 builder = builder.timeout(t);
258 }
259 builder.build()?
260 };
261
262 let inner = Inner {
263 api_key: ApiKey::new(api_key),
264 base_url: self
265 .base_url
266 .unwrap_or_else(|| crate::DEFAULT_BASE_URL.to_owned()),
267 http,
268 user_agent: self
269 .user_agent
270 .unwrap_or_else(|| crate::USER_AGENT.to_owned()),
271 betas: self.betas,
272 retry: self.retry.unwrap_or_default(),
273 };
274
275 Ok(Client {
276 inner: Arc::new(inner),
277 })
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use pretty_assertions::assert_eq;
285
286 #[test]
287 fn build_requires_api_key() {
288 let err = Client::builder().build().unwrap_err();
289 assert!(matches!(err, Error::InvalidConfig(_)));
290 }
291
292 #[test]
293 fn client_is_cheap_to_clone() {
294 let c1 = Client::new("sk-ant-x");
295 let c2 = c1.clone();
296 assert!(Arc::ptr_eq(&c1.inner, &c2.inner));
297 }
298
299 #[test]
300 fn builder_collects_betas_in_order() {
301 let client = Client::builder()
302 .api_key("sk-ant-x")
303 .beta("a")
304 .beta("b")
305 .build()
306 .unwrap();
307 assert_eq!(client.betas(), &["a".to_owned(), "b".to_owned()]);
308 }
309
310 #[test]
311 fn merge_betas_filters_empties_and_trims() {
312 assert_eq!(
313 merge_betas(&[" a ".into(), String::new()], &["", "b\n"]).as_deref(),
314 Some("a,b")
315 );
316 assert_eq!(merge_betas(&[], &[]), None);
317 }
318}