use crate::core::Protocol;
use crate::types::{ChatRequest, ChatResponse, Message, Role, Choice, Usage};
use crate::error::LlmConnectorError;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct AnthropicProtocol {
api_key: String,
}
impl AnthropicProtocol {
pub fn new(api_key: &str) -> Self {
Self {
api_key: api_key.to_string(),
}
}
pub fn api_key(&self) -> &str {
&self.api_key
}
}
#[async_trait]
impl Protocol for AnthropicProtocol {
type Request = AnthropicRequest;
type Response = AnthropicResponse;
fn name(&self) -> &str {
"anthropic"
}
fn chat_endpoint(&self, base_url: &str) -> String {
format!("{}/v1/messages", base_url.trim_end_matches('/'))
}
fn build_request(&self, request: &ChatRequest) -> Result<Self::Request, LlmConnectorError> {
let mut system_message = None;
let mut messages = Vec::new();
for msg in &request.messages {
match msg.role {
Role::System => {
if system_message.is_none() {
system_message = Some(msg.content.clone());
} else {
let existing = system_message.take().unwrap_or_default();
system_message = Some(format!("{}\n\n{}", existing, msg.content));
}
}
Role::User => {
messages.push(AnthropicMessage {
role: "user".to_string(),
content: msg.content.clone(),
});
}
Role::Assistant => {
messages.push(AnthropicMessage {
role: "assistant".to_string(),
content: msg.content.clone(),
});
}
Role::Tool => {
messages.push(AnthropicMessage {
role: "user".to_string(),
content: format!("Tool result: {}", msg.content),
});
}
}
}
Ok(AnthropicRequest {
model: request.model.clone(),
max_tokens: request.max_tokens.unwrap_or(1024), messages,
system: system_message,
temperature: request.temperature,
top_p: request.top_p,
stream: request.stream,
})
}
fn parse_response(&self, response: &str) -> Result<ChatResponse, LlmConnectorError> {
let anthropic_response: AnthropicResponse = serde_json::from_str(response)
.map_err(|e| LlmConnectorError::ParseError(format!("Failed to parse Anthropic response: {}", e)))?;
let content = anthropic_response.content.first()
.map(|c| c.text.clone())
.unwrap_or_default();
let choices = vec![Choice {
index: 0,
message: Message {
role: Role::Assistant,
content: content.clone(),
name: None,
tool_calls: None,
tool_call_id: None,
reasoning_content: None,
reasoning: None,
thought: None,
thinking: None,
},
finish_reason: Some(anthropic_response.stop_reason.unwrap_or_else(|| "stop".to_string())),
logprobs: None,
}];
let usage = Some(Usage {
prompt_tokens: anthropic_response.usage.input_tokens,
completion_tokens: anthropic_response.usage.output_tokens,
total_tokens: anthropic_response.usage.input_tokens + anthropic_response.usage.output_tokens,
completion_tokens_details: None,
prompt_cache_hit_tokens: None,
prompt_cache_miss_tokens: None,
prompt_tokens_details: None,
});
Ok(ChatResponse {
id: anthropic_response.id,
object: "chat.completion".to_string(),
created: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
model: anthropic_response.model,
choices,
content,
usage,
system_fingerprint: None,
})
}
fn map_error(&self, status: u16, body: &str) -> LlmConnectorError {
let error_info = serde_json::from_str::<serde_json::Value>(body)
.ok()
.and_then(|v| v.get("error").cloned())
.unwrap_or_else(|| serde_json::json!({"message": body}));
let message = error_info.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Unknown Anthropic error");
match status {
400 => LlmConnectorError::InvalidRequest(format!("Anthropic: {}", message)),
401 => LlmConnectorError::AuthenticationError(format!("Anthropic: {}", message)),
403 => LlmConnectorError::PermissionError(format!("Anthropic: {}", message)),
429 => LlmConnectorError::RateLimitError(format!("Anthropic: {}", message)),
500..=599 => LlmConnectorError::ServerError(format!("Anthropic: {}", message)),
_ => LlmConnectorError::ApiError(format!("Anthropic HTTP {}: {}", status, message)),
}
}
fn auth_headers(&self) -> Vec<(String, String)> {
vec![
("x-api-key".to_string(), self.api_key.clone()),
("Content-Type".to_string(), "application/json".to_string()),
("anthropic-version".to_string(), "2023-06-01".to_string()),
]
}
}
#[derive(Serialize, Debug)]
pub struct AnthropicRequest {
pub model: String,
pub max_tokens: u32,
pub messages: Vec<AnthropicMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
}
#[derive(Serialize, Debug)]
pub struct AnthropicMessage {
pub role: String,
pub content: String,
}
#[derive(Deserialize, Debug)]
pub struct AnthropicResponse {
pub id: String,
pub model: String,
pub content: Vec<AnthropicContent>,
pub stop_reason: Option<String>,
pub usage: AnthropicUsage,
}
#[derive(Deserialize, Debug)]
pub struct AnthropicContent {
#[serde(rename = "type")]
pub content_type: String,
pub text: String,
}
#[derive(Deserialize, Debug)]
pub struct AnthropicUsage {
pub input_tokens: u32,
pub output_tokens: u32,
}