lib_client_anthropic/
client.rs

1//! Anthropic API client implementation.
2
3use 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
12/// Anthropic API client.
13pub struct Client {
14    http: reqwest::Client,
15    auth: Arc<dyn AuthStrategy>,
16    base_url: String,
17}
18
19impl Client {
20    /// Create a new client builder.
21    pub fn builder() -> ClientBuilder<()> {
22        ClientBuilder::new()
23    }
24
25    /// Create a message (non-streaming).
26    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    /// Send a POST request with JSON body.
35    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    /// Handle API response.
63    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            // Try to parse error response
79            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                        // Try to extract retry-after from message
89                        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
111/// Client builder.
112pub struct ClientBuilder<A> {
113    auth: A,
114    base_url: String,
115}
116
117impl ClientBuilder<()> {
118    /// Create a new client builder.
119    pub fn new() -> Self {
120        Self {
121            auth: (),
122            base_url: DEFAULT_BASE_URL.to_string(),
123        }
124    }
125
126    /// Set the authentication strategy.
127    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    /// Set a custom base URL.
143    pub fn base_url(mut self, url: impl Into<String>) -> Self {
144        self.base_url = url.into();
145        self
146    }
147
148    /// Build the client.
149    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
158/// Extract retry-after value from error message.
159fn extract_retry_after(message: &str) -> Option<u64> {
160    // Try to find a number in the message that could be retry-after seconds
161    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}