diff options
Diffstat (limited to 'makima/src/llm/claude.rs')
| -rw-r--r-- | makima/src/llm/claude.rs | 304 |
1 files changed, 0 insertions, 304 deletions
diff --git a/makima/src/llm/claude.rs b/makima/src/llm/claude.rs deleted file mode 100644 index f475acd..0000000 --- a/makima/src/llm/claude.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! 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<Message>, - #[serde(skip_serializing_if = "Option::is_none")] - tools: Option<Vec<ToolDefinition>>, -} - -#[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<ContentBlock>), -} - -#[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<ResponseContentBlock>, - stop_reason: Option<String>, -} - -#[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<String>, - pub tool_calls: Vec<ToolCall>, - /// Raw tool use blocks for including in subsequent messages - pub raw_tool_uses: Vec<ResponseContentBlock>, - 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<Self, ClaudeError> { - 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<Message>, - tools: &[Tool], - ) -> Result<ChatResult, ClaudeError> { - let tool_definitions: Vec<ToolDefinition> = 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<String> = Vec::new(); - let mut raw_tool_uses: Vec<ResponseContentBlock> = 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<ToolCall> = 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<Message> { - let mut claude_messages: Vec<Message> = 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<ContentBlock> = 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 -} |
