#![allow(clippy::too_many_lines)]
use std::collections::HashMap;
use bytes::Bytes;
use serde::Deserialize;
use serde_json::json;
use super::response_transforms::{openai_tool_choice_to_anthropic, openai_tool_to_anthropic_tool};
use crate::model::{TransformError, TransformRequest, TransformResponse, validate_json_depth};
#[derive(Debug, Deserialize)]
pub(crate) struct OpenAiRequestBody {
pub(crate) model: String,
pub(crate) messages: Vec<OpenAiRequestMessage>,
#[serde(default)]
pub(crate) max_tokens: Option<u64>,
#[serde(default)]
pub(crate) temperature: Option<f64>,
#[serde(default)]
pub(crate) stop: Option<Vec<String>>,
#[serde(default)]
pub(crate) stream: Option<bool>,
#[serde(default)]
pub(crate) tools: Option<Vec<OpenAiRequestTool>>,
#[serde(default)]
pub(crate) tool_choice: Option<serde_json::Value>,
#[serde(default)]
pub(crate) enable_thinking: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct OpenAiRequestMessage {
pub(crate) role: String,
#[serde(default)]
pub(crate) content: Option<String>,
#[serde(default, rename = "tool_call_id")]
pub(crate) tool_call_id: Option<String>,
#[serde(default, rename = "tool_calls")]
pub(crate) tool_calls: Option<Vec<OpenAiToolCallDef>>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct OpenAiToolCallDef {
pub(crate) id: String,
#[serde(default)]
pub(crate) r#type: String,
pub(crate) function: OpenAiToolCallFunction,
}
#[derive(Debug, Deserialize)]
pub(crate) struct OpenAiToolCallFunction {
pub(crate) name: String,
#[serde(default)]
pub(crate) arguments: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct OpenAiRequestTool {
#[serde(default, rename = "type")]
pub(crate) tool_type: String,
#[serde(default)]
pub(crate) function: Option<OpenAiRequestToolFunction>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct OpenAiRequestToolFunction {
pub(crate) name: String,
#[serde(default)]
pub(crate) description: Option<String>,
#[serde(default)]
pub(crate) parameters: Option<serde_json::Value>,
}
pub(crate) fn parse_openai_body(bytes: &Bytes) -> Result<OpenAiRequestBody, TransformError> {
let value: serde_json::Value = serde_json::from_slice(bytes)
.map_err(|_| TransformError::InvalidFormat("invalid JSON body".into()))?;
validate_json_depth(&value)?;
serde_json::from_value(value)
.map_err(|_| TransformError::InvalidFormat("invalid request structure".into()))
}
#[allow(clippy::match_same_arms)]
pub fn openai_to_anthropic(req: &TransformRequest) -> Result<TransformResponse, TransformError> {
let body: OpenAiRequestBody = parse_openai_body(&req.body)?;
if body.messages.len() > crate::model::MAX_MESSAGES_COUNT {
return Err(TransformError::BufferLimitExceeded(format!(
"messages array length {} exceeds maximum of {}",
body.messages.len(),
crate::model::MAX_MESSAGES_COUNT
)));
}
let mut headers = HashMap::new();
if let Some(auth) = req.headers.get("authorization")
&& let Some(token) = auth.strip_prefix("Bearer ")
{
headers.insert("x-api-key".to_string(), token.to_string());
}
headers.insert("content-type".to_string(), "application/json".to_string());
let path = "/v1/messages".to_string();
let mut messages: Vec<serde_json::Value> = Vec::new();
for msg in &body.messages {
match msg.role.as_str() {
"system" => {
}
"user" => {
messages.push(json!({
"role": "user",
"content": msg.content.as_ref().map_or(serde_json::Value::String(String::new()), |c| {
serde_json::Value::Array(vec![json!({ "type": "text", "text": c })])
}),
}));
}
"assistant" => {
let has_tool_calls = msg
.tool_calls
.as_ref()
.is_some_and(|tc: &Vec<OpenAiToolCallDef>| !tc.is_empty());
let content_str = msg.content.as_deref().unwrap_or("");
if has_tool_calls {
let Some(tool_calls) = msg.tool_calls.as_ref() else {
unreachable!("has_tool_calls is true, so tool_calls must be Some")
};
let mut content_blocks: Vec<serde_json::Value> = Vec::new();
if !content_str.is_empty() {
content_blocks.push(json!({ "type": "text", "text": content_str }));
}
for tc in tool_calls {
let clean_id = tc.id.strip_prefix("toolu_").unwrap_or(&tc.id);
let id = format!("toolu_{clean_id}");
let args: serde_json::Value = serde_json::from_str(&tc.function.arguments)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
content_blocks.push(json!({
"type": "tool_use",
"id": id,
"name": tc.function.name,
"input": args,
}));
}
messages.push(json!({
"role": "assistant",
"content": content_blocks,
}));
} else {
messages.push(json!({
"role": "assistant",
"content": [{ "type": "text", "text": content_str }],
}));
}
}
"tool" => {
let tool_call_id = msg.tool_call_id.clone().unwrap_or_default();
let clean_id = tool_call_id.strip_prefix("toolu_").unwrap_or(&tool_call_id);
let content_text = msg.content.as_deref().unwrap_or("");
messages.push(json!({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": format!("toolu_{clean_id}"),
"content": [{ "type": "text", "text": content_text }],
"is_error": false,
}],
}));
}
_ => {
tracing::debug!(
"lossy downgrade: mapping unknown role '{}' to 'user'",
msg.role
);
}
}
}
let mut body_obj = serde_json::Map::new();
body_obj.insert("model".to_string(), serde_json::Value::String(body.model));
body_obj.insert("messages".to_string(), serde_json::Value::Array(messages));
let systems: Vec<&str> = body
.messages
.iter()
.filter(|m| m.role == "system")
.filter_map(|m| m.content.as_deref())
.collect();
if !systems.is_empty() {
body_obj.insert(
"system".to_string(),
serde_json::Value::String(systems.join("\n")),
);
}
if let Some(max_tokens) = body.max_tokens {
body_obj.insert(
"max_tokens".to_string(),
serde_json::Value::Number(
serde_json::Number::from_i128(i128::from(max_tokens))
.unwrap_or(serde_json::Number::from(0)),
),
);
}
if let Some(temperature) = body.temperature {
body_obj.insert(
"temperature".to_string(),
serde_json::Value::Number(
serde_json::Number::from_f64(temperature)
.map_or(serde_json::Number::from(0), |n| n),
),
);
}
if let Some(stop) = &body.stop {
body_obj.insert("stop_sequences".to_string(), json!(stop));
}
if let Some(stream) = body.stream {
body_obj.insert("stream".to_string(), serde_json::Value::Bool(stream));
}
if let Some(enable_thinking) = body.enable_thinking {
body_obj.insert(
"thinking".to_string(),
json!({
"type": if enable_thinking { "enabled" } else { "disabled" },
}),
);
}
if let Some(ref tools) = body.tools {
let anthropic_tools = tools
.iter()
.map(openai_tool_to_anthropic_tool)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|tool| !tool.is_null())
.collect::<Vec<_>>();
if !anthropic_tools.is_empty() {
body_obj.insert(
"tools".to_string(),
serde_json::Value::Array(anthropic_tools),
);
}
}
if let Some(ref tool_choice) = body.tool_choice {
let anthropic_tool_choice = openai_tool_choice_to_anthropic(tool_choice)?;
body_obj.insert("tool_choice".to_string(), anthropic_tool_choice);
}
let body_bytes = serde_json::to_vec(&serde_json::Value::Object(body_obj))
.map_err(|e| TransformError::InvalidFormat(format!("response serialization: {e}")))?;
Ok(TransformResponse {
headers,
path,
body: Bytes::from(body_bytes),
})
}