use super::{create_http_client, LlmProvider}; use anyhow::{bail, Context, Result}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::time::Duration; /// Anthropic Claude API client pub struct AnthropicClient { api_key: String, model: String, client: reqwest::Client, } #[derive(Debug, Serialize)] struct MessagesRequest { model: String, max_tokens: u32, #[serde(skip_serializing_if = "Option::is_none")] temperature: Option, messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] system: Option, } #[derive(Debug, Serialize, Deserialize)] struct AnthropicMessage { role: String, content: String, } #[derive(Debug, Deserialize)] struct MessagesResponse { content: Vec, } #[derive(Debug, Deserialize)] struct ContentBlock { #[serde(rename = "type")] content_type: String, text: String, } #[derive(Debug, Deserialize)] struct ErrorResponse { error: AnthropicError, } #[derive(Debug, Deserialize)] struct AnthropicError { #[serde(rename = "type")] error_type: String, message: String, } impl AnthropicClient { /// Create new Anthropic client pub fn new(api_key: &str, model: &str) -> Result { let client = create_http_client(Duration::from_secs(60))?; Ok(Self { api_key: api_key.to_string(), model: model.to_string(), client, }) } /// Set timeout pub fn with_timeout(mut self, timeout: Duration) -> Result { self.client = create_http_client(timeout)?; Ok(self) } /// Validate API key pub async fn validate_key(&self) -> Result { let url = "https://api.anthropic.com/v1/messages"; let request = MessagesRequest { model: self.model.clone(), max_tokens: 5, temperature: Some(0.0), messages: vec![AnthropicMessage { role: "user".to_string(), content: "Hi".to_string(), }], system: None, }; let response = self.client .post(url) .header("x-api-key", &self.api_key) .header("anthropic-version", "2023-06-01") .header("Content-Type", "application/json") .json(&request) .send() .await; match response { Ok(resp) => { if resp.status().is_success() { Ok(true) } else { let status = resp.status(); if status.as_u16() == 401 { Ok(false) } else { let text = resp.text().await.unwrap_or_default(); bail!("Anthropic API error: {} - {}", status, text) } } } Err(e) => Err(e.into()), } } } #[async_trait] impl LlmProvider for AnthropicClient { async fn generate(&self, prompt: &str) -> Result { let messages = vec![AnthropicMessage { role: "user".to_string(), content: prompt.to_string(), }]; self.messages_request(messages, None).await } async fn generate_with_system(&self, system: &str, user: &str) -> Result { let messages = vec![AnthropicMessage { role: "user".to_string(), content: user.to_string(), }]; let system = if system.is_empty() { None } else { Some(system.to_string()) }; self.messages_request(messages, system).await } async fn is_available(&self) -> bool { self.validate_key().await.unwrap_or(false) } fn name(&self) -> &str { "anthropic" } } impl AnthropicClient { async fn messages_request( &self, messages: Vec, system: Option, ) -> Result { let url = "https://api.anthropic.com/v1/messages"; let request = MessagesRequest { model: self.model.clone(), max_tokens: 500, temperature: Some(0.7), messages, system, }; let response = self.client .post(url) .header("x-api-key", &self.api_key) .header("anthropic-version", "2023-06-01") .header("Content-Type", "application/json") .json(&request) .send() .await .context("Failed to send request to Anthropic")?; let status = response.status(); if !status.is_success() { let text = response.text().await.unwrap_or_default(); // Try to parse error if let Ok(error) = serde_json::from_str::(&text) { bail!("Anthropic API error: {} ({})", error.error.message, error.error.error_type); } bail!("Anthropic API error: {} - {}", status, text); } let result: MessagesResponse = response .json() .await .context("Failed to parse Anthropic response")?; result.content .into_iter() .find(|c| c.content_type == "text") .map(|c| c.text.trim().to_string()) .ok_or_else(|| anyhow::anyhow!("No text response from Anthropic")) } } /// Available Anthropic models pub const ANTHROPIC_MODELS: &[&str] = &[ "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307", "claude-2.1", "claude-2.0", "claude-instant-1.2", ]; /// Check if a model name is valid pub fn is_valid_model(model: &str) -> bool { ANTHROPIC_MODELS.contains(&model) } #[cfg(test)] mod tests { use super::*; #[test] fn test_model_validation() { assert!(is_valid_model("claude-3-sonnet-20240229")); assert!(!is_valid_model("invalid-model")); } }