use crate::apis::api_client::{ApiClient, CompletionOptions, Message, ToolCall, ToolResult};
use crate::app::logger::{format_log_with_color, LogLevel};
use crate::errors::AppError;
use anyhow::{Context, Result};
use async_trait::async_trait;
use rand;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use reqwest::Client as ReqwestClient;
use reqwest::Response;
use serde::{Deserialize, Serialize};
use serde_json::{self, json, Value};
use std::env;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AnthropicMessage {
role: String,
content: Vec<AnthropicContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
enum AnthropicContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: Value,
},
#[serde(rename = "tool_result")]
ToolResult {
#[serde(rename = "tool_use_id")]
tool_call_id: String,
content: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AnthropicTool {
name: String,
description: Option<String>,
#[serde(rename = "input_schema")]
schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AnthropicResponseFormat {
#[serde(rename = "type")]
format_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
schema: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AnthropicToolChoice {
#[serde(rename = "type")]
choice_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AnthropicRequest {
model: String,
messages: Vec<AnthropicMessage>,
max_tokens: usize,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<AnthropicTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<AnthropicToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
response_format: Option<AnthropicResponseFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AnthropicResponse {
id: String,
model: String,
role: String,
content: Vec<AnthropicContent>,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
type_field: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stop_sequence: Option<String>,
}
pub struct AnthropicClient {
client: ReqwestClient,
model: String,
api_base: String,
}
impl AnthropicClient {
async fn send_request_with_retry<T: serde::Serialize + Clone>(
&self,
request: &T,
) -> Result<Response> {
let mut retries = 0;
let max_retries = 3; let mut delay_ms = 1000;
loop {
let result = self.client.post(&self.api_base).json(request).send().await;
match result {
Ok(resp) => {
if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS
|| resp.status().as_u16() == 529
{
if retries >= max_retries {
return Ok(resp);
}
let retry_after = resp
.headers()
.get("retry-after")
.and_then(|val| val.to_str().ok())
.and_then(|val| val.parse::<u64>().ok())
.unwrap_or(delay_ms);
let error_body = resp.text().await.unwrap_or_default();
eprintln!(
"{}",
format_log_with_color(
LogLevel::Warning,
&format!(
"Anthropic API rate limited or overloaded: {}",
error_body
)
)
);
let jitter = rand::random::<u64>() % 500;
let sleep_duration = Duration::from_millis(retry_after + jitter);
tokio::time::sleep(sleep_duration).await;
delay_ms = (delay_ms * 2).min(10000); retries += 1;
continue;
}
return Ok(resp);
}
Err(e) => {
if retries >= max_retries {
return Err(AppError::NetworkError(format!(
"Failed to send request to Anthropic after {} retries: {}",
retries, e
))
.into());
}
let jitter = rand::random::<u64>() % 500;
let sleep_duration = Duration::from_millis(delay_ms + jitter);
tokio::time::sleep(sleep_duration).await;
delay_ms = (delay_ms * 2).min(10000); retries += 1;
}
}
}
}
pub fn new(model: Option<String>) -> Result<Self> {
let api_key = env::var("ANTHROPIC_API_KEY")
.context("ANTHROPIC_API_KEY environment variable not set")?;
Self::with_api_key(api_key, model)
}
pub fn with_api_key(api_key: String, model: Option<String>) -> Result<Self> {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", api_key))?,
);
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
headers.insert("x-api-key", HeaderValue::from_str(&api_key)?);
let client = ReqwestClient::builder().default_headers(headers).build()?;
let model = model.unwrap_or_else(|| "claude-3-7-sonnet-20250219".to_string());
Ok(Self {
client,
model,
api_base: "https://api.anthropic.com/v1/messages".to_string(),
})
}
fn extract_system_message(&self, messages: &[Message]) -> Option<String> {
messages
.iter()
.find(|msg| msg.role == "system")
.map(|system_msg| system_msg.content.clone())
}
fn convert_messages(&self, messages: Vec<Message>) -> Vec<AnthropicMessage> {
messages
.into_iter()
.filter(|msg| msg.role != "system") .map(|msg| AnthropicMessage {
role: msg.role,
content: vec![AnthropicContent::Text { text: msg.content }],
})
.collect()
}
fn convert_tool_definitions(
&self,
tools: Vec<crate::apis::api_client::ToolDefinition>,
) -> Vec<AnthropicTool> {
tools
.into_iter()
.map(|tool| {
let mut schema = serde_json::Map::new();
schema.insert(
"$schema".to_string(),
json!("https://json-schema.org/draft/2020-12/schema"),
);
schema.insert("type".to_string(), json!("object"));
if let Value::Object(params) = &tool.parameters {
if let Some(props) = params.get("properties") {
schema.insert("properties".to_string(), props.clone());
}
if let Some(required) = params.get("required") {
schema.insert("required".to_string(), required.clone());
}
}
AnthropicTool {
name: tool.name,
description: Some(tool.description),
schema: Value::Object(schema),
}
})
.collect()
}
}
#[async_trait]
impl ApiClient for AnthropicClient {
async fn complete(&self, messages: Vec<Message>, options: CompletionOptions) -> Result<String> {
let system_message = self.extract_system_message(&messages);
let converted_messages = self.convert_messages(messages);
let max_tokens = options.max_tokens.unwrap_or(2048) as usize;
let mut request = AnthropicRequest {
model: self.model.clone(),
messages: converted_messages,
max_tokens,
system: system_message,
temperature: options.temperature,
top_p: options.top_p,
tools: None,
tool_choice: None,
response_format: None,
};
if let Some(json_schema) = &options.json_schema {
request.response_format = Some(AnthropicResponseFormat {
format_type: "json".to_string(),
schema: serde_json::from_str(json_schema).ok(),
});
}
let response = self.send_request_with_retry(&request).await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(AppError::NetworkError(format!(
"Anthropic API error: {} - {}",
status, error_text
))
.into());
}
let response_text = response.text().await.map_err(|e| {
let error_msg = format!("Failed to get response text: {}", e);
eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
AppError::NetworkError(error_msg)
})?;
eprintln!(
"{}",
format_log_with_color(
LogLevel::Debug,
&format!(
"Anthropic API response received: {} bytes",
response_text.len()
)
)
);
let anthropic_response: AnthropicResponse =
serde_json::from_str(&response_text).map_err(|e| {
let error_msg = format!("Failed to parse Anthropic response: {}", e);
eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
AppError::Other(error_msg)
})?;
let mut text_content = String::new();
for content_item in &anthropic_response.content {
if let AnthropicContent::Text { text } = content_item {
text_content = text.clone();
break;
}
}
if text_content.is_empty() {
let error_msg = "No text content in Anthropic response".to_string();
eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
return Err(AppError::LLMError(error_msg).into());
}
let content = text_content;
Ok(content)
}
async fn complete_with_tools(
&self,
messages: Vec<Message>,
options: CompletionOptions,
tool_results: Option<Vec<ToolResult>>,
) -> Result<(String, Option<Vec<ToolCall>>)> {
let system_message = self.extract_system_message(&messages);
let mut converted_messages = self.convert_messages(messages);
if let Some(results) = tool_results {
for result in results {
let tool_call_id = if result.tool_call_id.is_empty() {
format!("tool-{}", rand::random::<u64>())
} else {
result.tool_call_id.clone()
};
let tool_use_msg = AnthropicMessage {
role: "assistant".to_string(),
content: vec![AnthropicContent::ToolUse {
id: tool_call_id.clone(),
name: "tool".to_string(), input: json!({}), }],
};
let tool_result_msg = AnthropicMessage {
role: "user".to_string(),
content: vec![AnthropicContent::ToolResult {
tool_call_id: tool_call_id.clone(),
content: result.output.clone(),
}],
};
converted_messages.push(tool_use_msg);
converted_messages.push(tool_result_msg);
}
}
let max_tokens = options.max_tokens.unwrap_or(2048) as usize;
let mut request = AnthropicRequest {
model: self.model.clone(),
messages: converted_messages,
max_tokens,
system: system_message,
temperature: options.temperature,
top_p: options.top_p,
tools: None,
tool_choice: None,
response_format: None,
};
if let Some(json_schema) = &options.json_schema {
if options.tools.is_none() {
request.response_format = Some(AnthropicResponseFormat {
format_type: "json".to_string(),
schema: serde_json::from_str(json_schema).ok(),
});
}
}
if let Some(tools) = options.tools {
let converted_tools = self.convert_tool_definitions(tools);
request.tools = Some(converted_tools);
request.tool_choice = Some(AnthropicToolChoice {
choice_type: if options.require_tool_use {
"required".to_string()
} else {
"auto".to_string()
},
});
}
let response = self.send_request_with_retry(&request).await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(AppError::NetworkError(format!(
"Anthropic API error: {} - {}",
status, error_text
))
.into());
}
let response_text = response.text().await.map_err(|e| {
let error_msg = format!("Failed to get response text: {}", e);
eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
AppError::NetworkError(error_msg)
})?;
eprintln!(
"{}",
format_log_with_color(
LogLevel::Debug,
&format!(
"Anthropic API response received: {} bytes",
response_text.len()
)
)
);
let anthropic_response: AnthropicResponse =
serde_json::from_str(&response_text).map_err(|e| {
let error_msg = format!("Failed to parse Anthropic response: {}", e);
eprintln!("{}", format_log_with_color(LogLevel::Error, &error_msg));
AppError::Other(error_msg)
})?;
let mut tool_calls_vec = Vec::new();
let mut text_content = String::new();
for content_item in &anthropic_response.content {
match content_item {
AnthropicContent::Text { text } => {
if text_content.is_empty() {
text_content = text.clone();
}
}
AnthropicContent::ToolUse { name, input, .. } => {
tool_calls_vec.push(crate::apis::api_client::ToolCall {
id: None, name: name.clone(),
arguments: input.clone(),
});
}
AnthropicContent::ToolResult { .. } => {
}
}
}
let content = if text_content.is_empty() {
String::new()
} else {
text_content
};
let tool_calls = if tool_calls_vec.is_empty() {
None
} else {
Some(tool_calls_vec)
};
Ok((content, tool_calls))
}
}