use super::{
ProviderConversationItem, ProviderRequest, anthropic::anthropic_messages_body_with_cache_ttl,
};
use crate::thinking::ThinkingLevel;
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use std::collections::{HashMap, HashSet};
const TOOL_PREFIX: &str = "mcp__magi__";
pub(super) struct ClaudeProjection {
pub(super) system: String,
pub(super) messages: Vec<Value>,
pub(super) manifest: Vec<Value>,
pub(super) extra_body: Value,
}
impl ClaudeProjection {
pub(super) fn from_request(request: &ProviderRequest) -> Result<Self> {
let mut body = anthropic_messages_body_with_cache_ttl(
&request.model,
request,
None,
None,
ThinkingLevel::Default,
);
let mut system = body
.get("system")
.and_then(Value::as_str)
.unwrap_or("")
.to_owned();
system.push_str("\n\n");
system.push_str(super::claude_subscription::CLAUDE_CLI_PROMPT.trim());
let native_tool_names: HashMap<&str, &str> = request
.conversation_items_iter()
.filter_map(|item| match item {
ProviderConversationItem::ResponseItem(item) => Some((
item.get("call_id")?.as_str()?,
item.get("claude_tool_name")?.as_str()?,
)),
_ => None,
})
.collect();
let source_messages = body
.get("messages")
.and_then(Value::as_array)
.context("Claude conversation missing messages")?;
let mut messages: Vec<Value> = Vec::new();
for message in source_messages {
let role = message
.get("role")
.and_then(Value::as_str)
.context("Claude conversation message missing role")?;
if !matches!(role, "user" | "assistant") {
bail!("Claude conversation has unsupported role");
}
let mut content = message
.get("content")
.and_then(Value::as_array)
.context("Claude conversation message missing content")?
.clone();
content.retain(|block| match block.get("type").and_then(Value::as_str) {
Some("thinking") => block
.get("signature")
.and_then(Value::as_str)
.is_some_and(|signature| !signature.is_empty()),
Some("redacted_thinking") => block
.get("data")
.and_then(Value::as_str)
.is_some_and(|data| !data.is_empty()),
_ => true,
});
for block in &mut content {
if block.get("type").and_then(Value::as_str) == Some("tool_use") {
if let Some(name) = block
.get("id")
.and_then(Value::as_str)
.and_then(|id| native_tool_names.get(id))
{
block["name"] = json!(name);
continue;
}
let name = block
.get("name")
.and_then(Value::as_str)
.context("Claude replayed tool call missing name")?;
if name.is_empty()
|| name.len() > 50
|| !name.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-'
})
{
bail!("Claude replayed tool has invalid name");
}
block["name"] = json!(format!("{TOOL_PREFIX}{name}"));
}
}
if content.is_empty() {
continue;
}
if messages.last().and_then(|message| message["role"].as_str()) == Some(role) {
if let Some(parts) = messages
.last_mut()
.and_then(|message| message["content"].as_array_mut())
{
parts.extend(content);
}
} else {
messages.push(json!({"role":role,"content":content}));
}
}
if messages.last().and_then(|message| message["role"].as_str()) != Some("user")
|| messages
.last()
.and_then(|message| message["content"].as_array())
.is_none_or(Vec::is_empty)
{
bail!("Claude conversation must end with a nonempty user or tool result message");
}
let mut names = HashSet::new();
let mut manifest = Vec::new();
let mut tools = Vec::new();
for tool in body
.get("tools")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
let name = tool
.get("name")
.and_then(Value::as_str)
.context("Claude tool missing name")?;
if name.is_empty()
|| name.len() > 50
|| !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
|| !names.insert(name)
{
bail!("Claude tool names must be distinct ASCII identifiers (maximum 50 bytes)");
}
let schema = tool
.get("input_schema")
.filter(|schema| schema.is_object())
.context("Claude tool requires an object schema")?;
if ["oneOf", "allOf", "anyOf"]
.iter()
.any(|key| schema.get(*key).is_some())
{
bail!("Claude tool schema contains unsupported top-level alternatives");
}
let description = tool
.get("description")
.and_then(Value::as_str)
.unwrap_or("");
manifest.push(json!({"name":name,"description":description,"inputSchema":schema}));
tools.push(json!({"name":format!("{TOOL_PREFIX}{name}"),"description":description,"input_schema":schema}));
}
body["tools"] = json!(tools);
let extra_body = json!({"tools":body["tools"]});
Ok(Self {
system,
messages,
manifest,
extra_body,
})
}
}