use serde::Serialize;
use serde_json::Value;
use super::{AssistantMessage, ChatResponse, Choice, LlmProvider, ToolCall, ToolCallFn, ToolDef};
pub struct AnthropicProvider {
pub api_key: String,
pub model: String,
}
#[derive(Serialize)]
struct AnthropicRequest<'a> {
model: &'a str,
max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
system: Option<String>,
messages: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
tools: Option<Vec<AnthropicTool<'a>>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_choice: Option<Value>,
}
#[derive(Serialize)]
struct AnthropicTool<'a> {
name: &'a str,
description: &'a str,
input_schema: &'a Value,
}
fn convert_messages(messages: Vec<Value>) -> (Option<String>, Vec<Value>) {
let mut system: Option<String> = None;
let mut out: Vec<Value> = Vec::new();
for msg in messages {
let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("");
match role {
"system" => {
let text = msg
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
system = Some(text);
}
"user" => {
let content = &msg["content"];
let blocks = if let Some(text) = content.as_str() {
serde_json::json!([{"type": "text", "text": text}])
} else if let Some(arr) = content.as_array() {
let mapped: Vec<Value> = arr
.iter()
.map(|part| {
let ptype = part.get("type").and_then(|v| v.as_str()).unwrap_or("text");
if ptype == "image_url" {
let url = part
.get("image_url")
.and_then(|v| v.get("url"))
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(rest) = url.strip_prefix("data:")
&& let Some((meta, data)) = rest.split_once(',')
{
let media_type = meta.strip_suffix(";base64").unwrap_or(meta);
return serde_json::json!({
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": data,
}
});
}
serde_json::json!({
"type": "image",
"source": {
"type": "url",
"url": url,
}
})
} else {
serde_json::json!({
"type": "text",
"text": part.get("text").and_then(|v| v.as_str()).unwrap_or(""),
})
}
})
.collect();
Value::Array(mapped)
} else {
serde_json::json!([{"type": "text", "text": ""}])
};
out.push(serde_json::json!({"role": "user", "content": blocks}));
}
"tool" => {
let tool_use_id = msg
.get("tool_call_id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let content = msg
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
out.push(serde_json::json!({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": content,
}]
}));
}
"assistant" => {
if let Some(tool_calls) = msg.get("tool_calls").and_then(|v| v.as_array()) {
let blocks: Vec<Value> = tool_calls
.iter()
.map(|tc| {
let id = tc
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = tc
.get("function")
.and_then(|f| f.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let args_str = tc
.get("function")
.and_then(|f| f.get("arguments"))
.and_then(|v| v.as_str())
.unwrap_or("{}");
let input: Value =
serde_json::from_str(args_str).unwrap_or(serde_json::json!({}));
serde_json::json!({
"type": "tool_use",
"id": id,
"name": name,
"input": input,
})
})
.collect();
out.push(serde_json::json!({"role": "assistant", "content": blocks}));
} else {
let text = msg
.get("content")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
out.push(
serde_json::json!({"role": "assistant", "content": [{"type": "text", "text": text}]}),
);
}
}
_ => {
}
}
}
(system, out)
}
fn parse_response(body: Value) -> anyhow::Result<ChatResponse> {
let stop_reason = body
.get("stop_reason")
.and_then(|v| v.as_str())
.unwrap_or("end_turn");
let finish_reason = match stop_reason {
"tool_use" => "tool_calls".to_string(),
"end_turn" => "stop".to_string(),
other => other.to_string(),
};
let content_blocks = body
.get("content")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut text_parts: Vec<String> = Vec::new();
let mut tool_calls: Vec<ToolCall> = Vec::new();
for block in &content_blocks {
let btype = block.get("type").and_then(|v| v.as_str()).unwrap_or("");
match btype {
"text" => {
if let Some(t) = block.get("text").and_then(|v| v.as_str()) {
text_parts.push(t.to_string());
}
}
"tool_use" => {
let id = block
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = block
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let input = block.get("input").cloned().unwrap_or(serde_json::json!({}));
let arguments = serde_json::to_string(&input).unwrap_or_default();
tool_calls.push(ToolCall {
id,
kind: "function".to_string(),
function: ToolCallFn { name, arguments },
});
}
_ => {}
}
}
let content = if text_parts.is_empty() {
None
} else {
Some(text_parts.join(""))
};
Ok(ChatResponse {
choices: vec![Choice {
message: AssistantMessage {
role: "assistant".to_string(),
content,
tool_calls,
},
finish_reason,
}],
})
}
impl LlmProvider for AnthropicProvider {
async fn chat_with_tools(
&self,
messages: Vec<Value>,
tools: &[ToolDef],
tool_choice: Option<&str>,
) -> anyhow::Result<ChatResponse> {
let (system, converted) = convert_messages(messages);
let anthropic_tools: Vec<AnthropicTool<'_>> = tools
.iter()
.map(|t| AnthropicTool {
name: t.function.name,
description: t.function.description,
input_schema: &t.function.parameters,
})
.collect();
let tc_value = tool_choice.map(|tc| match tc {
"auto" => serde_json::json!({"type": "auto"}),
"none" => serde_json::json!({"type": "none"}),
"required" | "any" => serde_json::json!({"type": "any"}),
specific => serde_json::json!({"type": "tool", "name": specific}),
});
let request = AnthropicRequest {
model: &self.model,
max_tokens: 8192,
system,
messages: converted,
tools: if anthropic_tools.is_empty() {
None
} else {
Some(anthropic_tools)
},
tool_choice: tc_value,
};
let client = reqwest::Client::new();
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.header("Content-Type", "application/json")
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Anthropic API error {status}: {text}");
}
let body: Value = response.json().await?;
parse_response(body)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_messages_extracts_system() {
let msgs = vec![
serde_json::json!({"role": "system", "content": "You are helpful."}),
serde_json::json!({"role": "user", "content": "Hello"}),
];
let (system, converted) = convert_messages(msgs);
assert_eq!(system.as_deref(), Some("You are helpful."));
assert_eq!(converted.len(), 1);
assert_eq!(converted[0]["role"], "user");
assert_eq!(converted[0]["content"][0]["type"], "text");
assert_eq!(converted[0]["content"][0]["text"], "Hello");
}
#[test]
fn convert_messages_maps_image_url_to_base64_source() {
let msgs = vec![serde_json::json!({
"role": "user",
"content": [
{"type": "text", "text": "Describe this image"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR"}}
]
})];
let (_system, converted) = convert_messages(msgs);
assert_eq!(converted.len(), 1);
let content = converted[0]["content"].as_array().unwrap();
assert_eq!(content.len(), 2);
assert_eq!(content[0]["type"], "text");
assert_eq!(content[1]["type"], "image");
assert_eq!(content[1]["source"]["type"], "base64");
assert_eq!(content[1]["source"]["media_type"], "image/png");
assert_eq!(content[1]["source"]["data"], "iVBOR");
}
#[test]
fn convert_messages_maps_tool_result() {
let msgs = vec![serde_json::json!({
"role": "tool",
"tool_call_id": "call_abc",
"content": "{\"valid\": true}"
})];
let (_system, converted) = convert_messages(msgs);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0]["role"], "user");
let block = &converted[0]["content"][0];
assert_eq!(block["type"], "tool_result");
assert_eq!(block["tool_use_id"], "call_abc");
assert_eq!(block["content"], "{\"valid\": true}");
}
#[test]
fn convert_messages_maps_assistant_tool_calls() {
let msgs = vec![serde_json::json!({
"role": "assistant",
"tool_calls": [{
"id": "call_1",
"type": "function",
"function": {
"name": "validate_card",
"arguments": "{\"card\":{}}"
}
}]
})];
let (_system, converted) = convert_messages(msgs);
assert_eq!(converted.len(), 1);
assert_eq!(converted[0]["role"], "assistant");
let block = &converted[0]["content"][0];
assert_eq!(block["type"], "tool_use");
assert_eq!(block["id"], "call_1");
assert_eq!(block["name"], "validate_card");
assert_eq!(block["input"], serde_json::json!({"card": {}}));
}
#[test]
fn parse_response_text_only() {
let body = serde_json::json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"stop_reason": "end_turn",
"content": [
{"type": "text", "text": "Hello, world!"}
]
});
let resp = parse_response(body).unwrap();
assert_eq!(resp.choices.len(), 1);
assert_eq!(resp.choices[0].finish_reason, "stop");
assert_eq!(
resp.choices[0].message.content.as_deref(),
Some("Hello, world!")
);
assert!(resp.choices[0].message.tool_calls.is_empty());
}
#[test]
fn parse_response_tool_use() {
let body = serde_json::json!({
"id": "msg_456",
"type": "message",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{"type": "text", "text": "Let me validate that."},
{
"type": "tool_use",
"id": "toolu_01",
"name": "validate_card",
"input": {"card": {"type": "AdaptiveCard"}}
}
]
});
let resp = parse_response(body).unwrap();
assert_eq!(resp.choices[0].finish_reason, "tool_calls");
assert_eq!(
resp.choices[0].message.content.as_deref(),
Some("Let me validate that.")
);
assert_eq!(resp.choices[0].message.tool_calls.len(), 1);
let tc = &resp.choices[0].message.tool_calls[0];
assert_eq!(tc.id, "toolu_01");
assert_eq!(tc.kind, "function");
assert_eq!(tc.function.name, "validate_card");
let parsed_args: Value = serde_json::from_str(&tc.function.arguments).unwrap();
assert_eq!(
parsed_args,
serde_json::json!({"card": {"type": "AdaptiveCard"}})
);
}
}