#![allow(clippy::too_many_lines)]
use bytes::Bytes;
use serde::Deserialize;
use serde_json::json;
use super::openai_to_anthropic;
use crate::model::{
MAX_MESSAGES_COUNT, TransformError, TransformRequest, TransformResponse, validate_json_depth,
};
#[derive(Debug, Deserialize)]
pub(crate) struct OpenAiResponsesRequestBody {
pub(crate) model: String,
#[serde(default)]
pub(crate) input: Option<serde_json::Value>,
#[serde(default)]
pub(crate) instructions: Option<String>,
#[serde(default)]
pub(crate) tools: Option<Vec<OpenAiResponsesTool>>,
#[serde(default)]
pub(crate) tool_choice: Option<serde_json::Value>,
#[serde(default, rename = "max_output_tokens")]
pub(crate) max_output_tokens: Option<u64>,
#[serde(default)]
pub(crate) temperature: Option<f64>,
#[serde(default)]
pub(crate) stream: Option<bool>,
#[serde(default, rename = "previous_response_id")]
pub(crate) previous_response_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct OpenAiResponsesTool {
#[serde(default, rename = "type")]
pub(crate) tool_type: String,
#[serde(default)]
pub(crate) name: Option<String>,
#[serde(default)]
pub(crate) description: Option<String>,
#[serde(default)]
pub(crate) parameters: Option<serde_json::Value>,
}
pub(crate) fn parse_openai_responses_request_body(
bytes: &Bytes,
) -> Result<OpenAiResponsesRequestBody, 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 response structure".into()))
}
pub fn responses_to_anthropic(req: &TransformRequest) -> Result<TransformResponse, TransformError> {
let body: OpenAiResponsesRequestBody = parse_openai_responses_request_body(&req.body)?;
if body.previous_response_id.is_some() {
tracing::debug!(
"lossy downgrade: ignoring Responses API previous_response_id in stateless transform"
);
}
let mut messages = Vec::new();
if let Some(instructions) = body
.instructions
.as_deref()
.filter(|value| !value.is_empty())
{
messages.push(json!({
"role": "system",
"content": instructions,
}));
}
let input_messages = responses_input_to_chat_messages(body.input.as_ref())?;
if messages.len() + input_messages.len() > MAX_MESSAGES_COUNT {
return Err(TransformError::BufferLimitExceeded(format!(
"messages array length {} exceeds maximum of {}",
messages.len() + input_messages.len(),
MAX_MESSAGES_COUNT
)));
}
messages.extend(input_messages);
let mut synthetic_body = serde_json::Map::new();
synthetic_body.insert("model".to_string(), serde_json::Value::String(body.model));
synthetic_body.insert("messages".to_string(), serde_json::Value::Array(messages));
if let Some(max_output_tokens) = body.max_output_tokens {
synthetic_body.insert(
"max_tokens".to_string(),
serde_json::Value::Number(max_output_tokens.into()),
);
}
if let Some(temperature) = body.temperature {
synthetic_body.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(stream) = body.stream {
synthetic_body.insert("stream".to_string(), serde_json::Value::Bool(stream));
}
if let Some(ref tools) = body.tools {
synthetic_body.insert(
"tools".to_string(),
serde_json::Value::Array(responses_tools_to_chat_tools(tools)?),
);
}
if let Some(ref tool_choice) = body.tool_choice {
synthetic_body.insert(
"tool_choice".to_string(),
normalize_responses_tool_choice(tool_choice)?,
);
}
let synthetic_request = TransformRequest {
headers: req.headers.clone(),
path: "/v1/chat/completions".to_string(),
body: Bytes::from(
serde_json::to_vec(&serde_json::Value::Object(synthetic_body)).map_err(|e| {
TransformError::InvalidFormat(format!(
"Responses synthetic request serialization failed: {e}"
))
})?,
),
};
openai_to_anthropic(&synthetic_request)
}
pub(crate) fn responses_tools_to_chat_tools(
tools: &[OpenAiResponsesTool],
) -> Result<Vec<serde_json::Value>, TransformError> {
let mut chat_tools = Vec::new();
for tool in tools {
if !tool.tool_type.is_empty() && tool.tool_type != "function" {
tracing::debug!(
"lossy downgrade: skipping unsupported Responses tool type '{}'",
tool.tool_type
);
continue;
}
let name = tool.name.as_ref().ok_or_else(|| {
TransformError::MissingRequiredField("Responses tools[].name".to_string())
})?;
let mut function = serde_json::Map::new();
function.insert("name".to_string(), serde_json::Value::String(name.clone()));
if let Some(description) = &tool.description {
function.insert(
"description".to_string(),
serde_json::Value::String(description.clone()),
);
}
if let Some(parameters) = &tool.parameters {
function.insert("parameters".to_string(), parameters.clone());
}
chat_tools.push(json!({
"type": "function",
"function": serde_json::Value::Object(function),
}));
}
Ok(chat_tools)
}
pub(crate) fn normalize_responses_tool_choice(
tool_choice: &serde_json::Value,
) -> Result<serde_json::Value, TransformError> {
match tool_choice {
serde_json::Value::String(choice) => match choice.as_str() {
"auto" | "none" | "required" => Ok(serde_json::Value::String(choice.clone())),
other => Err(TransformError::InvalidFormat(format!(
"unsupported Responses tool_choice string: {other}"
))),
},
serde_json::Value::Object(map) => {
let choice_type = map
.get("type")
.and_then(serde_json::Value::as_str)
.unwrap_or_default();
match choice_type {
"function" => {
let name = map
.get("name")
.and_then(serde_json::Value::as_str)
.or_else(|| {
map.get("function")
.and_then(|value| value.get("name"))
.and_then(serde_json::Value::as_str)
})
.ok_or_else(|| {
TransformError::MissingRequiredField(
"Responses tool_choice.name".to_string(),
)
})?;
Ok(json!({
"type": "function",
"function": {
"name": name,
},
}))
}
"auto" | "none" | "required" => {
Ok(serde_json::Value::String(choice_type.to_string()))
}
other => Err(TransformError::InvalidFormat(format!(
"unsupported Responses tool_choice object type: {other}"
))),
}
}
other => Err(TransformError::InvalidFormat(format!(
"unsupported Responses tool_choice type: {other:?}"
))),
}
}
pub(crate) fn responses_input_to_chat_messages(
input: Option<&serde_json::Value>,
) -> Result<Vec<serde_json::Value>, TransformError> {
match input {
None | Some(serde_json::Value::Null) => Ok(Vec::new()),
Some(serde_json::Value::String(text)) => Ok(vec![json!({
"role": "user",
"content": text,
})]),
Some(serde_json::Value::Array(items)) => {
let mut messages = Vec::new();
for item in items {
messages.extend(responses_input_item_to_chat_messages(item)?);
}
Ok(messages)
}
Some(serde_json::Value::Object(obj)) => {
let item: serde_json::Value = serde_json::Value::Object(obj.clone());
responses_input_item_to_chat_messages(&item)
}
Some(other) => Err(TransformError::InvalidFormat(format!(
"unsupported Responses input type: {other:?}"
))),
}
}
pub(crate) fn responses_input_item_to_chat_messages(
item: &serde_json::Value,
) -> Result<Vec<serde_json::Value>, TransformError> {
let item_type = item.get("type").and_then(serde_json::Value::as_str);
match item_type {
Some("message") | None if item.get("role").is_some() => {
let role = item
.get("role")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
TransformError::MissingRequiredField("Responses message.role".to_string())
})?;
let chat_role = match role {
"developer" => "system",
"system" | "user" | "assistant" | "tool" => role,
other => {
tracing::debug!(
"lossy downgrade: mapping unsupported Responses role '{}' to 'user'",
other
);
"user"
}
};
Ok(vec![json!({
"role": chat_role,
"content": responses_content_to_text(item.get("content")),
})])
}
Some("function_call_output") => {
let call_id = item
.get("call_id")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
TransformError::MissingRequiredField(
"Responses function_call_output.call_id".to_string(),
)
})?;
Ok(vec![json!({
"role": "tool",
"tool_call_id": call_id,
"content": responses_content_to_text(item.get("output")),
})])
}
Some("function_call") => {
let call_id = item
.get("call_id")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
TransformError::MissingRequiredField(
"Responses function_call.call_id".to_string(),
)
})?;
let name = item
.get("name")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
TransformError::MissingRequiredField("Responses function_call.name".to_string())
})?;
let arguments = item
.get("arguments")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
Ok(vec![json!({
"role": "assistant",
"content": "",
"tool_calls": [{
"id": call_id,
"type": "function",
"function": {
"name": name,
"arguments": arguments,
},
}],
})])
}
Some("reasoning") => {
tracing::debug!("lossy downgrade: skipping standalone Responses reasoning input item");
Ok(Vec::new())
}
Some(other) => {
tracing::debug!(
"lossy downgrade: skipping unsupported Responses input item type '{}'",
other
);
Ok(Vec::new())
}
None => Ok(Vec::new()),
}
}
pub(crate) fn responses_content_to_text(content: Option<&serde_json::Value>) -> String {
match content {
Some(serde_json::Value::String(text)) => text.clone(),
Some(serde_json::Value::Array(parts)) => parts
.iter()
.filter_map(response_content_part_to_text)
.collect::<Vec<_>>()
.join("\n"),
Some(serde_json::Value::Object(obj)) => {
let part: serde_json::Value = serde_json::Value::Object(obj.clone());
response_content_part_to_text(&part).unwrap_or_default()
}
_ => String::new(),
}
}
pub(crate) fn response_content_part_to_text(part: &serde_json::Value) -> Option<String> {
match part {
serde_json::Value::String(text) => Some(text.clone()),
serde_json::Value::Object(map) => map
.get("text")
.and_then(serde_json::Value::as_str)
.map(str::to_string),
_ => None,
}
}