lib_client_anthropic/
client.rs1use crate::auth::AuthStrategy;
4use crate::error::{AnthropicError, Result};
5use crate::types::{CreateMessageRequest, CreateMessageResponse, ErrorResponse};
6use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
7use std::sync::Arc;
8
9const DEFAULT_BASE_URL: &str = "https://api.anthropic.com";
10const ANTHROPIC_VERSION: &str = "2023-06-01";
11
12pub struct Client {
14 http: reqwest::Client,
15 auth: Arc<dyn AuthStrategy>,
16 base_url: String,
17}
18
19impl Client {
20 pub fn builder() -> ClientBuilder<()> {
22 ClientBuilder::new()
23 }
24
25 pub async fn create_message(
27 &self,
28 request: CreateMessageRequest,
29 ) -> Result<CreateMessageResponse> {
30 let url = format!("{}/v1/messages", self.base_url);
31 self.post(&url, &request).await
32 }
33
34 async fn post<T, B>(&self, url: &str, body: &B) -> Result<T>
36 where
37 T: serde::de::DeserializeOwned,
38 B: serde::Serialize,
39 {
40 let mut headers = HeaderMap::new();
41 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
42 headers.insert(
43 "anthropic-version",
44 HeaderValue::from_static(ANTHROPIC_VERSION),
45 );
46
47 self.auth.apply(&mut headers).await?;
48
49 tracing::debug!(url = %url, "POST request");
50
51 let response = self
52 .http
53 .post(url)
54 .headers(headers)
55 .json(body)
56 .send()
57 .await?;
58
59 self.handle_response(response).await
60 }
61
62 async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T>
64 where
65 T: serde::de::DeserializeOwned,
66 {
67 let status = response.status();
68 let status_code = status.as_u16();
69
70 if status.is_success() {
71 let body = response.text().await?;
72 tracing::debug!(status = %status_code, "Response received");
73 serde_json::from_str(&body).map_err(AnthropicError::from)
74 } else {
75 let body = response.text().await?;
76 tracing::warn!(status = %status_code, body = %body, "API error");
77
78 if let Ok(error_response) = serde_json::from_str::<ErrorResponse>(&body) {
80 let message = error_response.error.message;
81 let error_type = error_response.error.error_type.as_str();
82
83 return Err(match status_code {
84 401 => AnthropicError::Unauthorized,
85 403 => AnthropicError::Forbidden(message),
86 404 => AnthropicError::NotFound(message),
87 429 => {
88 let retry_after = extract_retry_after(&message).unwrap_or(60);
90 AnthropicError::RateLimited { retry_after }
91 }
92 529 => AnthropicError::Overloaded,
93 _ => match error_type {
94 "invalid_request_error" => AnthropicError::InvalidRequest(message),
95 _ => AnthropicError::Api {
96 status: status_code,
97 message,
98 },
99 },
100 });
101 }
102
103 Err(AnthropicError::Api {
104 status: status_code,
105 message: body,
106 })
107 }
108 }
109}
110
111pub struct ClientBuilder<A> {
113 auth: A,
114 base_url: String,
115}
116
117impl ClientBuilder<()> {
118 pub fn new() -> Self {
120 Self {
121 auth: (),
122 base_url: DEFAULT_BASE_URL.to_string(),
123 }
124 }
125
126 pub fn auth<S: AuthStrategy + 'static>(self, strategy: S) -> ClientBuilder<S> {
128 ClientBuilder {
129 auth: strategy,
130 base_url: self.base_url,
131 }
132 }
133}
134
135impl Default for ClientBuilder<()> {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl<A: AuthStrategy + 'static> ClientBuilder<A> {
142 pub fn base_url(mut self, url: impl Into<String>) -> Self {
144 self.base_url = url.into();
145 self
146 }
147
148 pub fn build(self) -> Client {
150 Client {
151 http: reqwest::Client::new(),
152 auth: Arc::new(self.auth),
153 base_url: self.base_url,
154 }
155 }
156}
157
158fn extract_retry_after(message: &str) -> Option<u64> {
160 message.split_whitespace().find_map(|word| {
162 word.trim_matches(|c: char| !c.is_ascii_digit())
163 .parse()
164 .ok()
165 })
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::auth::ApiKeyAuth;
172 use crate::types::Message;
173
174 #[test]
175 fn test_builder() {
176 let client = Client::builder()
177 .auth(ApiKeyAuth::new("test-key"))
178 .base_url("https://custom.api.com")
179 .build();
180
181 assert_eq!(client.base_url, "https://custom.api.com");
182 }
183
184 #[test]
185 fn test_create_message_request() {
186 let request = CreateMessageRequest::new(
187 "claude-sonnet-4-20250514",
188 vec![Message::user("Hello")],
189 1024,
190 )
191 .with_system("You are helpful")
192 .with_temperature(0.7);
193
194 assert_eq!(request.model, "claude-sonnet-4-20250514");
195 assert_eq!(request.max_tokens, 1024);
196 assert_eq!(request.system, Some("You are helpful".to_string()));
197 assert_eq!(request.temperature, Some(0.7));
198 }
199
200 #[test]
201 fn test_extract_retry_after() {
202 assert_eq!(extract_retry_after("retry after 30 seconds"), Some(30));
203 assert_eq!(extract_retry_after("wait 60s"), Some(60));
204 assert_eq!(extract_retry_after("no number here"), None);
205 }
206}