use anyhow::{Context, Result};
use async_trait::async_trait;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use super::provider::ModelProvider;
use super::types::*;
pub struct AnthropicProvider {
client: Client,
api_key: String,
model: String,
base_url: String,
}
impl AnthropicProvider {
pub fn new(api_key: String, model: Option<String>, base_url: Option<String>) -> Self {
Self {
client: Client::new(),
api_key,
model: model.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string()),
base_url: base_url.unwrap_or_else(|| "https://api.anthropic.com".to_string()),
}
}
fn build_request_body(&self, request: &CompletionRequest) -> AnthropicRequest {
let messages: Vec<AnthropicMessage> = request
.messages
.iter()
.map(|msg| AnthropicMessage {
role: match msg.role {
Role::User => "user".to_string(),
Role::Assistant => "assistant".to_string(),
Role::System => "user".to_string(), },
content: msg
.content
.iter()
.map(|block| match block {
ContentBlock::Text { text } => AnthropicContent::Text {
text: text.clone(),
},
ContentBlock::ToolUse { id, name, input } => AnthropicContent::ToolUse {
id: id.clone(),
name: name.clone(),
input: input.clone(),
},
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => AnthropicContent::ToolResult {
tool_use_id: tool_use_id.clone(),
content: content.clone(),
is_error: *is_error,
},
})
.collect(),
})
.collect();
let tools: Vec<AnthropicTool> = request
.tools
.iter()
.map(|t| AnthropicTool {
name: t.name.clone(),
description: t.description.clone(),
input_schema: t.input_schema.clone(),
})
.collect();
AnthropicRequest {
model: self.model.clone(),
max_tokens: request.max_tokens,
system: if request.system.is_empty() {
None
} else {
Some(request.system.clone())
},
messages,
tools: if tools.is_empty() { None } else { Some(tools) },
}
}
}
#[async_trait]
impl ModelProvider for AnthropicProvider {
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
let url = format!("{}/v1/messages", self.base_url);
let body = self.build_request_body(&request);
let response = self
.client
.post(&url)
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&body)
.send()
.await
.context("Failed to send request to Anthropic API")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Anthropic API error ({}): {}", status, error_text);
}
let api_response: AnthropicResponse = response
.json()
.await
.context("Failed to parse Anthropic API response")?;
let content: Vec<ContentBlock> = api_response
.content
.into_iter()
.map(|block| match block {
AnthropicContent::Text { text, .. } => ContentBlock::Text { text },
AnthropicContent::ToolUse { id, name, input, .. } => {
ContentBlock::ToolUse { id, name, input }
}
AnthropicContent::ToolResult { .. } => {
ContentBlock::Text {
text: "[tool_result in response]".to_string(),
}
}
})
.collect();
let stop_reason = match api_response.stop_reason.as_deref() {
Some("end_turn") => StopReason::EndTurn,
Some("tool_use") => StopReason::ToolUse,
Some("max_tokens") => StopReason::MaxTokens,
Some("stop_sequence") => StopReason::StopSequence,
_ => StopReason::EndTurn,
};
let usage = api_response.usage.map(|u| Usage {
input_tokens: u.input_tokens,
output_tokens: u.output_tokens,
});
Ok(CompletionResponse {
content,
stop_reason,
usage,
})
}
fn name(&self) -> &str {
"anthropic"
}
fn model_id(&self) -> &str {
&self.model
}
fn supports_tools(&self) -> bool {
true
}
}
#[derive(Serialize)]
struct AnthropicRequest {
model: String,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
messages: Vec<AnthropicMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<AnthropicTool>>,
}
#[derive(Serialize)]
struct AnthropicMessage {
role: String,
content: Vec<AnthropicContent>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum AnthropicContent {
#[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,
#[serde(default)]
is_error: bool,
},
}
#[derive(Serialize)]
struct AnthropicTool {
name: String,
description: String,
input_schema: serde_json::Value,
}
#[derive(Deserialize)]
struct AnthropicResponse {
content: Vec<AnthropicContent>,
stop_reason: Option<String>,
usage: Option<AnthropicUsage>,
}
#[derive(Deserialize)]
struct AnthropicUsage {
input_tokens: u32,
output_tokens: u32,
}