use backon::ExponentialBuilder;
use backon::Retryable;
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::config::Config;
use crate::error::AnthropicError;
use crate::retry;
#[derive(Debug, Clone)]
pub struct Client<C: Config> {
http: reqwest::Client,
config: C,
backoff: ExponentialBuilder,
}
impl Client<crate::config::AnthropicConfig> {
#[must_use]
pub fn new() -> Self {
Self::with_config(crate::config::AnthropicConfig::new())
}
}
impl<C: Config + Default> Default for Client<C> {
fn default() -> Self {
Self::with_config(C::default())
}
}
impl<C: Config> Client<C> {
#[must_use]
#[expect(
clippy::expect_used,
reason = "reqwest client build failure is rare (TLS/resolver init) and fatal; matches reqwest::Client::new() pattern"
)]
pub fn with_config(config: C) -> Self {
Self {
http: reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(5))
.timeout(std::time::Duration::from_secs(600))
.build()
.expect("reqwest client"),
config,
backoff: retry::default_backoff_builder(),
}
}
#[must_use]
pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
#[must_use]
pub const fn with_backoff(mut self, backoff: ExponentialBuilder) -> Self {
self.backoff = backoff;
self
}
#[must_use]
pub const fn config(&self) -> &C {
&self.config
}
pub(crate) async fn get<O: DeserializeOwned>(&self, path: &str) -> Result<O, AnthropicError> {
let mk = || async {
let headers = self.config.headers()?;
Ok(self
.http
.get(self.config.url(path))
.headers(headers)
.query(&self.config.query())
.build()?)
};
self.execute(mk).await
}
pub(crate) async fn get_with_query<Q, O>(
&self,
path: &str,
query: &Q,
) -> Result<O, AnthropicError>
where
Q: Serialize + Sync + ?Sized,
O: DeserializeOwned,
{
let mk = || async {
let headers = self.config.headers()?;
Ok(self
.http
.get(self.config.url(path))
.headers(headers)
.query(&self.config.query())
.query(query)
.build()?)
};
self.execute(mk).await
}
pub(crate) async fn post<I, O>(&self, path: &str, body: I) -> Result<O, AnthropicError>
where
I: Serialize + Send + Sync,
O: DeserializeOwned,
{
let mk = || async {
let headers = self.config.headers()?;
Ok(self
.http
.post(self.config.url(path))
.headers(headers)
.query(&self.config.query())
.json(&body)
.build()?)
};
self.execute(mk).await
}
#[cfg(feature = "streaming")]
pub(crate) async fn post_stream<I: Serialize + Send + Sync>(
&self,
path: &str,
body: I,
) -> Result<reqwest::Response, AnthropicError> {
self.config.validate_auth()?;
let headers = self.config.headers()?;
let request = self
.http
.post(self.config.url(path))
.headers(headers)
.query(&self.config.query())
.json(&body)
.build()?;
let response = self
.http
.execute(request)
.await
.map_err(AnthropicError::Reqwest)?;
let status = response.status();
if status.is_success() {
Ok(response)
} else {
let bytes = response.bytes().await.map_err(AnthropicError::Reqwest)?;
Err(crate::error::deserialize_api_error(status, &bytes))
}
}
async fn execute<O, M, Fut>(&self, mk: M) -> Result<O, AnthropicError>
where
O: DeserializeOwned,
M: Fn() -> Fut + Send + Sync,
Fut: core::future::Future<Output = Result<reqwest::Request, AnthropicError>> + Send,
{
self.config.validate_auth()?;
let bytes = self.execute_raw(mk).await?;
let resp: O =
serde_json::from_slice(&bytes).map_err(|e| crate::error::map_deser(&e, &bytes))?;
Ok(resp)
}
async fn execute_raw<M, Fut>(&self, mk: M) -> Result<bytes::Bytes, AnthropicError>
where
M: Fn() -> Fut + Send + Sync,
Fut: core::future::Future<Output = Result<reqwest::Request, AnthropicError>> + Send,
{
let http_client = self.http.clone();
(|| async {
let request = mk().await?;
let response = http_client
.execute(request)
.await
.map_err(AnthropicError::Reqwest)?;
let status = response.status();
let bytes = response.bytes().await.map_err(AnthropicError::Reqwest)?;
if status.is_success() {
return Ok(bytes);
}
Err(crate::error::deserialize_api_error(status, &bytes))
})
.retry(self.backoff)
.when(AnthropicError::is_retryable)
.await
}
}