use crate::proxy::error::ProxyError;
use serde_json::{json, Value};
pub fn anthropic_to_responses(
body: Value,
cache_key: Option<&str>,
is_codex_oauth: bool,
) -> Result<Value, ProxyError> {
let mut result = json!({});
if let Some(model) = body.get("model").and_then(|m| m.as_str()) {
result["model"] = json!(model);
}
if let Some(system) = body.get("system") {
let instructions = if let Some(text) = system.as_str() {
super::transform::sanitize_system_text(text)
.map(|text| text.into_owned())
.unwrap_or_default()
} else if let Some(arr) = system.as_array() {
arr.iter()
.filter_map(|msg| {
msg.get("text")
.and_then(|t| t.as_str())
.and_then(super::transform::sanitize_system_text)
.map(|text| text.into_owned())
})
.collect::<Vec<_>>()
.join("\n\n")
} else {
String::new()
};
if !instructions.is_empty() {
result["instructions"] = json!(instructions);
}
}
if let Some(msgs) = body.get("messages").and_then(|m| m.as_array()) {
result["input"] = json!(convert_messages_to_input(msgs)?);
}
if let Some(v) = body.get("max_tokens") {
result["max_output_tokens"] = v.clone();
}
if let Some(v) = body.get("temperature") {
result["temperature"] = v.clone();
}
if let Some(v) = body.get("top_p") {
result["top_p"] = v.clone();
}
if let Some(v) = body.get("stream") {
result["stream"] = v.clone();
}
if let Some(model_name) = body.get("model").and_then(|m| m.as_str()) {
if super::transform::supports_reasoning_effort(model_name) {
if let Some(effort) = super::transform::resolve_reasoning_effort(&body) {
result["reasoning"] = json!({ "effort": effort });
}
}
}
if let Some(tools) = body.get("tools").and_then(|t| t.as_array()) {
let response_tools: Vec<Value> = tools
.iter()
.filter(|t| t.get("type").and_then(|v| v.as_str()) != Some("BatchTool"))
.map(|t| {
json!({
"type": "function",
"name": t.get("name").and_then(|n| n.as_str()).unwrap_or(""),
"description": t.get("description"),
"parameters": super::transform::clean_schema(
t.get("input_schema").cloned().unwrap_or(json!({}))
)
})
})
.collect();
if !response_tools.is_empty() {
result["tools"] = json!(response_tools);
}
}
if let Some(v) = body.get("tool_choice") {
result["tool_choice"] = map_tool_choice_to_responses(v);
}
if let Some(key) = cache_key {
result["prompt_cache_key"] = json!(key);
}
if is_codex_oauth {
result["store"] = json!(false);
const REASONING_MARKER: &str = "reasoning.encrypted_content";
let mut includes: Vec<Value> = body
.get("include")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if !includes
.iter()
.any(|v| v.as_str() == Some(REASONING_MARKER))
{
includes.push(json!(REASONING_MARKER));
}
result["include"] = json!(includes);
if let Some(obj) = result.as_object_mut() {
obj.remove("max_output_tokens");
obj.remove("temperature");
obj.remove("top_p");
obj.entry("instructions".to_string()).or_insert(json!(""));
obj.entry("tools".to_string()).or_insert(json!([]));
obj.entry("parallel_tool_calls".to_string())
.or_insert(json!(false));
obj.insert("stream".to_string(), json!(true));
}
}
Ok(result)
}
fn map_tool_choice_to_responses(tool_choice: &Value) -> Value {
match tool_choice {
Value::String(_) => tool_choice.clone(),
Value::Object(obj) => match obj.get("type").and_then(|t| t.as_str()) {
Some("any") => json!("required"),
Some("auto") => json!("auto"),
Some("none") => json!("none"),
Some("tool") => {
let name = obj.get("name").and_then(|n| n.as_str()).unwrap_or("");
json!({
"type": "function",
"name": name
})
}
_ => tool_choice.clone(),
},
_ => tool_choice.clone(),
}
}
pub(crate) fn map_responses_stop_reason(
status: Option<&str>,
has_tool_use: bool,
incomplete_reason: Option<&str>,
) -> Option<&'static str> {
status.map(|s| match s {
"completed" => {
if has_tool_use {
"tool_use"
} else {
"end_turn"
}
}
"incomplete" => {
if matches!(
incomplete_reason,
Some("max_output_tokens") | Some("max_tokens")
) || incomplete_reason.is_none()
{
"max_tokens"
} else {
"end_turn"
}
}
_ => "end_turn",
})
}
pub(crate) fn build_anthropic_usage_from_responses(usage: Option<&Value>) -> Value {
let u = match usage {
Some(v) if !v.is_null() => v,
_ => {
return json!({
"input_tokens": 0,
"output_tokens": 0
})
}
};
let input = u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
let output = u.get("output_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
let mut result = json!({
"input_tokens": input,
"output_tokens": output
});
if let Some(cached) = u
.pointer("/input_tokens_details/cached_tokens")
.and_then(|v| v.as_u64())
{
result["cache_read_input_tokens"] = json!(cached);
}
if let Some(cached) = u
.pointer("/prompt_tokens_details/cached_tokens")
.and_then(|v| v.as_u64())
{
if result.get("cache_read_input_tokens").is_none() {
result["cache_read_input_tokens"] = json!(cached);
}
}
if let Some(v) = u.get("cache_read_input_tokens") {
result["cache_read_input_tokens"] = v.clone();
}
if let Some(v) = u.get("cache_creation_input_tokens") {
result["cache_creation_input_tokens"] = v.clone();
}
result
}
fn convert_messages_to_input(messages: &[Value]) -> Result<Vec<Value>, ProxyError> {
let mut input = Vec::new();
for msg in messages {
let role = msg.get("role").and_then(|r| r.as_str()).unwrap_or("user");
let content = msg.get("content");
match content {
Some(Value::String(text)) => {
let content_type = if role == "assistant" {
"output_text"
} else {
"input_text"
};
input.push(json!({
"role": role,
"content": [{ "type": content_type, "text": text }]
}));
}
Some(Value::Array(blocks)) => {
let mut message_content = Vec::new();
for block in blocks {
let block_type = block.get("type").and_then(|t| t.as_str()).unwrap_or("");
match block_type {
"text" => {
if let Some(text) = block.get("text").and_then(|t| t.as_str()) {
let content_type = if role == "assistant" {
"output_text"
} else {
"input_text"
};
message_content.push(json!({
"type": content_type,
"text": text
}));
}
}
"image" => {
if let Some(source) = block.get("source") {
let media_type = source
.get("media_type")
.and_then(|m| m.as_str())
.unwrap_or("image/png");
let data =
source.get("data").and_then(|d| d.as_str()).unwrap_or("");
message_content.push(json!({
"type": "input_image",
"image_url": format!("data:{media_type};base64,{data}")
}));
}
}
"tool_use" => {
if !message_content.is_empty() {
input.push(json!({
"role": role,
"content": message_content.clone()
}));
message_content.clear();
}
let id = block.get("id").and_then(|i| i.as_str()).unwrap_or("");
let name = block.get("name").and_then(|n| n.as_str()).unwrap_or("");
let arguments = block.get("input").cloned().unwrap_or(json!({}));
input.push(json!({
"type": "function_call",
"call_id": id,
"name": name,
"arguments": serde_json::to_string(&arguments).unwrap_or_default()
}));
}
"tool_result" => {
if !message_content.is_empty() {
input.push(json!({
"role": role,
"content": message_content.clone()
}));
message_content.clear();
}
let call_id = block
.get("tool_use_id")
.and_then(|i| i.as_str())
.unwrap_or("");
let output = match block.get("content") {
Some(Value::String(s)) => s.clone(),
Some(v) => serde_json::to_string(v).unwrap_or_default(),
None => String::new(),
};
input.push(json!({
"type": "function_call_output",
"call_id": call_id,
"output": output
}));
}
"thinking" => {}
_ => {}
}
}
if !message_content.is_empty() {
input.push(json!({
"role": role,
"content": message_content
}));
}
}
_ => {
input.push(json!({ "role": role }));
}
}
}
Ok(input)
}
pub fn responses_to_anthropic(body: Value) -> Result<Value, ProxyError> {
let output = body
.get("output")
.and_then(|o| o.as_array())
.ok_or_else(|| ProxyError::TransformError("No output in response".to_string()))?;
let mut content = Vec::new();
let mut has_tool_use = false;
for item in output {
let item_type = item.get("type").and_then(|t| t.as_str()).unwrap_or("");
match item_type {
"message" => {
if let Some(msg_content) = item.get("content").and_then(|c| c.as_array()) {
for block in msg_content {
let block_type = block.get("type").and_then(|t| t.as_str()).unwrap_or("");
if block_type == "output_text" {
if let Some(text) = block.get("text").and_then(|t| t.as_str()) {
if !text.is_empty() {
content.push(json!({"type": "text", "text": text}));
}
}
} else if block_type == "refusal" {
if let Some(refusal) = block.get("refusal").and_then(|t| t.as_str()) {
if !refusal.is_empty() {
content.push(json!({"type": "text", "text": refusal}));
}
}
}
}
}
}
"function_call" => {
let call_id = item.get("call_id").and_then(|i| i.as_str()).unwrap_or("");
let name = item.get("name").and_then(|n| n.as_str()).unwrap_or("");
let args_str = item
.get("arguments")
.and_then(|a| a.as_str())
.unwrap_or("{}");
let input: Value = serde_json::from_str(args_str).unwrap_or(json!({}));
content.push(json!({
"type": "tool_use",
"id": call_id,
"name": name,
"input": input
}));
has_tool_use = true;
}
"reasoning" => {
if let Some(summary) = item.get("summary").and_then(|s| s.as_array()) {
let thinking_text: String = summary
.iter()
.filter_map(|s| {
if s.get("type").and_then(|t| t.as_str()) == Some("summary_text") {
s.get("text").and_then(|t| t.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("");
if !thinking_text.is_empty() {
content.push(json!({
"type": "thinking",
"thinking": thinking_text
}));
}
}
}
_ => {}
}
}
Ok(json!({
"id": body.get("id").and_then(|i| i.as_str()).unwrap_or(""),
"type": "message",
"role": "assistant",
"content": content,
"model": body.get("model").and_then(|m| m.as_str()).unwrap_or(""),
"stop_reason": map_responses_stop_reason(
body.get("status").and_then(|s| s.as_str()),
has_tool_use,
body.pointer("/incomplete_details/reason")
.and_then(|r| r.as_str()),
),
"stop_sequence": null,
"usage": build_anthropic_usage_from_responses(body.get("usage"))
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anthropic_to_responses_removes_billing_header_from_system_string() {
let input = json!({
"model": "gpt-5",
"system": "x-anthropic-billing-header: cc_version=2.1.120.cf9; cc_entrypoint=cli; cch=543cf;\nYou are helpful.",
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false).expect("transform responses");
assert_eq!(result["instructions"], json!("You are helpful."));
}
#[test]
fn anthropic_to_responses_removes_billing_header_from_system_array() {
let input = json!({
"model": "gpt-5",
"system": [{
"type": "text",
"text": "x-anthropic-billing-header: cc_version=2.1.120.cf9; cc_entrypoint=cli; cch=543cf;\nProject instructions"
}],
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false).expect("transform responses");
assert_eq!(result["instructions"], json!("Project instructions"));
}
#[test]
fn anthropic_to_responses_omits_empty_billing_header_system_block() {
let input = json!({
"model": "gpt-5",
"system": [{
"type": "text",
"text": "x-anthropic-billing-header: cc_version=2.1.120.cf9; cc_entrypoint=cli; cch=543cf;"
}],
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false).expect("transform responses");
assert!(result.get("instructions").is_none());
}
#[test]
fn anthropic_to_responses_codex_oauth_sets_required_contract_fields() {
let input = json!({
"model": "gpt-5-codex",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, true).expect("transform responses");
assert_eq!(result["store"], json!(false));
assert_eq!(result["include"], json!(["reasoning.encrypted_content"]));
assert_eq!(result["instructions"], json!(""));
assert_eq!(result["tools"], json!([]));
assert_eq!(result["parallel_tool_calls"], json!(false));
assert_eq!(result["stream"], json!(true));
}
#[test]
fn anthropic_to_responses_codex_oauth_strips_unsupported_fields() {
let input = json!({
"model": "gpt-5-codex",
"max_tokens": 1024,
"temperature": 0.7,
"top_p": 0.9,
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, true).expect("transform responses");
assert!(result.get("max_output_tokens").is_none());
assert!(result.get("temperature").is_none());
assert!(result.get("top_p").is_none());
}
#[test]
fn anthropic_to_responses_non_codex_keeps_openai_fields() {
let input = json!({
"model": "gpt-5-codex",
"max_tokens": 1024,
"temperature": 0.7,
"top_p": 0.9,
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false).expect("transform responses");
assert_eq!(result["max_output_tokens"], json!(1024));
assert_eq!(result["temperature"], json!(0.7));
assert_eq!(result["top_p"], json!(0.9));
assert!(result.get("store").is_none());
}
#[test]
fn anthropic_to_responses_maps_reasoning_effort_for_gpt5_models() {
let input = json!({
"model": "gpt-5.4",
"max_tokens": 1024,
"thinking": {"type": "adaptive"},
"messages": [{"role": "user", "content": "Hello"}]
});
let result = anthropic_to_responses(input, None, false).expect("transform responses");
assert_eq!(result["reasoning"]["effort"], json!("xhigh"));
}
}