use std::collections::HashMap;
use crate::{
builder::LLMBackend,
chat::{ChatMessage, ChatProvider, ChatResponse, ChatRole, MessageType, Tool, ToolChoice, Usage},
completion::{CompletionProvider, CompletionRequest, CompletionResponse},
embedding::EmbeddingProvider,
error::LLMError,
models::{ModelListRawEntry, ModelListRequest, ModelListResponse, ModelsProvider},
stt::SpeechToTextProvider,
tts::TextToSpeechProvider,
FunctionCall, ToolCall,
};
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use chrono::{DateTime, Utc};
use futures::stream::Stream;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug)]
pub struct Anthropic {
pub api_key: String,
pub model: String,
pub max_tokens: u32,
pub temperature: f32,
pub timeout_seconds: u64,
pub system: String,
pub stream: bool,
pub top_p: Option<f32>,
pub top_k: Option<u32>,
pub tools: Option<Vec<Tool>>,
pub tool_choice: Option<ToolChoice>,
pub reasoning: bool,
pub thinking_budget_tokens: Option<u32>,
client: Client,
}
#[derive(Serialize, Debug)]
struct AnthropicTool<'a> {
name: &'a str,
description: &'a str,
#[serde(rename = "input_schema")]
schema: &'a serde_json::Value,
}
#[derive(Serialize, Debug)]
struct ThinkingConfig {
#[serde(rename = "type")]
thinking_type: String,
budget_tokens: u32,
}
#[derive(Serialize, Debug)]
struct AnthropicCompleteRequest<'a> {
messages: Vec<AnthropicMessage<'a>>,
model: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
top_k: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<AnthropicTool<'a>>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
thinking: Option<ThinkingConfig>,
}
#[derive(Serialize, Debug)]
struct AnthropicMessage<'a> {
role: &'a str,
content: Vec<MessageContent<'a>>,
}
#[derive(Serialize, Debug)]
struct MessageContent<'a> {
#[serde(rename = "type")]
message_type: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
image_url: Option<ImageUrlContent<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<ImageSource<'a>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "id")]
tool_use_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "name")]
tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "input")]
tool_input: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none", rename = "tool_use_id")]
tool_result_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "content")]
tool_output: Option<String>,
}
#[derive(Serialize, Debug)]
struct ImageUrlContent<'a> {
url: &'a str,
}
#[derive(Serialize, Debug)]
struct ImageSource<'a> {
#[serde(rename = "type")]
source_type: &'a str,
media_type: &'a str,
data: String,
}
#[derive(Deserialize, Debug)]
struct AnthropicCompleteResponse {
content: Vec<AnthropicContent>,
usage: Option<AnthropicUsage>,
}
#[derive(Deserialize, Debug)]
struct AnthropicUsage {
input_tokens: u32,
output_tokens: u32,
}
#[derive(Serialize, Deserialize, Debug)]
struct AnthropicContent {
text: Option<String>,
#[serde(rename = "type")]
content_type: Option<String>,
thinking: Option<String>,
name: Option<String>,
input: Option<serde_json::Value>,
id: Option<String>,
}
#[derive(Deserialize, Debug)]
struct AnthropicStreamResponse {
#[serde(rename = "type")]
response_type: String,
delta: Option<AnthropicDelta>,
}
#[derive(Deserialize, Debug)]
struct AnthropicDelta {
#[allow(dead_code)]
#[serde(rename = "type")]
delta_type: Option<String>,
text: Option<String>,
}
impl std::fmt::Display for AnthropicCompleteResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for content in self.content.iter() {
match content.content_type {
Some(ref t) if t == "tool_use" => write!(
f,
"{{\n \"name\": {}, \"input\": {}\n}}",
content.name.clone().unwrap_or_default(),
content.input.clone().unwrap_or(serde_json::Value::Null)
)?,
Some(ref t) if t == "thinking" => {
write!(f, "{}", content.thinking.clone().unwrap_or_default())?
}
_ => write!(
f,
"{}",
self.content
.iter()
.map(|c| c.text.clone().unwrap_or_default())
.collect::<Vec<_>>()
.join("\n")
)?,
}
}
Ok(())
}
}
impl ChatResponse for AnthropicCompleteResponse {
fn text(&self) -> Option<String> {
Some(
self.content
.iter()
.filter_map(|c| {
if c.content_type == Some("text".to_string()) || c.content_type.is_none() {
c.text.clone()
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n"),
)
}
fn thinking(&self) -> Option<String> {
self.content
.iter()
.find(|c| c.content_type == Some("thinking".to_string()))
.and_then(|c| c.thinking.clone())
}
fn tool_calls(&self) -> Option<Vec<ToolCall>> {
match self
.content
.iter()
.filter_map(|c| {
if c.content_type == Some("tool_use".to_string()) {
Some(ToolCall {
id: c.id.clone().unwrap_or_default(),
call_type: "function".to_string(),
function: FunctionCall {
name: c.name.clone().unwrap_or_default(),
arguments: serde_json::to_string(
&c.input.clone().unwrap_or(serde_json::Value::Null),
)
.unwrap_or_default(),
},
})
} else {
None
}
})
.collect::<Vec<ToolCall>>()
{
v if v.is_empty() => None,
v => Some(v),
}
}
fn usage(&self) -> Option<Usage> {
self.usage.as_ref().map(|u| Usage {
prompt_tokens: u.input_tokens,
completion_tokens: u.output_tokens,
total_tokens: u.input_tokens + u.output_tokens,
})
}
}
impl Anthropic {
#[allow(clippy::too_many_arguments)]
pub fn new(
api_key: impl Into<String>,
model: Option<String>,
max_tokens: Option<u32>,
temperature: Option<f32>,
timeout_seconds: Option<u64>,
system: Option<String>,
stream: Option<bool>,
top_p: Option<f32>,
top_k: Option<u32>,
tools: Option<Vec<Tool>>,
tool_choice: Option<ToolChoice>,
reasoning: Option<bool>,
thinking_budget_tokens: Option<u32>,
) -> Self {
let mut builder = Client::builder();
if let Some(sec) = timeout_seconds {
builder = builder.timeout(std::time::Duration::from_secs(sec));
}
Self {
api_key: api_key.into(),
model: model.unwrap_or_else(|| "claude-3-sonnet-20240229".to_string()),
max_tokens: max_tokens.unwrap_or(300),
temperature: temperature.unwrap_or(0.7),
system: system.unwrap_or_else(|| "You are a helpful assistant.".to_string()),
timeout_seconds: timeout_seconds.unwrap_or(30),
stream: stream.unwrap_or(false),
top_p,
top_k,
tools,
tool_choice,
reasoning: reasoning.unwrap_or(false),
thinking_budget_tokens,
client: builder.build().expect("Failed to build reqwest Client"),
}
}
}
#[async_trait]
impl ChatProvider for Anthropic {
async fn chat_with_tools(
&self,
messages: &[ChatMessage],
tools: Option<&[Tool]>,
) -> Result<Box<dyn ChatResponse>, LLMError> {
if self.api_key.is_empty() {
return Err(LLMError::AuthError("Missing Anthropic API key".to_string()));
}
let anthropic_messages: Vec<AnthropicMessage> = messages
.iter()
.map(|m| AnthropicMessage {
role: match m.role {
ChatRole::User => "user",
ChatRole::Assistant => "assistant",
},
content: match &m.message_type {
MessageType::Text => vec![MessageContent {
message_type: Some("text"),
text: Some(&m.content),
image_url: None,
source: None,
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: None,
tool_output: None,
}],
MessageType::Pdf(_) => unimplemented!(),
MessageType::Image((image_mime, raw_bytes)) => {
vec![MessageContent {
message_type: Some("image"),
text: None,
image_url: None,
source: Some(ImageSource {
source_type: "base64",
media_type: image_mime.mime_type(),
data: BASE64.encode(raw_bytes),
}),
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: None,
tool_output: None,
}]
}
MessageType::ImageURL(url) => vec![MessageContent {
message_type: Some("image_url"),
text: None,
image_url: Some(ImageUrlContent { url }),
source: None,
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: None,
tool_output: None,
}],
MessageType::ToolUse(calls) => calls
.iter()
.map(|c| MessageContent {
message_type: Some("tool_use"),
text: None,
image_url: None,
source: None,
tool_use_id: Some(c.id.clone()),
tool_input: Some(
serde_json::from_str(&c.function.arguments)
.unwrap_or(c.function.arguments.clone().into()),
),
tool_name: Some(c.function.name.clone()),
tool_result_id: None,
tool_output: None,
})
.collect(),
MessageType::ToolResult(responses) => responses
.iter()
.map(|r| MessageContent {
message_type: Some("tool_result"),
text: None,
image_url: None,
source: None,
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: Some(r.id.clone()),
tool_output: Some(r.function.arguments.clone()),
})
.collect(),
},
})
.collect();
let maybe_tool_slice: Option<&[Tool]> = tools.or(self.tools.as_deref());
let anthropic_tools = maybe_tool_slice.map(|slice| {
slice
.iter()
.map(|tool| AnthropicTool {
name: &tool.function.name,
description: &tool.function.description,
schema: &tool.function.parameters,
})
.collect::<Vec<_>>()
});
let tool_choice = match self.tool_choice {
Some(ToolChoice::Auto) => {
Some(HashMap::from([("type".to_string(), "auto".to_string())]))
}
Some(ToolChoice::Any) => Some(HashMap::from([("type".to_string(), "any".to_string())])),
Some(ToolChoice::Tool(ref tool_name)) => Some(HashMap::from([
("type".to_string(), "tool".to_string()),
("name".to_string(), tool_name.clone()),
])),
Some(ToolChoice::None) => {
Some(HashMap::from([("type".to_string(), "none".to_string())]))
}
None => None,
};
let final_tool_choice = if anthropic_tools.is_some() {
tool_choice.clone()
} else {
None
};
let thinking = if self.reasoning {
Some(ThinkingConfig {
thinking_type: "enabled".to_string(),
budget_tokens: self.thinking_budget_tokens.unwrap_or(16000),
})
} else {
None
};
let req_body = AnthropicCompleteRequest {
messages: anthropic_messages,
model: &self.model,
max_tokens: Some(self.max_tokens),
temperature: Some(self.temperature),
system: Some(&self.system),
stream: Some(self.stream),
top_p: self.top_p,
top_k: self.top_k,
tools: anthropic_tools,
tool_choice: final_tool_choice,
thinking,
};
let mut request = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.json(&req_body);
if self.timeout_seconds > 0 {
request = request.timeout(std::time::Duration::from_secs(self.timeout_seconds));
}
if log::log_enabled!(log::Level::Trace) {
if let Ok(json) = serde_json::to_string(&req_body) {
log::trace!("Anthropic request payload: {json}");
}
}
log::debug!("Anthropic request: POST /v1/messages");
let resp = request.send().await?;
log::debug!("Anthropic HTTP status: {}", resp.status());
let resp = resp.error_for_status()?;
let body = resp.text().await?;
let json_resp: AnthropicCompleteResponse = serde_json::from_str(&body)
.map_err(|e| LLMError::HttpError(format!("Failed to parse JSON: {e}")))?;
Ok(Box::new(json_resp))
}
async fn chat(&self, messages: &[ChatMessage]) -> Result<Box<dyn ChatResponse>, LLMError> {
self.chat_with_tools(messages, None).await
}
async fn chat_stream(
&self,
messages: &[ChatMessage],
) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<String, LLMError>> + Send>>, LLMError>
{
if self.api_key.is_empty() {
return Err(LLMError::AuthError("Missing Anthropic API key".to_string()));
}
let anthropic_messages: Vec<AnthropicMessage> = messages
.iter()
.map(|m| AnthropicMessage {
role: match m.role {
ChatRole::User => "user",
ChatRole::Assistant => "assistant",
},
content: match &m.message_type {
MessageType::Text => vec![MessageContent {
message_type: Some("text"),
text: Some(&m.content),
image_url: None,
source: None,
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: None,
tool_output: None,
}],
MessageType::Pdf(_) => unimplemented!(),
MessageType::Image((image_mime, raw_bytes)) => {
vec![MessageContent {
message_type: Some("image"),
text: None,
image_url: None,
source: Some(ImageSource {
source_type: "base64",
media_type: image_mime.mime_type(),
data: BASE64.encode(raw_bytes),
}),
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: None,
tool_output: None,
}]
}
_ => vec![MessageContent {
message_type: Some("text"),
text: Some(&m.content),
image_url: None,
source: None,
tool_use_id: None,
tool_input: None,
tool_name: None,
tool_result_id: None,
tool_output: None,
}],
},
})
.collect();
let req_body = AnthropicCompleteRequest {
messages: anthropic_messages,
model: &self.model,
max_tokens: Some(self.max_tokens),
temperature: Some(self.temperature),
system: Some(&self.system),
stream: Some(true),
top_p: self.top_p,
top_k: self.top_k,
tools: None,
tool_choice: None,
thinking: None,
};
let mut request = self
.client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.json(&req_body);
if self.timeout_seconds > 0 {
request = request.timeout(std::time::Duration::from_secs(self.timeout_seconds));
}
let response = request.send().await?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await?;
return Err(LLMError::ResponseFormatError {
message: format!("Anthropic API returned error status: {status}"),
raw_response: error_text,
});
}
Ok(crate::chat::create_sse_stream(
response,
parse_anthropic_sse_chunk,
))
}
}
#[async_trait]
impl CompletionProvider for Anthropic {
async fn complete(&self, _req: &CompletionRequest) -> Result<CompletionResponse, LLMError> {
unimplemented!()
}
}
#[async_trait]
impl EmbeddingProvider for Anthropic {
async fn embed(&self, _text: Vec<String>) -> Result<Vec<Vec<f32>>, LLMError> {
Err(LLMError::ProviderError(
"Embedding not supported".to_string(),
))
}
}
#[async_trait]
impl SpeechToTextProvider for Anthropic {
async fn transcribe(&self, _audio: Vec<u8>) -> Result<String, LLMError> {
Err(LLMError::ProviderError(
"Speech to text not supported".to_string(),
))
}
}
#[async_trait]
impl TextToSpeechProvider for Anthropic {}
#[derive(Clone, Debug, Deserialize)]
pub struct AnthropicModelListResponse {
data: Vec<AnthropicModelEntry>,
}
impl ModelListResponse for AnthropicModelListResponse {
fn get_models(&self) -> Vec<String> {
self.data.iter().map(|m| m.id.clone()).collect()
}
fn get_models_raw(&self) -> Vec<Box<dyn ModelListRawEntry>> {
self.data
.iter()
.map(|e| Box::new(e.clone()) as Box<dyn ModelListRawEntry>)
.collect()
}
fn get_backend(&self) -> LLMBackend {
LLMBackend::Anthropic
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct AnthropicModelEntry {
created_at: DateTime<Utc>,
id: String,
#[serde(flatten)]
extra: Value,
}
impl ModelListRawEntry for AnthropicModelEntry {
fn get_id(&self) -> String {
self.id.clone()
}
fn get_created_at(&self) -> DateTime<Utc> {
self.created_at
}
fn get_raw(&self) -> Value {
self.extra.clone()
}
}
#[async_trait]
impl ModelsProvider for Anthropic {
async fn list_models(
&self,
_request: Option<&ModelListRequest>,
) -> Result<Box<dyn ModelListResponse>, LLMError> {
let resp = self
.client
.get("https://api.anthropic.com/v1/models")
.header("x-api-key", &self.api_key)
.header("Content-Type", "application/json")
.header("anthropic-version", "2023-06-01")
.send()
.await?;
let result: AnthropicModelListResponse = resp.json().await?;
Ok(Box::new(result))
}
}
impl crate::LLMProvider for Anthropic {
fn tools(&self) -> Option<&[Tool]> {
self.tools.as_deref()
}
}
fn parse_anthropic_sse_chunk(chunk: &str) -> Result<Option<String>, LLMError> {
for line in chunk.lines() {
let line = line.trim();
if let Some(data) = line.strip_prefix("data: ") {
match serde_json::from_str::<AnthropicStreamResponse>(data) {
Ok(response) => {
if response.response_type == "content_block_delta" {
if let Some(delta) = response.delta {
if let Some(text) = delta.text {
return Ok(Some(text));
}
}
}
return Ok(None);
}
Err(_) => continue,
}
}
}
Ok(None)
}