use serde_json::{Value, json};
use super::map_stop_reason;
use crate::error::Result;
pub(super) fn transform_response(body: &mut Value) -> Result<()> {
if body.get("stop_reason").is_none() {
return Ok(());
}
let id = body.get("id").cloned().unwrap_or(json!(""));
let model = body.get("model").cloned().unwrap_or(json!(""));
let content_blocks = body.get("content").and_then(|v| v.as_array()).cloned();
let text_content: Option<String> = content_blocks
.as_ref()
.map(|blocks| joined_block_text(blocks, "text", "text"));
let reasoning_content: Option<String> = content_blocks.as_ref().and_then(|blocks| {
let joined = joined_block_text(blocks, "thinking", "thinking");
if joined.is_empty() { None } else { Some(joined) }
});
let tool_calls: Option<Vec<Value>> = content_blocks.as_ref().map(|blocks| tool_calls_from_blocks(blocks));
let stop_reason = body.get("stop_reason").and_then(|v| v.as_str()).unwrap_or("end_turn");
let finish_reason = map_stop_reason(stop_reason);
let usage = usage_token_counts(body);
let message = assistant_message(text_content.as_deref(), tool_calls, reasoning_content);
*body = json!({
"id": id,
"object": "chat.completion",
"created": crate::provider::unix_timestamp_secs(),
"model": model,
"choices": [{
"index": 0,
"message": message,
"finish_reason": finish_reason
}],
"usage": {
"prompt_tokens": usage.prompt_tokens,
"completion_tokens": usage.output_tokens,
"total_tokens": usage.prompt_tokens + usage.output_tokens,
"prompt_tokens_details": {
"cached_tokens": usage.cache_read_tokens,
"cache_creation_tokens": usage.cache_creation_tokens,
"audio_tokens": 0
}
}
});
Ok(())
}
fn joined_block_text(blocks: &[Value], block_type: &str, field: &str) -> String {
blocks
.iter()
.filter(|b| b.get("type").and_then(|t| t.as_str()) == Some(block_type))
.filter_map(|b| b.get(field).and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("")
}
fn tool_calls_from_blocks(blocks: &[Value]) -> Vec<Value> {
blocks
.iter()
.filter(|b| {
matches!(
b.get("type").and_then(|t| t.as_str()),
Some("tool_use") | Some("server_tool_use")
)
})
.map(|b| {
let arguments = serde_json::to_string(b.get("input").unwrap_or(&json!({}))).unwrap_or_default();
json!({
"id": b.get("id").cloned().unwrap_or(json!("")),
"type": "function",
"function": {
"name": b.get("name").cloned().unwrap_or(json!("")),
"arguments": arguments
}
})
})
.collect()
}
struct AnthropicUsage {
prompt_tokens: u64,
output_tokens: u64,
cache_read_tokens: u64,
cache_creation_tokens: u64,
}
fn usage_token_counts(body: &Value) -> AnthropicUsage {
let input_tokens = body
.pointer("/usage/input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cache_creation_tokens = body
.pointer("/usage/cache_creation_input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let cache_read_tokens = body
.pointer("/usage/cache_read_input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let output_tokens = body
.pointer("/usage/output_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let prompt_tokens = input_tokens + cache_creation_tokens + cache_read_tokens;
AnthropicUsage {
prompt_tokens,
output_tokens,
cache_read_tokens,
cache_creation_tokens,
}
}
fn assistant_message(
text_content: Option<&str>,
tool_calls: Option<Vec<Value>>,
reasoning_content: Option<String>,
) -> Value {
let has_tool_calls = tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
let message_content = match text_content {
Some(text) if !text.is_empty() => json!(text),
_ => Value::Null,
};
let mut message = json!({
"role": "assistant",
"content": message_content
});
if let (Some(tc), true) = (tool_calls, has_tool_calls) {
message["tool_calls"] = json!(tc);
}
if let Some(reasoning) = reasoning_content {
message["reasoning_content"] = json!(reasoning);
}
message
}