use crate::core::Protocol;
use crate::error::LlmConnectorError;
use crate::protocols::common::capabilities::ProviderCapabilities;
use crate::types::{
ChatRequest, ChatResponse, Choice, FunctionCall, Message, Role, ToolCall, ToolChoice, Usage,
};
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 capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities::anthropic()
}
fn chat_endpoint(&self, base_url: &str, _model: &str) -> String {
let base = base_url.trim_end_matches('/');
if base.ends_with("/v1") {
format!("{}/messages", base)
} else {
format!("{}/v1/messages", base)
}
}
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 => {
let text = msg.content_as_text();
if system_message.is_none() {
system_message = Some(text);
} else {
let existing = system_message.take().unwrap_or_default();
system_message = Some(format!("{}\n\n{}", existing, text));
}
}
Role::User => {
let content = serde_json::to_value(&msg.content).unwrap();
messages.push(AnthropicMessage {
role: "user".to_string(),
content,
});
}
Role::Assistant => {
let mut content = serde_json::to_value(&msg.content).unwrap_or_else(|_| {
serde_json::Value::Array(vec![serde_json::json!({
"type": "text",
"text": msg.content_as_text()
})])
});
if !content.is_array() {
content = serde_json::Value::Array(vec![serde_json::json!({
"type": "text",
"text": msg.content_as_text()
})]);
}
if let (Some(tool_calls), Some(content_arr)) =
(&msg.tool_calls, content.as_array_mut())
{
for (index, tool_call) in tool_calls.iter().enumerate() {
let tool_id = if tool_call.id.is_empty() {
format!("toolu_{}", index)
} else {
tool_call.id.clone()
};
let input = tool_call.arguments_value().unwrap_or_else(|_| {
serde_json::json!({
"_raw": tool_call.function.arguments
})
});
content_arr.push(serde_json::json!({
"type": "tool_use",
"id": tool_id,
"name": tool_call.function.name,
"input": input
}));
}
}
messages.push(AnthropicMessage {
role: "assistant".to_string(),
content,
});
}
Role::Tool => {
let tool_use_id = msg.tool_call_id.clone().unwrap_or_default();
let text = msg.content_as_text();
messages.push(AnthropicMessage {
role: "user".to_string(),
content: serde_json::json!([{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": text
}]),
});
}
}
}
let reasoning_parts = crate::protocols::common::thinking::map_reasoning_request_parts(
request,
self.capabilities(),
);
let mut max_tokens = request.max_tokens.unwrap_or(1024);
let thinking = if let Some(budget) = reasoning_parts.thinking_budget {
if max_tokens <= budget {
max_tokens = budget + 4096;
}
Some(AnthropicThinking {
thinking_type: "enabled".to_string(),
budget_tokens: budget,
})
} else {
None
};
Ok(AnthropicRequest {
model: request.model.clone(),
max_tokens,
messages,
system: system_message,
temperature: request.temperature,
top_p: request.top_p,
stream: request.stream,
thinking,
tools: request.tools.as_ref().map(|tools| {
tools
.iter()
.map(|tool| AnthropicTool {
name: tool.function.name.clone(),
description: tool.function.description.clone(),
input_schema: tool.function.parameters.clone(),
})
.collect()
}),
tool_choice: request
.tool_choice
.as_ref()
.and_then(map_tool_choice_to_anthropic),
})
}
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 mut message_blocks = Vec::new();
let mut thinking_content = String::new();
let mut text_content = String::new();
let mut tool_calls = Vec::new();
for block in &anthropic_response.content {
match block.content_type.as_str() {
"text" => {
if let Some(text) = &block.text {
message_blocks.push(crate::types::MessageBlock::text(text));
text_content.push_str(text);
}
}
"thinking" => {
if let Some(thinking) = &block.thinking {
thinking_content.push_str(thinking);
}
}
"tool_use" => {
if let (Some(id), Some(name)) = (&block.id, &block.name) {
let arguments = block
.input
.as_ref()
.map(|v| v.to_string())
.unwrap_or_else(|| "{}".to_string());
tool_calls.push(ToolCall {
id: id.clone(),
call_type: "function".to_string(),
function: FunctionCall {
name: name.clone(),
arguments,
thought_signature: None,
},
index: Some(tool_calls.len()),
thought_signature: None,
});
}
}
_ => {
}
}
}
let thinking = if !thinking_content.is_empty() {
Some(thinking_content)
} else {
None
};
let choices = vec![Choice {
index: 0,
message: Message {
role: Role::Assistant,
content: message_blocks,
name: None,
tool_calls: if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
},
tool_call_id: None,
reasoning_content: None,
reasoning: None,
thought: None,
thinking,
},
finish_reason: Some(
anthropic_response
.stop_reason
.map(|reason| {
if reason == "tool_use" {
"tool_calls".to_string()
} else {
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: anthropic_response.usage.cache_read_input_tokens,
prompt_cache_miss_tokens: None,
prompt_tokens_details: Some(crate::types::PromptTokensDetails {
cached_tokens: anthropic_response.usage.cache_read_input_tokens,
cache_read_input_tokens: anthropic_response.usage.cache_read_input_tokens,
cache_creation_input_tokens: anthropic_response.usage.cache_creation_input_tokens,
}),
});
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: text_content,
reasoning_content: None,
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");
let error_type = error_info
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("");
let msg = format!("Anthropic: {}", message);
if error_type == "invalid_request_error"
&& (message.contains("too long")
|| message.contains("maximum")
|| message.contains("context"))
{
return LlmConnectorError::ContextLengthExceeded(msg);
}
match status {
400 => LlmConnectorError::InvalidRequest(msg),
401 => LlmConnectorError::AuthenticationError(msg),
403 => LlmConnectorError::PermissionError(msg),
429 => LlmConnectorError::RateLimitError(msg),
500..=599 => LlmConnectorError::ServerError(msg),
_ => LlmConnectorError::ApiError(format!("Anthropic HTTP {}: {}", status, message)),
}
}
fn auth_headers(&self) -> Vec<(String, String)> {
let mut headers =
crate::protocols::common::auth::api_key_header(&self.api_key, "x-api-key");
headers.push(("anthropic-version".to_string(), "2023-06-01".to_string()));
headers
}
fn build_auth_headers_for_override(&self, api_key: &str) -> Vec<(String, String)> {
crate::protocols::common::auth::api_key_header(api_key, "x-api-key")
}
#[cfg(feature = "streaming")]
async fn parse_stream_response(
&self,
response: reqwest::Response,
) -> Result<crate::types::ChatStream, LlmConnectorError> {
use std::sync::{Arc, Mutex};
let message_id = Arc::new(Mutex::new(String::new()));
Ok(crate::protocols::common::streamers::map_sse_json_stream(
response,
move |json_str| {
let message_id = message_id.clone();
let event = serde_json::from_str::<serde_json::Value>(&json_str).map_err(|e| {
LlmConnectorError::ParseError(format!(
"Failed to parse Anthropic streaming event: {}. JSON: {}",
e, json_str
))
})?;
let event_type = event.get("type").and_then(|t| t.as_str()).unwrap_or("");
match event_type {
"message_start" => {
if let Some(msg_id) = event
.get("message")
.and_then(|m| m.get("id"))
.and_then(|id| id.as_str())
&& let Ok(mut id) = message_id.lock()
{
*id = msg_id.to_string();
}
Ok(None)
}
"content_block_start" | "content_block_delta" | "message_delta" => {
let id = message_id.lock().map(|id| id.clone()).unwrap_or_default();
crate::protocols::common::streamers::interpret_anthropic_event(&event, &id)
}
_ => Ok(None),
}
},
))
}
}
#[derive(Serialize, Deserialize, 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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<AnthropicThinking>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<AnthropicTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<AnthropicToolChoice>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AnthropicThinking {
#[serde(rename = "type")]
pub thinking_type: String,
pub budget_tokens: u32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AnthropicMessage {
pub role: String,
pub content: serde_json::Value, }
#[derive(Serialize, Deserialize, Debug)]
pub struct AnthropicTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AnthropicToolChoice {
#[serde(rename = "type")]
pub choice_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<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: Option<String>,
pub thinking: Option<String>,
pub signature: Option<String>,
pub id: Option<String>,
pub name: Option<String>,
pub input: Option<serde_json::Value>,
}
#[derive(Deserialize, Debug)]
pub struct AnthropicUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub cache_creation_input_tokens: Option<u32>,
pub cache_read_input_tokens: Option<u32>,
}
fn map_tool_choice_to_anthropic(tool_choice: &ToolChoice) -> Option<AnthropicToolChoice> {
match tool_choice {
ToolChoice::Mode(mode) => match mode.as_str() {
"none" => None,
"required" => Some(AnthropicToolChoice {
choice_type: "any".to_string(),
name: None,
}),
"auto" => Some(AnthropicToolChoice {
choice_type: "auto".to_string(),
name: None,
}),
_ => Some(AnthropicToolChoice {
choice_type: "auto".to_string(),
name: None,
}),
},
ToolChoice::Function { function, .. } => Some(AnthropicToolChoice {
choice_type: "tool".to_string(),
name: Some(function.name.clone()),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Tool;
#[test]
fn test_anthropic_parsing_with_thinking_and_text() {
let response_json = r#"
{
"id": "msg_123",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet-20241022",
"content": [
{
"type": "thinking",
"thinking": "This is a thinking block",
"signature": "sig_123"
},
{
"type": "text",
"text": "Hello there, nice to meet."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
}
"#;
let protocol = AnthropicProtocol::new("test-key");
let result = protocol.parse_response(response_json).unwrap();
assert_eq!(result.content, "Hello there, nice to meet.");
assert_eq!(
result.choices[0].message.content[0].as_text().unwrap(),
"Hello there, nice to meet."
);
assert_eq!(
result.choices[0].message.thinking.as_deref(),
Some("This is a thinking block")
);
}
#[test]
fn test_anthropic_parsing_only_thinking() {
let response_json = r#"
{
"id": "msg_124",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet-20241022",
"content": [
{
"type": "thinking",
"thinking": "Just thinking...",
"signature": "sig_124"
}
],
"stop_reason": "max_tokens",
"stop_sequence": null,
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
}
"#;
let protocol = AnthropicProtocol::new("test-key");
let result = protocol.parse_response(response_json).unwrap();
assert_eq!(result.content, "");
assert!(result.choices[0].message.content.is_empty());
assert_eq!(
result.choices[0].message.thinking.as_deref(),
Some("Just thinking...")
);
}
#[test]
fn test_anthropic_request_single_tool_with_tool_use_and_result() {
let protocol = AnthropicProtocol::new("test-key");
let tool = Tool::function(
"get_weather",
Some("Get weather".to_string()),
serde_json::json!({
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}),
);
let assistant_with_tool = Message::assistant_with_tool_calls(vec![ToolCall {
id: "toolu_1".to_string(),
call_type: "function".to_string(),
function: FunctionCall {
name: "get_weather".to_string(),
arguments: r#"{"city":"beijing"}"#.to_string(),
thought_signature: None,
},
index: Some(0),
thought_signature: None,
}]);
let tool_result = Message::tool(r#"{"temp":26}"#, "toolu_1");
let request = ChatRequest::new("claude-3-5-sonnet")
.with_tools(vec![tool])
.with_tool_choice(ToolChoice::function("get_weather"))
.add_message(Message::user("weather?"))
.add_message(assistant_with_tool)
.add_message(tool_result);
let mapped = protocol.build_request(&request).unwrap();
assert_eq!(mapped.tools.as_ref().map(|v| v.len()), Some(1));
assert_eq!(
mapped.tool_choice.as_ref().map(|c| c.choice_type.as_str()),
Some("tool")
);
assert_eq!(
mapped.tool_choice.as_ref().and_then(|c| c.name.as_deref()),
Some("get_weather")
);
let assistant_content = mapped.messages[1].content.as_array().unwrap();
assert_eq!(assistant_content[0]["type"], "tool_use");
assert_eq!(assistant_content[0]["id"], "toolu_1");
assert_eq!(assistant_content[0]["name"], "get_weather");
assert_eq!(assistant_content[0]["input"]["city"], "beijing");
let tool_result_content = mapped.messages[2].content.as_array().unwrap();
assert_eq!(tool_result_content[0]["type"], "tool_result");
assert_eq!(tool_result_content[0]["tool_use_id"], "toolu_1");
}
#[test]
fn test_anthropic_request_multi_tools_and_auto_choice() {
let protocol = AnthropicProtocol::new("test-key");
let request = ChatRequest::new("claude-3-5-sonnet")
.with_tools(vec![
Tool::function(
"get_weather",
Some("Get weather".to_string()),
serde_json::json!({"type":"object"}),
),
Tool::function(
"get_time",
Some("Get time".to_string()),
serde_json::json!({"type":"object"}),
),
])
.with_tool_choice(ToolChoice::auto())
.add_message(Message::user("hi"));
let mapped = protocol.build_request(&request).unwrap();
assert_eq!(mapped.tools.as_ref().map(|v| v.len()), Some(2));
assert_eq!(
mapped.tool_choice.as_ref().map(|c| c.choice_type.as_str()),
Some("auto")
);
}
#[test]
fn test_anthropic_request_required_tool_choice_serialization() {
let protocol = AnthropicProtocol::new("test-key");
let request = ChatRequest::new("claude-3-5-sonnet")
.with_tools(vec![Tool::function(
"get_weather",
Some("Get weather".to_string()),
serde_json::json!({"type":"object"}),
)])
.with_tool_choice(ToolChoice::required())
.add_message(Message::user("hi"));
let mapped = protocol.build_request(&request).unwrap();
assert_eq!(mapped.tools.as_ref().map(|v| v.len()), Some(1));
assert_eq!(
mapped.tool_choice.as_ref().map(|c| c.choice_type.as_str()),
Some("any")
);
assert_eq!(
mapped.tool_choice.as_ref().and_then(|c| c.name.as_deref()),
None
);
}
#[test]
fn test_anthropic_parse_non_streaming_tool_use_to_tool_calls() {
let response_json = r#"
{
"id": "msg_125",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet-20241022",
"content": [
{
"type": "tool_use",
"id": "toolu_1",
"name": "get_weather",
"input": {"city":"beijing"}
}
],
"stop_reason": "tool_use",
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
}
"#;
let protocol = AnthropicProtocol::new("test-key");
let result = protocol.parse_response(response_json).unwrap();
let tool_calls = result.choices[0].message.tool_calls.as_ref().unwrap();
assert_eq!(tool_calls.len(), 1);
assert_eq!(tool_calls[0].id, "toolu_1");
assert_eq!(tool_calls[0].function.name, "get_weather");
assert_eq!(tool_calls[0].function.arguments, r#"{"city":"beijing"}"#);
assert_eq!(
result.choices[0].finish_reason.as_deref(),
Some("tool_calls")
);
}
#[test]
fn test_anthropic_parse_streaming_tool_use_deltas() {
let start_event = serde_json::json!({
"type": "content_block_start",
"index": 0,
"content_block": {
"type": "tool_use",
"id": "toolu_1",
"name": "get_weather",
"input": {}
}
});
let start_chunk = crate::protocols::common::streamers::interpret_anthropic_event(
&start_event,
"msg_stream",
)
.unwrap()
.unwrap();
let start_tool_calls = start_chunk.choices[0].delta.tool_calls.as_ref().unwrap();
assert_eq!(start_tool_calls[0].id, "toolu_1");
assert_eq!(start_tool_calls[0].function.name, "get_weather");
assert_eq!(start_tool_calls[0].index, Some(0));
let delta_event = serde_json::json!({
"type": "content_block_delta",
"index": 0,
"delta": {
"type": "input_json_delta",
"partial_json": "{\"city\":\"bei"
}
});
let delta_chunk = crate::protocols::common::streamers::interpret_anthropic_event(
&delta_event,
"msg_stream",
)
.unwrap()
.unwrap();
let delta_tool_calls = delta_chunk.choices[0].delta.tool_calls.as_ref().unwrap();
assert_eq!(delta_tool_calls[0].index, Some(0));
assert_eq!(delta_tool_calls[0].function.arguments, "{\"city\":\"bei");
}
}