//! Anthropic API provider implementation. use async_trait::async_trait; use gyandoot_core::{ConversationContext, MessageRole, ProviderResult}; use reqwest::Client; use tracing::debug; use crate::{Provider, ProviderResponse}; /// Provider for the Anthropic (Claude) API. pub struct AnthropicProvider { api_key: String, model: String, client: Client, } impl AnthropicProvider { /// Create a new Anthropic provider. pub fn new(api_key: String, model: String) -> Self { Self { api_key, model, client: Client::new(), } } /// Build the messages array in Anthropic's format. fn build_messages(&self, context: &ConversationContext) -> Vec { context .messages .iter() .filter(|m| m.role != MessageRole::System) .map(|m| { let role = match m.role { MessageRole::User => "user", MessageRole::Assistant => "assistant", MessageRole::Tool => "user", MessageRole::System => "user", // filtered above }; serde_json::json!({ "role": role, "content": m.content, }) }) .collect() } /// Extract the system prompt from context (Anthropic uses a separate field). fn extract_system(&self, context: &ConversationContext) -> String { context .messages .iter() .find(|m| m.role == MessageRole::System) .map(|m| m.content.clone()) .unwrap_or_default() } } #[async_trait] impl Provider for AnthropicProvider { async fn chat(&self, context: ConversationContext) -> ProviderResult { debug!("Anthropic chat request (model: {})", self.model); let system = self.extract_system(&context); let messages = self.build_messages(&context); let mut body = serde_json::json!({ "model": self.model, "messages": messages, "max_tokens": context.max_tokens, }); if !system.is_empty() { body["system"] = serde_json::Value::String(system); } let response = self .client .post("https://api.anthropic.com/v1/messages") .header("x-api-key", &self.api_key) .header("anthropic-version", "2023-06-01") .json(&body) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response.text().await.unwrap_or_default(); return Err(gyandoot_core::ProviderError::Api(format!( "Anthropic API error ({}): {}", status, error_text ))); } let data: serde_json::Value = response.json().await?; let content = data["content"][0]["text"] .as_str() .unwrap_or_default() .to_string(); let finish_reason = data["stop_reason"].as_str().unwrap_or("end_turn").to_string(); Ok(ProviderResponse { content, tool_calls: None, finish_reason, }) } async fn chat_stream( &self, context: ConversationContext, ) -> ProviderResult> + Send + Unpin>> { // Fall back to non-streaming for now let result = self.chat(context).await?; let stream = tokio_stream::StreamExt::chain( tokio_stream::once(Ok(result.content)), tokio_stream::empty(), ); Ok(Box::new(stream)) } }