use crate::anthropic::types::{ContentBlock, CreateMessageRequest, MessageResponse, Usage};
use crate::openai::types::{
ChatCompletionMessageToolCall, ChatCompletionRequestMessage, ChatCompletionTool,
ChatCompletionToolChoiceOption, CreateChatCompletionRequest, FunctionCall, FunctionNameOnly,
FunctionObject,
};
use crate::provider::ProviderChatResponse;
use serde_json::{Value, json};
pub fn anthropic_to_openai(req: &CreateMessageRequest) -> CreateChatCompletionRequest {
let mut messages: Vec<ChatCompletionRequestMessage> = Vec::new();
if let Some(text) = flatten_system(req.system.as_ref())
&& !text.is_empty()
{
messages.push(ChatCompletionRequestMessage {
role: "system".to_string(),
content: Some(Value::String(text)),
tool_call_id: None,
tool_calls: None,
});
}
for msg in &req.messages {
convert_message_into(msg, &mut messages);
}
let tools = req.tools.as_ref().map(|tools| {
tools
.iter()
.filter_map(convert_tool_definition)
.collect::<Vec<_>>()
});
let tool_choice = req.tool_choice.as_ref().and_then(convert_tool_choice);
CreateChatCompletionRequest {
model: req.model.clone(),
messages,
temperature: req.temperature,
stream: req.stream,
tools,
tool_choice,
}
}
pub fn openai_to_anthropic(resp: ProviderChatResponse, model: String) -> MessageResponse {
let mut content: Vec<ContentBlock> = Vec::new();
if let Some(text) = resp.content
&& !text.is_empty()
{
content.push(ContentBlock::Text { text });
}
for tc in resp.tool_calls {
let input = serde_json::from_str::<Value>(&tc.function.arguments)
.unwrap_or_else(|_| Value::Object(Default::default()));
content.push(ContentBlock::ToolUse {
id: tc.id,
name: tc.function.name,
input,
});
}
let id = format!("msg_{}", uuid::Uuid::new_v4().simple());
MessageResponse {
id,
kind: "message",
role: "assistant",
content,
model,
stop_reason: None,
stop_sequence: None,
usage: Usage {
input_tokens: resp.prompt_tokens,
output_tokens: resp.completion_tokens,
},
}
}
pub fn map_finish_reason(finish_reason: Option<&str>) -> Option<String> {
finish_reason.map(|reason| match reason {
"stop" => "end_turn".to_string(),
"length" => "max_tokens".to_string(),
"tool_calls" => "tool_use".to_string(),
"content_filter" => "end_turn".to_string(),
other => other.to_string(),
})
}
pub(crate) fn flatten_system(system: Option<&Value>) -> Option<String> {
let value = system?;
match value {
Value::String(s) => Some(s.clone()),
Value::Array(blocks) => {
let parts: Vec<String> = blocks
.iter()
.filter_map(|block| {
let obj = block.as_object()?;
if obj.get("type").and_then(Value::as_str) == Some("text") {
obj.get("text").and_then(Value::as_str).map(str::to_owned)
} else {
None
}
})
.collect();
Some(parts.join("\n"))
}
Value::Null => None,
other => Some(other.to_string()),
}
}
fn convert_message_into(msg: &Value, out: &mut Vec<ChatCompletionRequestMessage>) {
let Some(obj) = msg.as_object() else {
return;
};
let role = obj.get("role").and_then(Value::as_str).unwrap_or("user");
let Some(content) = obj.get("content") else {
return;
};
match content {
Value::String(s) => {
out.push(ChatCompletionRequestMessage {
role: role.to_string(),
content: Some(Value::String(s.clone())),
tool_call_id: None,
tool_calls: None,
});
}
Value::Array(blocks) => match role {
"assistant" => convert_assistant_blocks(blocks, out),
"user" => convert_user_blocks(blocks, out),
_ => {
out.push(ChatCompletionRequestMessage {
role: role.to_string(),
content: Some(Value::String(stringify(content))),
tool_call_id: None,
tool_calls: None,
});
}
},
Value::Null => {
out.push(ChatCompletionRequestMessage {
role: role.to_string(),
content: Some(Value::String(String::new())),
tool_call_id: None,
tool_calls: None,
});
}
other => {
out.push(ChatCompletionRequestMessage {
role: role.to_string(),
content: Some(Value::String(stringify(other))),
tool_call_id: None,
tool_calls: None,
});
}
}
}
fn convert_assistant_blocks(blocks: &[Value], out: &mut Vec<ChatCompletionRequestMessage>) {
let mut text_parts: Vec<String> = Vec::new();
let mut tool_calls: Vec<ChatCompletionMessageToolCall> = Vec::new();
for block in blocks {
let Some(obj) = block.as_object() else {
continue;
};
let Some(kind) = obj.get("type").and_then(Value::as_str) else {
continue;
};
match kind {
"text" => {
if let Some(text) = obj.get("text").and_then(Value::as_str) {
text_parts.push(text.to_string());
}
}
"tool_use" => {
let id = obj
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let name = obj
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let input = obj
.get("input")
.cloned()
.unwrap_or(Value::Object(Default::default()));
let arguments = serde_json::to_string(&input).unwrap_or_else(|_| "{}".to_string());
tool_calls.push(ChatCompletionMessageToolCall {
id,
kind: "function".to_string(),
function: FunctionCall { name, arguments },
});
}
"thinking" => {}
_ => {}
}
}
let content_text = text_parts.join("\n");
out.push(ChatCompletionRequestMessage {
role: "assistant".to_string(),
content: if content_text.is_empty() && !tool_calls.is_empty() {
None
} else {
Some(Value::String(content_text))
},
tool_call_id: None,
tool_calls: if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
},
});
}
fn convert_user_blocks(blocks: &[Value], out: &mut Vec<ChatCompletionRequestMessage>) {
let mut regular_parts: Vec<Value> = Vec::new();
let flush_regular = |regular: &mut Vec<Value>, sink: &mut Vec<ChatCompletionRequestMessage>| {
if regular.is_empty() {
return;
}
let content = if regular.len() == 1
&& regular[0]
.as_object()
.and_then(|o| o.get("type"))
.and_then(Value::as_str)
== Some("text")
{
let text = regular[0]
.get("text")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
Value::String(text)
} else {
Value::Array(std::mem::take(regular))
};
sink.push(ChatCompletionRequestMessage {
role: "user".to_string(),
content: Some(content),
tool_call_id: None,
tool_calls: None,
});
regular.clear();
};
for block in blocks {
let Some(obj) = block.as_object() else {
continue;
};
let Some(kind) = obj.get("type").and_then(Value::as_str) else {
continue;
};
match kind {
"tool_result" => {
flush_regular(&mut regular_parts, out);
let tool_use_id = obj
.get("tool_use_id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let raw = obj.get("content").cloned().unwrap_or(Value::Null);
let content_str = match &raw {
Value::String(s) => s.clone(),
Value::Array(items) => items
.iter()
.filter_map(|item| {
let o = item.as_object()?;
if o.get("type").and_then(Value::as_str) == Some("text") {
o.get("text").and_then(Value::as_str).map(str::to_owned)
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n"),
Value::Null => String::new(),
other => stringify(other),
};
out.push(ChatCompletionRequestMessage {
role: "tool".to_string(),
content: Some(Value::String(content_str)),
tool_call_id: Some(tool_use_id),
tool_calls: None,
});
}
"text" => {
let text = obj
.get("text")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
regular_parts.push(json!({"type": "text", "text": text}));
}
"image" => {
if let Some(source) = obj.get("source").and_then(Value::as_object) {
let kind_src = source.get("type").and_then(Value::as_str).unwrap_or("");
if kind_src == "base64" {
let media_type = source
.get("media_type")
.and_then(Value::as_str)
.unwrap_or("image/png");
let data = source.get("data").and_then(Value::as_str).unwrap_or("");
let url = format!("data:{media_type};base64,{data}");
regular_parts.push(json!({
"type": "image_url",
"image_url": {"url": url}
}));
} else if kind_src == "url"
&& let Some(url) = source.get("url").and_then(Value::as_str)
{
regular_parts.push(json!({
"type": "image_url",
"image_url": {"url": url}
}));
}
}
}
"thinking" => {}
_ => {}
}
}
flush_regular(&mut regular_parts, out);
}
fn convert_tool_definition(tool: &Value) -> Option<ChatCompletionTool> {
let obj = tool.as_object()?;
let name = obj.get("name").and_then(Value::as_str)?.to_string();
let description = obj
.get("description")
.and_then(Value::as_str)
.map(str::to_owned);
let parameters = obj
.get("input_schema")
.cloned()
.or_else(|| Some(json!({"type": "object", "properties": {}})));
Some(ChatCompletionTool {
kind: "function".to_string(),
function: FunctionObject {
name,
description,
parameters,
},
})
}
fn convert_tool_choice(choice: &Value) -> Option<ChatCompletionToolChoiceOption> {
let obj = choice.as_object()?;
let kind = obj.get("type").and_then(Value::as_str)?;
match kind {
"auto" => Some(ChatCompletionToolChoiceOption::String("auto".to_string())),
"any" => Some(ChatCompletionToolChoiceOption::String(
"required".to_string(),
)),
"none" => Some(ChatCompletionToolChoiceOption::String("none".to_string())),
"tool" => {
let name = obj.get("name").and_then(Value::as_str)?.to_string();
Some(ChatCompletionToolChoiceOption::Named {
function: FunctionNameOnly { name },
})
}
_ => None,
}
}
pub(crate) fn stringify(value: &Value) -> String {
serde_json::to_string(value).unwrap_or_default()
}