use crate::error::{ApiErrorResponse, Error, Result};
use crate::streaming::StreamEvent;
use crate::types::{MessagesRequest, MessagesResponse};
use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt, TryStreamExt};
use reqwest::{Client, StatusCode};
use std::pin::Pin;
use tracing::{debug, instrument};
const ANTHROPIC_API_URL: &str = "https://api.anthropic.com/v1/messages";
const API_VERSION: &str = "2023-06-01";
pub struct ClaudeClient {
http: Client,
api_key: String,
api_url: String,
api_version: String,
}
impl ClaudeClient {
pub fn anthropic(api_key: impl Into<String>) -> Self {
Self {
http: Client::new(),
api_key: api_key.into(),
api_url: ANTHROPIC_API_URL.to_string(),
api_version: API_VERSION.to_string(),
}
}
#[instrument(skip(self, request), fields(model = %request.model))]
pub async fn send_message(&self, request: MessagesRequest) -> Result<MessagesResponse> {
debug!("Sending message to Claude API");
let mut request = request;
request.stream = Some(false);
let response = self
.http
.post(&self.api_url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("content-type", "application/json")
.json(&request)
.send()
.await?;
let status = response.status();
debug!("Received response with status: {}", status);
match status {
StatusCode::OK => {
let messages_response: MessagesResponse = response.json().await?;
Ok(messages_response)
}
StatusCode::TOO_MANY_REQUESTS => {
let retry_after = response
.headers()
.get("retry-after")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok());
let error_body = response.text().await.unwrap_or_default();
Err(Error::RateLimit {
retry_after,
message: error_body,
})
}
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
let error_body = response.text().await.unwrap_or_default();
Err(Error::Authentication(error_body))
}
StatusCode::BAD_REQUEST => {
let error_text = response.text().await.unwrap_or_default();
if let Ok(api_error) = serde_json::from_str::<ApiErrorResponse>(&error_text) {
Err(Error::Api {
status: status.as_u16(),
message: api_error.error.message,
error_type: Some(api_error.error.error_type),
})
} else {
Err(Error::InvalidRequest(error_text))
}
}
_ if status.is_server_error() => {
let error_body = response.text().await.unwrap_or_default();
Err(Error::Server {
status: status.as_u16(),
message: error_body,
})
}
_ => {
let error_body = response.text().await.unwrap_or_default();
Err(Error::Api {
status: status.as_u16(),
message: error_body,
error_type: None,
})
}
}
}
#[instrument(skip(self, request), fields(model = %request.model))]
pub async fn send_streaming(
&self,
request: MessagesRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>> {
debug!("Sending streaming message to Claude API");
let mut request = request;
request.stream = Some(true);
let response = self
.http
.post(&self.api_url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", &self.api_version)
.header("content-type", "application/json")
.json(&request)
.send()
.await?;
let status = response.status();
debug!("Received streaming response with status: {}", status);
if !status.is_success() {
return Err(self.handle_error_response(status, response).await);
}
let byte_stream = response.bytes_stream();
let event_stream = byte_stream.eventsource();
let stream = event_stream.map(|result| {
let event = result.map_err(|e| Error::StreamParse(e.to_string()))?;
if event.data.is_empty() {
return Ok(None);
}
let stream_event = match event.event.as_str() {
"ping" => Some(StreamEvent::Ping),
"error" => {
let error: crate::streaming::StreamError = serde_json::from_str(&event.data)
.map_err(|e| Error::StreamParse(e.to_string()))?;
Some(StreamEvent::Error { error })
}
_ => {
Some(
serde_json::from_str::<StreamEvent>(&event.data).map_err(|e| {
Error::StreamParse(format!(
"Failed to parse event '{}': {}",
event.event, e
))
})?,
)
}
};
Ok(stream_event)
});
let filtered_stream = stream.try_filter_map(|opt| async move { Ok(opt) });
Ok(Box::pin(filtered_stream))
}
async fn handle_error_response(
&self,
status: StatusCode,
response: reqwest::Response,
) -> Error {
match status {
StatusCode::TOO_MANY_REQUESTS => {
let retry_after = response
.headers()
.get("retry-after")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok());
let error_body = response.text().await.unwrap_or_default();
Error::RateLimit {
retry_after,
message: error_body,
}
}
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
let error_body = response.text().await.unwrap_or_default();
Error::Authentication(error_body)
}
StatusCode::BAD_REQUEST => {
let error_text = response.text().await.unwrap_or_default();
if let Ok(api_error) = serde_json::from_str::<ApiErrorResponse>(&error_text) {
Error::Api {
status: status.as_u16(),
message: api_error.error.message,
error_type: Some(api_error.error.error_type),
}
} else {
Error::InvalidRequest(error_text)
}
}
_ if status.is_server_error() => {
let error_body = response.text().await.unwrap_or_default();
Error::Server {
status: status.as_u16(),
message: error_body,
}
}
_ => {
let error_body = response.text().await.unwrap_or_default();
Error::Api {
status: status.as_u16(),
message: error_body,
error_type: None,
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = ClaudeClient::anthropic("test-key");
assert_eq!(client.api_key, "test-key");
assert_eq!(client.api_url, ANTHROPIC_API_URL);
assert_eq!(client.api_version, API_VERSION);
}
}