oli_server/apis/
anthropic.rs

1use crate::apis::api_client::{ApiClient, CompletionOptions, Message, ToolCall, ToolResult};
2use crate::app::logger::{format_log_with_color, LogLevel};
3use crate::errors::AppError;
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use rand;
7use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
8use reqwest::Client as ReqwestClient;
9use reqwest::Response;
10use serde::{Deserialize, Serialize};
11use serde_json::{self, json, Value};
12use std::env;
13use std::time::Duration;
14
15// Anthropic API models
16#[derive(Debug, Clone, Serialize, Deserialize)]
17struct AnthropicMessage {
18    role: String,
19    content: Vec<AnthropicContent>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type")]
24enum AnthropicContent {
25    #[serde(rename = "text")]
26    Text { text: String },
27
28    #[serde(rename = "tool_use")]
29    ToolUse {
30        id: String,
31        name: String,
32        input: Value,
33    },
34
35    #[serde(rename = "tool_result")]
36    ToolResult {
37        #[serde(rename = "tool_use_id")]
38        tool_call_id: String,
39        content: String,
40    },
41}
42
43// The AnthropicToolUse struct is no longer needed as we're using AnthropicContent::ToolUse
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46struct AnthropicTool {
47    name: String,
48    description: Option<String>,
49    #[serde(rename = "input_schema")]
50    schema: Value,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54struct AnthropicResponseFormat {
55    #[serde(rename = "type")]
56    format_type: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    schema: Option<Value>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62struct AnthropicToolChoice {
63    #[serde(rename = "type")]
64    choice_type: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68struct AnthropicRequest {
69    model: String,
70    messages: Vec<AnthropicMessage>,
71    max_tokens: usize,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    system: Option<String>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    temperature: Option<f32>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    top_p: Option<f32>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    tools: Option<Vec<AnthropicTool>>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    tool_choice: Option<AnthropicToolChoice>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    response_format: Option<AnthropicResponseFormat>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87struct AnthropicResponse {
88    id: String,
89    model: String,
90    role: String,
91    content: Vec<AnthropicContent>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    usage: Option<Value>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    type_field: Option<String>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    stop_reason: Option<String>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    stop_sequence: Option<String>,
100}
101
102pub struct AnthropicClient {
103    client: ReqwestClient,
104    model: String,
105    api_base: String,
106}
107
108impl AnthropicClient {
109    // Helper function to send a request with retry logic for overload errors
110    async fn send_request_with_retry<T: serde::Serialize + Clone>(
111        &self,
112        request: &T,
113    ) -> Result<Response> {
114        // Implement retry logic with exponential backoff for 529 overload errors
115        let mut retries = 0;
116        let max_retries = 3; // Maximum number of retries
117        let mut delay_ms = 1000; // Start with 1 second delay
118
119        loop {
120            let result = self.client.post(&self.api_base).json(request).send().await;
121
122            match result {
123                Ok(resp) => {
124                    // If response is 429 (rate limit) or 529 (overloaded), retry
125                    if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS
126                        || resp.status().as_u16() == 529
127                    {
128                        if retries >= max_retries {
129                            // Return the last error response if max retries reached
130                            return Ok(resp);
131                        }
132
133                        // Extract retry-after header if available before cloning for the error body
134                        let retry_after = resp
135                            .headers()
136                            .get("retry-after")
137                            .and_then(|val| val.to_str().ok())
138                            .and_then(|val| val.parse::<u64>().ok())
139                            .unwrap_or(delay_ms);
140
141                        // Clone the response for logging
142                        let error_body = resp.text().await.unwrap_or_default();
143                        eprintln!(
144                            "{}",
145                            format_log_with_color(
146                                LogLevel::Warning,
147                                &format!(
148                                    "Anthropic API rate limited or overloaded: {}",
149                                    error_body
150                                )
151                            )
152                        );
153
154                        // Exponential backoff with jitter
155                        let jitter = rand::random::<u64>() % 500;
156                        let sleep_duration = Duration::from_millis(retry_after + jitter);
157
158                        // Sleep and retry
159                        tokio::time::sleep(sleep_duration).await;
160
161                        // Increase delay for next retry
162                        delay_ms = (delay_ms * 2).min(10000); // Cap at 10 seconds
163                        retries += 1;
164                        continue;
165                    }
166
167                    // For other status codes, return the response
168                    return Ok(resp);
169                }
170                Err(e) => {
171                    // For network errors, also use retry logic
172                    if retries >= max_retries {
173                        return Err(AppError::NetworkError(format!(
174                            "Failed to send request to Anthropic after {} retries: {}",
175                            retries, e
176                        ))
177                        .into());
178                    }
179
180                    // Exponential backoff with jitter
181                    let jitter = rand::random::<u64>() % 500;
182                    let sleep_duration = Duration::from_millis(delay_ms + jitter);
183                    tokio::time::sleep(sleep_duration).await;
184
185                    // Increase delay for next retry
186                    delay_ms = (delay_ms * 2).min(10000); // Cap at 10 seconds
187                    retries += 1;
188                }
189            }
190        }
191    }
192
193    pub fn new(model: Option<String>) -> Result<Self> {
194        // Try to get API key from environment
195        let api_key = env::var("ANTHROPIC_API_KEY")
196            .context("ANTHROPIC_API_KEY environment variable not set")?;
197
198        Self::with_api_key(api_key, model)
199    }
200
201    pub fn with_api_key(api_key: String, model: Option<String>) -> Result<Self> {
202        // Create new client with appropriate headers
203        let mut headers = HeaderMap::new();
204        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
205        headers.insert(
206            AUTHORIZATION,
207            HeaderValue::from_str(&format!("Bearer {}", api_key))?,
208        );
209        headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
210        headers.insert("x-api-key", HeaderValue::from_str(&api_key)?);
211
212        let client = ReqwestClient::builder().default_headers(headers).build()?;
213
214        // Default to Claude 3.7 Sonnet as the latest model with tooling capabilities
215        let model = model.unwrap_or_else(|| "claude-3-7-sonnet-20250219".to_string());
216
217        Ok(Self {
218            client,
219            model,
220            api_base: "https://api.anthropic.com/v1/messages".to_string(),
221        })
222    }
223
224    fn extract_system_message(&self, messages: &[Message]) -> Option<String> {
225        messages
226            .iter()
227            .find(|msg| msg.role == "system")
228            .map(|system_msg| system_msg.content.clone())
229    }
230
231    fn convert_messages(&self, messages: Vec<Message>) -> Vec<AnthropicMessage> {
232        messages
233            .into_iter()
234            .filter(|msg| msg.role != "system") // Filter out system messages
235            .map(|msg| AnthropicMessage {
236                role: msg.role,
237                content: vec![AnthropicContent::Text { text: msg.content }],
238            })
239            .collect()
240    }
241
242    fn convert_tool_definitions(
243        &self,
244        tools: Vec<crate::apis::api_client::ToolDefinition>,
245    ) -> Vec<AnthropicTool> {
246        tools
247            .into_iter()
248            .map(|tool| {
249                // Create a proper JSON Schema compliant schema object
250                let mut schema = serde_json::Map::new();
251                schema.insert(
252                    "$schema".to_string(),
253                    json!("https://json-schema.org/draft/2020-12/schema"),
254                );
255                schema.insert("type".to_string(), json!("object"));
256
257                // Add properties and required fields if they exist in the original parameters
258                if let Value::Object(params) = &tool.parameters {
259                    if let Some(props) = params.get("properties") {
260                        schema.insert("properties".to_string(), props.clone());
261                    }
262
263                    if let Some(required) = params.get("required") {
264                        schema.insert("required".to_string(), required.clone());
265                    }
266                }
267
268                AnthropicTool {
269                    name: tool.name,
270                    description: Some(tool.description),
271                    schema: Value::Object(schema),
272                }
273            })
274            .collect()
275    }
276}
277
278#[async_trait]
279impl ApiClient for AnthropicClient {
280    async fn complete(&self, messages: Vec<Message>, options: CompletionOptions) -> Result<String> {
281        // Extract system message if present
282        let system_message = self.extract_system_message(&messages);
283        let converted_messages = self.convert_messages(messages);
284
285        let max_tokens = options.max_tokens.unwrap_or(2048) as usize;
286
287        let mut request = AnthropicRequest {
288            model: self.model.clone(),
289            messages: converted_messages,
290            max_tokens,
291            system: system_message,
292            temperature: options.temperature,
293            top_p: options.top_p,
294            tools: None,
295            tool_choice: None,
296            response_format: None,
297        };
298
299        // Add structured output format if specified in options
300        if let Some(json_schema) = &options.json_schema {
301            request.response_format = Some(AnthropicResponseFormat {
302                format_type: "json".to_string(),
303                schema: serde_json::from_str(json_schema).ok(),
304            });
305        }
306
307        // Use our retry function instead of direct API call
308        let response = self.send_request_with_retry(&request).await?;
309
310        if !response.status().is_success() {
311            let status = response.status();
312            let error_text = response
313                .text()
314                .await
315                .unwrap_or_else(|_| "Unknown error".to_string());
316            return Err(AppError::NetworkError(format!(
317                "Anthropic API error: {} - {}",
318                status, error_text
319            ))
320            .into());
321        }
322
323        // Get the response as a string first for debugging
324        let response_text = response.text().await.map_err(|e| {
325            let error_msg = format!("Failed to get response text: {}", e);
326            eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
327            AppError::NetworkError(error_msg)
328        })?;
329
330        // Log response details
331        eprintln!(
332            "{}",
333            format_log_with_color(
334                LogLevel::Debug,
335                &format!(
336                    "Anthropic API response received: {} bytes",
337                    response_text.len()
338                )
339            )
340        );
341
342        // Try to parse the response
343        let anthropic_response: AnthropicResponse =
344            serde_json::from_str(&response_text).map_err(|e| {
345                let error_msg = format!("Failed to parse Anthropic response: {}", e);
346                eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
347                AppError::Other(error_msg)
348            })?;
349
350        // Extract content from response
351        let mut text_content = String::new();
352
353        // Look for text content in the response
354        for content_item in &anthropic_response.content {
355            if let AnthropicContent::Text { text } = content_item {
356                text_content = text.clone();
357                break;
358            }
359        }
360
361        // Return an error if no text content was found
362        if text_content.is_empty() {
363            let error_msg = "No text content in Anthropic response".to_string();
364            eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
365            return Err(AppError::LLMError(error_msg).into());
366        }
367
368        let content = text_content;
369
370        Ok(content)
371    }
372
373    async fn complete_with_tools(
374        &self,
375        messages: Vec<Message>,
376        options: CompletionOptions,
377        tool_results: Option<Vec<ToolResult>>,
378    ) -> Result<(String, Option<Vec<ToolCall>>)> {
379        // Extract system message if present
380        let system_message = self.extract_system_message(&messages);
381        let mut converted_messages = self.convert_messages(messages);
382
383        // Add tool results if they exist
384        if let Some(results) = tool_results {
385            // For each tool result, we need to add corresponding messages
386            for result in results {
387                // Ensure we have a valid tool_call_id
388                let tool_call_id = if result.tool_call_id.is_empty() {
389                    // Generate a simple UUID-like string if no ID was provided
390                    format!("tool-{}", rand::random::<u64>())
391                } else {
392                    result.tool_call_id.clone()
393                };
394
395                // Create a tool use message (from assistant)
396                let tool_use_msg = AnthropicMessage {
397                    role: "assistant".to_string(),
398                    content: vec![AnthropicContent::ToolUse {
399                        id: tool_call_id.clone(),
400                        name: "tool".to_string(), // We don't have the original name
401                        input: json!({}),         // We don't need the input for this
402                    }],
403                };
404
405                // Create a tool result message (from user) with proper tool_result content
406                let tool_result_msg = AnthropicMessage {
407                    role: "user".to_string(),
408                    content: vec![AnthropicContent::ToolResult {
409                        tool_call_id: tool_call_id.clone(),
410                        content: result.output.clone(),
411                    }],
412                };
413
414                // Add both messages to the conversation
415                converted_messages.push(tool_use_msg);
416                converted_messages.push(tool_result_msg);
417            }
418        }
419
420        let max_tokens = options.max_tokens.unwrap_or(2048) as usize;
421
422        let mut request = AnthropicRequest {
423            model: self.model.clone(),
424            messages: converted_messages,
425            max_tokens,
426            system: system_message,
427            temperature: options.temperature,
428            top_p: options.top_p,
429            tools: None,
430            tool_choice: None,
431            response_format: None,
432        };
433
434        // IMPORTANT: Add response_format only if json_schema exists AND tools don't exist
435        // This fixes the "extra inputs are not permitted" error when using tools
436        if let Some(json_schema) = &options.json_schema {
437            // Only add response_format if we're not using tools
438            if options.tools.is_none() {
439                request.response_format = Some(AnthropicResponseFormat {
440                    format_type: "json".to_string(),
441                    schema: serde_json::from_str(json_schema).ok(),
442                });
443            }
444        }
445
446        // Add tools if they exist
447        if let Some(tools) = options.tools {
448            let converted_tools = self.convert_tool_definitions(tools);
449            request.tools = Some(converted_tools);
450
451            // Set tool choice based on option
452            request.tool_choice = Some(AnthropicToolChoice {
453                choice_type: if options.require_tool_use {
454                    "required".to_string()
455                } else {
456                    "auto".to_string()
457                },
458            });
459        }
460
461        // Use our retry function instead of direct API call
462        let response = self.send_request_with_retry(&request).await?;
463
464        if !response.status().is_success() {
465            let status = response.status();
466            let error_text = response
467                .text()
468                .await
469                .unwrap_or_else(|_| "Unknown error".to_string());
470            return Err(AppError::NetworkError(format!(
471                "Anthropic API error: {} - {}",
472                status, error_text
473            ))
474            .into());
475        }
476
477        // Get the response as a string first for debugging
478        let response_text = response.text().await.map_err(|e| {
479            let error_msg = format!("Failed to get response text: {}", e);
480            eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
481            AppError::NetworkError(error_msg)
482        })?;
483
484        // Log response details
485        eprintln!(
486            "{}",
487            format_log_with_color(
488                LogLevel::Debug,
489                &format!(
490                    "Anthropic API response received: {} bytes",
491                    response_text.len()
492                )
493            )
494        );
495
496        // Try to parse the response
497        let anthropic_response: AnthropicResponse =
498            serde_json::from_str(&response_text).map_err(|e| {
499                let error_msg = format!("Failed to parse Anthropic response: {}", e);
500                eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
501                AppError::Other(error_msg)
502            })?;
503
504        // First extract tool calls from content
505        let mut tool_calls_vec = Vec::new();
506        let mut text_content = String::new();
507
508        // Process each content item
509        for content_item in &anthropic_response.content {
510            match content_item {
511                AnthropicContent::Text { text } => {
512                    // If we don't have a text content yet, use this one
513                    if text_content.is_empty() {
514                        text_content = text.clone();
515                    }
516                }
517                AnthropicContent::ToolUse { name, input, .. } => {
518                    // Add a tool call
519                    tool_calls_vec.push(crate::apis::api_client::ToolCall {
520                        id: None, // Anthropic doesn't provide IDs like OpenAI
521                        name: name.clone(),
522                        arguments: input.clone(),
523                    });
524                }
525                AnthropicContent::ToolResult { .. } => {
526                    // Tool results are not processed here, they're for the API to recognize tool result responses
527                }
528            }
529        }
530
531        // If we didn't find any text content, use an empty string
532        let content = if text_content.is_empty() {
533            String::new()
534        } else {
535            text_content
536        };
537
538        // We no longer need to check a top-level tool_use field as all tool uses
539        // will be in the content array already
540
541        // Return None if no tool calls found, otherwise return the vector
542        let tool_calls = if tool_calls_vec.is_empty() {
543            None
544        } else {
545            Some(tool_calls_vec)
546        };
547
548        Ok((content, tool_calls))
549    }
550}