use serde_json::{json, Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Block {
Text(String),
ToolCall {
id: Option<String>,
name: String,
args: Value,
},
ToolResult {
tool_call_id: Option<String>,
name: String,
response: Value,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Message {
pub role: Role,
pub blocks: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: Value,
}
impl Message {
#[must_use]
pub fn semantic(&self) -> Message {
let blocks = self
.blocks
.iter()
.map(|b| match b {
Block::Text(t) => Block::Text(t.clone()),
Block::ToolCall { name, args, .. } => Block::ToolCall {
id: None,
name: name.clone(),
args: args.clone(),
},
Block::ToolResult { name, response, .. } => Block::ToolResult {
tool_call_id: None,
name: name.clone(),
response: response.clone(),
},
})
.collect();
Message {
role: self.role,
blocks,
}
}
#[must_use]
pub fn is_gemini_native(&self) -> bool {
self.blocks.iter().all(|b| match b {
Block::Text(_) => true,
Block::ToolCall { id, .. } => id.is_none(),
Block::ToolResult { tool_call_id, .. } => tool_call_id.is_none(),
})
}
}
#[must_use]
pub fn to_anthropic(m: &Message) -> Value {
let role = match m.role {
Role::User => "user",
Role::Assistant => "assistant",
};
let content: Vec<Value> = m
.blocks
.iter()
.map(|b| match b {
Block::Text(t) => json!({ "type": "text", "text": t }),
Block::ToolCall { id, name, args } => {
let mut o = Map::new();
o.insert("type".into(), json!("tool_use"));
if let Some(id) = id {
o.insert("id".into(), json!(id));
}
o.insert("name".into(), json!(name));
o.insert("input".into(), args.clone());
Value::Object(o)
},
Block::ToolResult {
tool_call_id,
response,
..
} => {
let mut o = Map::new();
o.insert("type".into(), json!("tool_result"));
if let Some(id) = tool_call_id {
o.insert("tool_use_id".into(), json!(id));
}
o.insert("content".into(), response.clone());
Value::Object(o)
},
})
.collect();
json!({ "role": role, "content": content })
}
pub fn from_anthropic(v: &Value) -> Result<Message, String> {
let role = match v.get("role").and_then(Value::as_str) {
Some("user") => Role::User,
Some("assistant") => Role::Assistant,
other => return Err(format!("anthropic: bad role {other:?}")),
};
let content = v
.get("content")
.and_then(Value::as_array)
.ok_or("anthropic: content must be an array")?;
let mut blocks = Vec::with_capacity(content.len());
for b in content {
let ty = b
.get("type")
.and_then(Value::as_str)
.ok_or("anthropic: block missing type")?;
match ty {
"text" => blocks.push(Block::Text(
b.get("text")
.and_then(Value::as_str)
.ok_or("anthropic: text block missing text")?
.to_string(),
)),
"tool_use" => blocks.push(Block::ToolCall {
id: b.get("id").and_then(Value::as_str).map(str::to_string),
name: b
.get("name")
.and_then(Value::as_str)
.ok_or("anthropic: tool_use missing name")?
.to_string(),
args: b.get("input").cloned().unwrap_or(Value::Null),
}),
"tool_result" => blocks.push(Block::ToolResult {
tool_call_id: b
.get("tool_use_id")
.and_then(Value::as_str)
.map(str::to_string),
name: b
.get("_name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
response: b.get("content").cloned().unwrap_or(Value::Null),
}),
other => return Err(format!("anthropic: unknown block type {other}")),
}
}
Ok(Message { role, blocks })
}
#[must_use]
pub fn to_gemini(m: &Message) -> Value {
let role = match m.role {
Role::User => "user",
Role::Assistant => "model",
};
let parts: Vec<Value> = m
.blocks
.iter()
.map(|b| match b {
Block::Text(t) => json!({ "text": t }),
Block::ToolCall { name, args, .. } => {
json!({ "functionCall": { "name": name, "args": args } })
},
Block::ToolResult { name, response, .. } => {
json!({ "functionResponse": { "name": name, "response": response } })
},
})
.collect();
json!({ "role": role, "parts": parts })
}
pub fn from_gemini(v: &Value) -> Result<Message, String> {
let role = match v.get("role").and_then(Value::as_str) {
Some("user") => Role::User,
Some("model") => Role::Assistant,
other => return Err(format!("gemini: bad role {other:?}")),
};
let parts = v
.get("parts")
.and_then(Value::as_array)
.ok_or("gemini: parts must be an array")?;
let mut blocks = Vec::with_capacity(parts.len());
for p in parts {
if let Some(t) = p.get("text").and_then(Value::as_str) {
blocks.push(Block::Text(t.to_string()));
} else if let Some(fc) = p.get("functionCall") {
blocks.push(Block::ToolCall {
id: None, name: fc
.get("name")
.and_then(Value::as_str)
.ok_or("gemini: functionCall missing name")?
.to_string(),
args: fc.get("args").cloned().unwrap_or(Value::Null),
});
} else if let Some(fr) = p.get("functionResponse") {
blocks.push(Block::ToolResult {
tool_call_id: None, name: fr
.get("name")
.and_then(Value::as_str)
.ok_or("gemini: functionResponse missing name")?
.to_string(),
response: fr.get("response").cloned().unwrap_or(Value::Null),
});
} else {
return Err(format!("gemini: unrecognized part {p}"));
}
}
Ok(Message { role, blocks })
}
#[must_use]
pub fn tool_to_anthropic(t: &ToolSchema) -> Value {
json!({ "name": t.name, "description": t.description, "input_schema": t.parameters })
}
#[must_use]
pub fn tool_to_gemini(t: &ToolSchema) -> Value {
json!({ "name": t.name, "description": t.description, "parameters": t.parameters })
}
pub fn tool_from_anthropic(v: &Value) -> Result<ToolSchema, String> {
Ok(ToolSchema {
name: v
.get("name")
.and_then(Value::as_str)
.ok_or("anthropic tool: missing name")?
.to_string(),
description: v
.get("description")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
parameters: v.get("input_schema").cloned().unwrap_or(json!({})),
})
}
pub fn tool_from_gemini(v: &Value) -> Result<ToolSchema, String> {
Ok(ToolSchema {
name: v
.get("name")
.and_then(Value::as_str)
.ok_or("gemini tool: missing name")?
.to_string(),
description: v
.get("description")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
parameters: v.get("parameters").cloned().unwrap_or(json!({})),
})
}
#[cfg(test)]
mod tests;