//! Claude API client for LLM tool calling. use serde::{Deserialize, Serialize}; use thiserror::Error; use super::tools::{Tool, ToolCall}; const CLAUDE_API_URL: &str = "https://api.anthropic.com/v1/messages"; const ANTHROPIC_VERSION: &str = "2023-06-01"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClaudeModel { Opus, Sonnet, } impl ClaudeModel { pub fn model_id(&self) -> &'static str { match self { ClaudeModel::Opus => "claude-opus-4-5-20251101", ClaudeModel::Sonnet => "claude-sonnet-4-5-20250929", } } } impl Default for ClaudeModel { fn default() -> Self { ClaudeModel::Opus } } #[derive(Debug, Error)] pub enum ClaudeError { #[error("HTTP request failed: {0}")] Request(#[from] reqwest::Error), #[error("API error: {0}")] Api(String), #[error("Missing API key")] MissingApiKey, } #[derive(Debug, Clone)] pub struct ClaudeClient { api_key: String, client: reqwest::Client, model: ClaudeModel, } // Request types #[derive(Debug, Serialize)] struct ClaudeRequest { model: String, max_tokens: u32, messages: Vec, #[serde(skip_serializing_if = "Option::is_none")] tools: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { pub role: String, pub content: MessageContent, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MessageContent { Text(String), Blocks(Vec), } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ContentBlock { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "tool_use")] ToolUse { id: String, name: String, input: serde_json::Value, }, #[serde(rename = "tool_result")] ToolResult { tool_use_id: String, content: String, }, } #[derive(Debug, Serialize)] struct ToolDefinition { name: String, description: String, input_schema: serde_json::Value, } // Response types #[derive(Debug, Deserialize)] struct ClaudeResponse { content: Vec, stop_reason: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum ResponseContentBlock { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "tool_use")] ToolUse { id: String, name: String, input: serde_json::Value, }, } #[derive(Debug)] pub struct ChatResult { pub content: Option, pub tool_calls: Vec, /// Raw tool use blocks for including in subsequent messages pub raw_tool_uses: Vec, pub stop_reason: String, } impl ClaudeClient { pub fn new(api_key: String, model: ClaudeModel) -> Self { Self { api_key, client: reqwest::Client::new(), model, } } pub fn from_env(model: ClaudeModel) -> Result { let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| ClaudeError::MissingApiKey)?; Ok(Self::new(api_key, model)) } pub async fn chat_with_tools( &self, messages: Vec, tools: &[Tool], ) -> Result { let tool_definitions: Vec = tools .iter() .map(|t| ToolDefinition { name: t.name.clone(), description: t.description.clone(), input_schema: t.parameters.clone(), }) .collect(); let request = ClaudeRequest { model: self.model.model_id().to_string(), max_tokens: 4096, messages, tools: Some(tool_definitions), }; let response = self .client .post(CLAUDE_API_URL) .header("x-api-key", &self.api_key) .header("anthropic-version", ANTHROPIC_VERSION) .header("Content-Type", "application/json") .json(&request) .send() .await?; if !response.status().is_success() { let error_text = response.text().await.unwrap_or_default(); return Err(ClaudeError::Api(error_text)); } let claude_response: ClaudeResponse = response.json().await?; let stop_reason = claude_response.stop_reason.unwrap_or_else(|| "end_turn".to_string()); // Extract text content and tool uses from content blocks let mut text_parts: Vec = Vec::new(); let mut raw_tool_uses: Vec = Vec::new(); for block in &claude_response.content { match block { ResponseContentBlock::Text { text } => { if !text.is_empty() { text_parts.push(text.clone()); } } ResponseContentBlock::ToolUse { .. } => { raw_tool_uses.push(block.clone()); } } } let content = if text_parts.is_empty() { None } else { Some(text_parts.join("\n")) }; // Convert tool uses to ToolCalls let tool_calls: Vec = raw_tool_uses .iter() .filter_map(|block| { if let ResponseContentBlock::ToolUse { id, name, input } = block { Some(ToolCall { id: id.clone(), name: name.clone(), arguments: input.clone(), }) } else { None } }) .collect(); Ok(ChatResult { content, tool_calls, raw_tool_uses, stop_reason, }) } } /// Helper to convert Groq-style messages to Claude messages pub fn groq_messages_to_claude(messages: &[super::groq::Message]) -> Vec { let mut claude_messages: Vec = Vec::new(); for msg in messages { match msg.role.as_str() { "system" => { // Claude handles system prompts as first user message if let Some(ref content) = msg.content { claude_messages.push(Message { role: "user".to_string(), content: MessageContent::Text(format!("[System Instructions]: {}", content)), }); // Add assistant acknowledgment to maintain conversation structure claude_messages.push(Message { role: "assistant".to_string(), content: MessageContent::Text("Understood. I'll follow these instructions.".to_string()), }); } } "user" => { if let Some(ref content) = msg.content { claude_messages.push(Message { role: "user".to_string(), content: MessageContent::Text(content.clone()), }); } } "assistant" => { let mut blocks: Vec = Vec::new(); // Add text content if present if let Some(ref content) = msg.content { if !content.is_empty() { blocks.push(ContentBlock::Text { text: content.clone() }); } } // Add tool uses if present if let Some(ref tool_calls) = msg.tool_calls { for tc in tool_calls { let input: serde_json::Value = serde_json::from_str(&tc.function.arguments).unwrap_or_default(); blocks.push(ContentBlock::ToolUse { id: tc.id.clone(), name: tc.function.name.clone(), input, }); } } if !blocks.is_empty() { claude_messages.push(Message { role: "assistant".to_string(), content: MessageContent::Blocks(blocks), }); } } "tool" => { // Tool results in Claude go in a user message with tool_result blocks if let Some(ref content) = msg.content { let tool_use_id = msg.tool_call_id.clone().unwrap_or_default(); claude_messages.push(Message { role: "user".to_string(), content: MessageContent::Blocks(vec![ContentBlock::ToolResult { tool_use_id, content: content.clone(), }]), }); } } _ => {} } } claude_messages }