use serde::Serialize;
use serde_json::{Value, json};
use super::{OpenAIContent, OpenAIContentPart, OpenAIMessage, OpenAIRequest};
#[derive(Debug, Clone, Serialize)]
pub struct AnthropicRequest {
pub model: String,
pub messages: Vec<AnthropicMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Value>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AnthropicMessage {
pub role: String,
pub content: Value,
}
impl AnthropicRequest {
pub fn from_openai(request: &OpenAIRequest) -> Self {
let mut system = Vec::new();
let mut messages: Vec<AnthropicMessage> = Vec::new();
for message in &request.messages {
match message.role.as_str() {
"system" => {
if let Some(text) = plain_text(message) {
system.push(text);
}
}
"tool" => push_tool_result(&mut messages, message),
_ => messages.push(convert_turn(message)),
}
}
Self {
model: request.model.clone(),
messages,
system: (!system.is_empty()).then(|| system.join("\n\n")),
stream: request.stream,
max_tokens: request.max_tokens,
temperature: request.temperature,
tools: request
.tools
.as_ref()
.map(|tools| tools.iter().map(to_anthropic_tool).collect()),
}
}
}
fn plain_text(message: &OpenAIMessage) -> Option<String> {
match message.content.as_ref()? {
OpenAIContent::Text(text) => Some(text.clone()),
OpenAIContent::Parts(parts) => {
let text: String = parts
.iter()
.filter_map(|part| match part {
OpenAIContentPart::Text { text } => Some(text.as_str()),
OpenAIContentPart::ImageUrl { .. } => None,
})
.collect();
(!text.is_empty()).then_some(text)
}
}
}
fn convert_turn(message: &OpenAIMessage) -> AnthropicMessage {
if let Some(text) = bare_text(message) {
return AnthropicMessage {
role: message.role.clone(),
content: Value::String(text),
};
}
let mut blocks = content_blocks(message);
if let Some(tool_calls) = &message.tool_calls {
for call in tool_calls {
blocks.push(json!({
"type": "tool_use",
"id": call.id,
"name": call.function.name,
"input": serde_json::from_str::<Value>(&call.function.arguments)
.unwrap_or_else(|_| json!({})),
}));
}
}
AnthropicMessage {
role: message.role.clone(),
content: Value::Array(blocks),
}
}
fn bare_text(message: &OpenAIMessage) -> Option<String> {
if message.tool_calls.is_some() {
return None;
}
match message.content.as_ref()? {
OpenAIContent::Text(text) => Some(text.clone()),
OpenAIContent::Parts(parts) => match parts.as_slice() {
[OpenAIContentPart::Text { text }] => Some(text.clone()),
_ => None,
},
}
}
fn content_blocks(message: &OpenAIMessage) -> Vec<Value> {
match message.content.as_ref() {
None => Vec::new(),
Some(OpenAIContent::Text(text)) => vec![json!({ "type": "text", "text": text })],
Some(OpenAIContent::Parts(parts)) => parts.iter().map(convert_part).collect(),
}
}
fn convert_part(part: &OpenAIContentPart) -> Value {
match part {
OpenAIContentPart::Text { text } => json!({ "type": "text", "text": text }),
OpenAIContentPart::ImageUrl { image_url } => match parse_data_uri(&image_url.url) {
Some((media_type, data)) => json!({
"type": "image",
"source": { "type": "base64", "media_type": media_type, "data": data },
}),
None => json!({
"type": "image",
"source": { "type": "url", "url": image_url.url },
}),
},
}
}
fn parse_data_uri(url: &str) -> Option<(String, String)> {
let rest = url.strip_prefix("data:")?;
let (meta, data) = rest.split_once(";base64,")?;
let media_type = match meta.split_once(';') {
Some((media_type, _parameters)) => media_type,
None => meta,
};
(!media_type.is_empty()).then(|| (media_type.to_string(), data.to_string()))
}
fn push_tool_result(messages: &mut Vec<AnthropicMessage>, message: &OpenAIMessage) {
let block = json!({
"type": "tool_result",
"tool_use_id": message.tool_call_id.clone().unwrap_or_default(),
"content": plain_text(message).unwrap_or_default(),
});
let Some(block) = merge_into_open_tool_turn(messages, block) else {
return;
};
messages.push(AnthropicMessage {
role: "user".to_string(),
content: json!([block]),
});
}
fn merge_into_open_tool_turn(messages: &mut [AnthropicMessage], block: Value) -> Option<Value> {
let Some(last) = messages.last_mut() else {
return Some(block);
};
let Value::Array(blocks) = &mut last.content else {
return Some(block);
};
let opens_with_tool_result = blocks
.first()
.and_then(|block| block.get("type"))
.and_then(Value::as_str)
== Some("tool_result");
if !opens_with_tool_result {
return Some(block);
}
blocks.push(block);
None
}
fn to_anthropic_tool(tool: &Value) -> Value {
let Some(function) = tool.get("function") else {
return tool.clone();
};
let mut out = serde_json::Map::new();
if let Some(name) = function.get("name") {
out.insert("name".to_string(), name.clone());
}
if let Some(description) = function.get("description") {
out.insert("description".to_string(), description.clone());
}
out.insert(
"input_schema".to_string(),
function
.get("parameters")
.cloned()
.unwrap_or_else(|| json!({ "type": "object", "properties": {} })),
);
Value::Object(out)
}
#[cfg(test)]
mod tests;