use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::engine::{stable_id, DEFAULT_MODEL};
use crate::protocol::{
create_chat_completion_with_solver, ChatCompletionRequest, ChatMessage, MessageContent,
ToolCall,
};
use crate::solver::UniversalSolver;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnthropicMessagesRequest {
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub messages: Vec<AnthropicMessageInput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default)]
pub stream: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnthropicMessageInput {
pub role: String,
pub content: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnthropicMessage {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub role: String,
pub model: String,
pub content: Vec<AnthropicContentBlock>,
pub stop_reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
pub usage: AnthropicUsage,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AnthropicContentBlock {
Text { text: String },
ToolUse {
id: String,
name: String,
#[serde(default)]
input: Value,
},
}
impl AnthropicContentBlock {
fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnthropicUsage {
pub input_tokens: u32,
pub output_tokens: u32,
}
impl AnthropicMessagesRequest {
#[must_use]
pub fn to_chat_completion_request(&self) -> ChatCompletionRequest {
let mut messages = Vec::new();
if let Some(system) = self.system.as_ref() {
let text = anthropic_content_to_text(system);
if !text.trim().is_empty() {
messages.push(ChatMessage::new("system", text));
}
}
let mut tool_names_by_id: HashMap<String, String> = HashMap::new();
for message in &self.messages {
append_anthropic_message(message, &mut messages, &mut tool_names_by_id);
}
ChatCompletionRequest {
model: self.model.clone(),
messages,
temperature: self.temperature,
stream: false,
tools: self.tools.iter().map(anthropic_tool_to_openai).collect(),
tool_choice: self
.tool_choice
.as_ref()
.map(anthropic_tool_choice_to_openai),
functions: Vec::new(),
function_call: None,
}
}
}
fn append_anthropic_message(
input: &AnthropicMessageInput,
out: &mut Vec<ChatMessage>,
tool_names_by_id: &mut HashMap<String, String>,
) {
match &input.content {
Value::String(text) => {
if !text.trim().is_empty() {
out.push(ChatMessage::new(input.role.clone(), text.clone()));
}
}
Value::Array(blocks) => {
append_anthropic_blocks(&input.role, blocks, out, tool_names_by_id);
}
other => append_anthropic_blocks(
&input.role,
std::slice::from_ref(other),
out,
tool_names_by_id,
),
}
}
fn append_anthropic_blocks(
role: &str,
blocks: &[Value],
out: &mut Vec<ChatMessage>,
tool_names_by_id: &mut HashMap<String, String>,
) {
let is_assistant = role.eq_ignore_ascii_case("assistant");
let mut text = String::new();
let mut tool_calls = Vec::new();
let mut tool_results: Vec<(String, String)> = Vec::new();
for block in blocks {
match block.get("type").and_then(Value::as_str) {
Some("text") => {
if let Some(chunk) = block.get("text").and_then(Value::as_str) {
if !text.is_empty() {
text.push('\n');
}
text.push_str(chunk);
}
}
Some("tool_use") => {
let id = block
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let name = block
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let arguments = block
.get("input")
.map_or_else(|| String::from("{}"), std::string::ToString::to_string);
if !name.is_empty() {
tool_names_by_id.insert(id.clone(), name.clone());
}
tool_calls.push(ToolCall::function(id, name, arguments));
}
Some("tool_result") => {
let id = block
.get("tool_use_id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let content = block
.get("content")
.map_or_else(String::new, anthropic_content_to_text);
tool_results.push((id, content));
}
_ => {}
}
}
if is_assistant {
if tool_calls.is_empty() {
if !text.trim().is_empty() {
out.push(ChatMessage::assistant(text));
}
} else {
let mut message = ChatMessage::assistant_tool_calls(tool_calls);
if !text.trim().is_empty() {
message.content = MessageContent::Text(text);
}
out.push(message);
}
} else {
if !text.trim().is_empty() {
out.push(ChatMessage::new(role.to_owned(), text));
}
for (id, content) in tool_results {
let name = tool_names_by_id.get(&id).cloned();
out.push(ChatMessage {
role: String::from("tool"),
content: MessageContent::Text(content),
tool_call_id: Some(id),
name,
..ChatMessage::default()
});
}
}
}
fn anthropic_tool_to_openai(tool: &Value) -> Value {
let mut function = serde_json::Map::new();
if let Some(name) = tool.get("name") {
function.insert(String::from("name"), name.clone());
}
if let Some(description) = tool.get("description") {
function.insert(String::from("description"), description.clone());
}
if let Some(schema) = tool.get("input_schema") {
function.insert(String::from("parameters"), schema.clone());
}
json!({ "type": "function", "function": Value::Object(function) })
}
fn anthropic_tool_choice_to_openai(choice: &Value) -> Value {
match choice.get("type").and_then(Value::as_str) {
Some("none") => Value::String(String::from("none")),
Some("any") => Value::String(String::from("required")),
Some("tool") => {
let name = choice.get("name").cloned().unwrap_or(Value::Null);
json!({ "type": "function", "function": { "name": name } })
}
_ => Value::String(String::from("auto")),
}
}
#[must_use]
pub fn create_anthropic_message_with_solver(
request: &AnthropicMessagesRequest,
solver: &UniversalSolver,
) -> AnthropicMessage {
let chat_request = request.to_chat_completion_request();
let completion = create_chat_completion_with_solver(&chat_request, solver);
let model = request
.model
.clone()
.unwrap_or_else(|| String::from(DEFAULT_MODEL));
let choice = completion.choices.first();
let requests_tools = choice.is_some_and(|choice| choice.finish_reason == "tool_calls");
let (content, stop_reason, seed) = if requests_tools {
let calls = choice
.map(|choice| choice.message.tool_calls.clone())
.unwrap_or_default();
let seed = calls
.iter()
.map(|call| format!("{}({})", call.function.name, call.function.arguments))
.collect::<Vec<_>>()
.join("|");
let blocks = calls.into_iter().map(tool_call_to_block).collect();
(blocks, String::from("tool_use"), seed)
} else {
let text = choice
.map(|choice| choice.message.content.plain_text())
.unwrap_or_default();
(
vec![AnthropicContentBlock::text(text.clone())],
String::from("end_turn"),
text,
)
};
AnthropicMessage {
id: stable_id("msg", &seed),
kind: String::from("message"),
role: String::from("assistant"),
model,
content,
stop_reason,
stop_sequence: None,
usage: AnthropicUsage {
input_tokens: completion.usage.prompt_tokens,
output_tokens: completion.usage.completion_tokens,
},
}
}
fn tool_call_to_block(call: ToolCall) -> AnthropicContentBlock {
let input = serde_json::from_str::<Value>(&call.function.arguments)
.unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
AnthropicContentBlock::ToolUse {
id: call.id,
name: call.function.name,
input,
}
}
#[must_use]
pub fn anthropic_message_sse(message: &AnthropicMessage) -> String {
let message_start = json!({
"type": "message_start",
"message": {
"id": message.id,
"type": "message",
"role": "assistant",
"model": message.model,
"content": [],
"stop_reason": Value::Null,
"stop_sequence": Value::Null,
"usage": {
"input_tokens": message.usage.input_tokens,
"output_tokens": 0,
}
}
});
let message_delta = json!({
"type": "message_delta",
"delta": {"stop_reason": message.stop_reason, "stop_sequence": Value::Null},
"usage": {"output_tokens": message.usage.output_tokens}
});
let message_stop = json!({"type": "message_stop"});
let mut body = String::new();
push_sse_event(&mut body, "message_start", &message_start);
for (index, block) in message.content.iter().enumerate() {
push_content_block_events(&mut body, index, block);
}
push_sse_event(&mut body, "message_delta", &message_delta);
push_sse_event(&mut body, "message_stop", &message_stop);
body
}
fn push_content_block_events(body: &mut String, index: usize, block: &AnthropicContentBlock) {
match block {
AnthropicContentBlock::Text { text } => {
push_sse_event(
body,
"content_block_start",
&json!({
"type": "content_block_start",
"index": index,
"content_block": {"type": "text", "text": ""}
}),
);
push_sse_event(
body,
"content_block_delta",
&json!({
"type": "content_block_delta",
"index": index,
"delta": {"type": "text_delta", "text": text}
}),
);
}
AnthropicContentBlock::ToolUse { id, name, input } => {
push_sse_event(
body,
"content_block_start",
&json!({
"type": "content_block_start",
"index": index,
"content_block": {"type": "tool_use", "id": id, "name": name, "input": {}}
}),
);
push_sse_event(
body,
"content_block_delta",
&json!({
"type": "content_block_delta",
"index": index,
"delta": {"type": "input_json_delta", "partial_json": input.to_string()}
}),
);
}
}
push_sse_event(
body,
"content_block_stop",
&json!({"type": "content_block_stop", "index": index}),
);
}
fn push_sse_event(body: &mut String, event: &str, data: &Value) {
body.push_str("event: ");
body.push_str(event);
body.push('\n');
body.push_str("data: ");
body.push_str(&data.to_string());
body.push_str("\n\n");
}
fn anthropic_content_to_text(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
Value::Array(blocks) => blocks
.iter()
.map(anthropic_content_to_text)
.filter(|text| !text.trim().is_empty())
.collect::<Vec<_>>()
.join("\n"),
Value::Object(object) => object
.get("text")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.unwrap_or_default(),
_ => String::new(),
}
}