use crate::anthropic;
use crate::openai;
pub fn anthropic_tools_to_openai(tools: &[anthropic::Tool]) -> Vec<openai::ChatTool> {
tools
.iter()
.map(|t| openai::ChatTool {
tool_type: "function".to_string(),
function: openai::FunctionDef {
name: t.name.clone(),
description: t.description.clone(),
parameters: Some(t.input_schema.clone()),
strict: None,
},
})
.collect()
}
pub fn openai_tools_to_anthropic(tools: &[openai::ChatTool]) -> Vec<anthropic::Tool> {
tools
.iter()
.map(|t| anthropic::Tool {
name: t.function.name.clone(),
description: t.function.description.clone(),
input_schema: coerce_anthropic_input_schema(
t.function
.parameters
.clone()
.unwrap_or_else(|| serde_json::json!({})),
),
})
.collect()
}
pub fn coerce_anthropic_input_schema(schema: serde_json::Value) -> serde_json::Value {
let serde_json::Value::Object(mut obj) = schema else {
return serde_json::json!({"type": "object", "properties": {}});
};
if obj.get("type").and_then(|v| v.as_str()) != Some("object") {
obj.insert(
"type".to_string(),
serde_json::Value::String("object".to_string()),
);
}
obj.entry("properties".to_string())
.or_insert_with(|| serde_json::json!({}));
serde_json::Value::Object(obj)
}
const GEMINI_DISALLOWED_SCHEMA_KEYS: &[&str] = &[
"$schema",
"anyOf",
"oneOf",
"allOf",
"not",
"default",
"const",
"$defs",
"definitions",
"additionalProperties",
"$ref",
"if",
"then",
"else",
];
pub fn sanitize_schema_for_gemini(schema: serde_json::Value) -> serde_json::Value {
match schema {
serde_json::Value::Object(mut map) => {
for key in GEMINI_DISALLOWED_SCHEMA_KEYS {
map.remove(*key);
}
let sanitized: serde_json::Map<String, serde_json::Value> = map
.into_iter()
.map(|(k, v)| (k, sanitize_schema_for_gemini(v)))
.collect();
serde_json::Value::Object(sanitized)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(sanitize_schema_for_gemini).collect())
}
other => other,
}
}
pub fn anthropic_tool_choice_to_openai(tc: &anthropic::ToolChoice) -> openai::ChatToolChoice {
match tc {
anthropic::ToolChoice::Auto { .. } => openai::ChatToolChoice::Simple("auto".to_string()),
anthropic::ToolChoice::Any { .. } => openai::ChatToolChoice::Simple("required".to_string()),
anthropic::ToolChoice::None => openai::ChatToolChoice::Simple("none".to_string()),
anthropic::ToolChoice::Tool { name } => {
openai::ChatToolChoice::Named(openai::chat_completions::NamedToolChoice {
choice_type: "function".to_string(),
function: openai::chat_completions::NamedFunction { name: name.clone() },
})
}
}
}
pub fn openai_tool_choice_to_anthropic(tc: &openai::ChatToolChoice) -> anthropic::ToolChoice {
match tc {
openai::ChatToolChoice::Simple(s) => match s.as_str() {
"none" => anthropic::ToolChoice::None,
"required" => anthropic::ToolChoice::Any {
disable_parallel_tool_use: None,
},
_ => anthropic::ToolChoice::Auto {
disable_parallel_tool_use: None,
},
},
openai::ChatToolChoice::Named(named) => anthropic::ToolChoice::Tool {
name: named.function.name.clone(),
},
}
}
pub fn normalize_schema_for_strict(schema: serde_json::Value) -> serde_json::Value {
let mut schema = schema;
let Some(obj) = schema.as_object_mut() else {
return schema;
};
if obj.get("type").and_then(|t| t.as_str()) == Some("object") {
obj.insert(
"additionalProperties".to_string(),
serde_json::Value::Bool(false),
);
let prop_keys: Vec<String> = obj
.get("properties")
.and_then(|p| p.as_object())
.map(|p| p.keys().cloned().collect())
.unwrap_or_default();
if !prop_keys.is_empty() {
let existing: std::collections::HashSet<String> = obj
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut merged: Vec<String> = prop_keys.into_iter().chain(existing).collect();
merged.sort();
merged.dedup();
obj.insert(
"required".to_string(),
serde_json::Value::Array(
merged.into_iter().map(serde_json::Value::String).collect(),
),
);
}
}
if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
for prop_val in props.values_mut() {
*prop_val = normalize_schema_for_strict(prop_val.clone());
}
}
for key in ["anyOf", "oneOf", "allOf"] {
if let Some(arr) = obj.get_mut(key).and_then(|v| v.as_array_mut()) {
for item in arr.iter_mut() {
*item = normalize_schema_for_strict(item.clone());
}
}
}
if let Some(items) = obj.get("items").cloned() {
let normalized = normalize_schema_for_strict(items);
obj.insert("items".to_string(), normalized);
}
for key in ["$defs", "definitions"] {
if let Some(defs) = obj.get_mut(key).and_then(|v| v.as_object_mut()) {
for def_val in defs.values_mut() {
*def_val = normalize_schema_for_strict(def_val.clone());
}
}
}
schema
}
pub fn apply_strict_to_forced_tool(tools: &mut [serde_json::Value], forced_name: &str) {
for tool in tools.iter_mut() {
let Some(function) = tool.get_mut("function") else {
continue;
};
let name_matches = function.get("name").and_then(|n| n.as_str()) == Some(forced_name);
if name_matches {
if let Some(obj) = function.as_object_mut() {
obj.insert("strict".to_string(), serde_json::Value::Bool(true));
let params = obj
.get("parameters")
.cloned()
.filter(|v| !v.is_null())
.unwrap_or_else(|| {
serde_json::json!({
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
})
});
obj.insert(
"parameters".to_string(),
normalize_schema_for_strict(params),
);
}
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
fn sample_anthropic_tool() -> anthropic::Tool {
anthropic::Tool {
name: "get_weather".into(),
description: Some("Get weather for a location".into()),
input_schema: json!({
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}),
}
}
fn sample_openai_tool() -> openai::ChatTool {
openai::ChatTool {
tool_type: "function".into(),
function: openai::FunctionDef {
name: "get_weather".into(),
description: Some("Get weather for a location".into()),
parameters: Some(json!({
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
})),
strict: None,
},
}
}
#[test]
fn anthropic_to_openai_tool() {
let tools = anthropic_tools_to_openai(&[sample_anthropic_tool()]);
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].tool_type, "function");
assert_eq!(tools[0].function.name, "get_weather");
assert_eq!(
tools[0].function.description.as_deref(),
Some("Get weather for a location")
);
assert_eq!(
tools[0].function.parameters,
Some(sample_anthropic_tool().input_schema)
);
}
#[test]
fn openai_to_anthropic_tool() {
let tools = openai_tools_to_anthropic(&[sample_openai_tool()]);
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "get_weather");
assert_eq!(
tools[0].description.as_deref(),
Some("Get weather for a location")
);
assert_eq!(tools[0].input_schema, sample_anthropic_tool().input_schema);
}
#[test]
fn empty_tools_list() {
assert!(anthropic_tools_to_openai(&[]).is_empty());
assert!(openai_tools_to_anthropic(&[]).is_empty());
}
#[test]
fn tool_without_description() {
let tool = anthropic::Tool {
name: "no_desc".into(),
description: None,
input_schema: json!({"type": "object"}),
};
let openai = anthropic_tools_to_openai(&[tool]);
assert!(openai[0].function.description.is_none());
let anthropic = openai_tools_to_anthropic(&openai);
assert!(anthropic[0].description.is_none());
}
#[test]
fn openai_tool_without_parameters_defaults_to_object() {
let tool = openai::ChatTool {
tool_type: "function".into(),
function: openai::FunctionDef {
name: "simple".into(),
description: None,
parameters: None,
strict: None,
},
};
let anthropic = openai_tools_to_anthropic(&[tool]);
assert_eq!(
anthropic[0].input_schema,
json!({"type": "object", "properties": {}})
);
}
#[test]
fn multiple_tools_preserved() {
let tools = vec![
anthropic::Tool {
name: "tool_a".into(),
description: Some("A".into()),
input_schema: json!({"type": "object"}),
},
anthropic::Tool {
name: "tool_b".into(),
description: Some("B".into()),
input_schema: json!({"type": "object", "properties": {"x": {"type": "number"}}}),
},
];
let openai = anthropic_tools_to_openai(&tools);
assert_eq!(openai.len(), 2);
assert_eq!(openai[0].function.name, "tool_a");
assert_eq!(openai[1].function.name, "tool_b");
let back = openai_tools_to_anthropic(&openai);
assert_eq!(back.len(), 2);
assert_eq!(back[0].name, "tool_a");
assert_eq!(back[1].name, "tool_b");
assert_eq!(back[1].input_schema, tools[1].input_schema);
}
#[test]
fn tool_choice_auto() {
let openai = anthropic_tool_choice_to_openai(&anthropic::ToolChoice::Auto {
disable_parallel_tool_use: None,
});
assert!(matches!(openai, openai::ChatToolChoice::Simple(ref s) if s == "auto"));
let back = openai_tool_choice_to_anthropic(&openai);
assert!(matches!(back, anthropic::ToolChoice::Auto { .. }));
}
#[test]
fn tool_choice_any_to_required() {
let openai = anthropic_tool_choice_to_openai(&anthropic::ToolChoice::Any {
disable_parallel_tool_use: None,
});
assert!(matches!(openai, openai::ChatToolChoice::Simple(ref s) if s == "required"));
let back = openai_tool_choice_to_anthropic(&openai);
assert!(matches!(back, anthropic::ToolChoice::Any { .. }));
}
#[test]
fn tool_choice_none() {
let openai = anthropic_tool_choice_to_openai(&anthropic::ToolChoice::None);
assert!(matches!(openai, openai::ChatToolChoice::Simple(ref s) if s == "none"));
let back = openai_tool_choice_to_anthropic(&openai);
assert!(matches!(back, anthropic::ToolChoice::None));
}
#[test]
fn tool_choice_specific_tool() {
let tc = anthropic::ToolChoice::Tool {
name: "get_weather".into(),
};
let openai = anthropic_tool_choice_to_openai(&tc);
match &openai {
openai::ChatToolChoice::Named(named) => {
assert_eq!(named.choice_type, "function");
assert_eq!(named.function.name, "get_weather");
}
_ => panic!("expected Named tool choice"),
}
let back = openai_tool_choice_to_anthropic(&openai);
match back {
anthropic::ToolChoice::Tool { name } => assert_eq!(name, "get_weather"),
other => panic!("expected ToolChoice::Tool, got {:?}", other),
}
}
#[test]
fn openai_unknown_simple_choice_defaults_to_auto() {
let tc = openai::ChatToolChoice::Simple("something_else".into());
assert!(matches!(
openai_tool_choice_to_anthropic(&tc),
anthropic::ToolChoice::Auto { .. }
));
}
#[test]
fn disable_parallel_tool_use_roundtrips_via_serde() {
let json = serde_json::json!({"type": "auto", "disable_parallel_tool_use": true});
let tc: anthropic::ToolChoice = serde_json::from_value(json).unwrap();
match tc {
anthropic::ToolChoice::Auto {
disable_parallel_tool_use: Some(true),
} => {}
other => panic!(
"expected Auto with disable_parallel_tool_use=true, got {:?}",
other
),
}
}
#[test]
fn auto_without_disable_parallel_omits_field_in_json() {
let tc = anthropic::ToolChoice::Auto {
disable_parallel_tool_use: None,
};
let json = serde_json::to_value(&tc).unwrap();
assert_eq!(json, serde_json::json!({"type": "auto"}));
}
#[test]
fn claude_code_read_tool_roundtrip() {
let tool = anthropic::Tool {
name: "Read".into(),
description: Some("Reads a file from the local filesystem.".into()),
input_schema: json!({
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "The absolute path to the file to read"},
"offset": {"type": "number", "description": "The line number to start reading from"},
"limit": {"type": "number", "description": "The number of lines to read"}
},
"required": ["file_path"]
}),
};
let openai = anthropic_tools_to_openai(std::slice::from_ref(&tool));
let back = openai_tools_to_anthropic(&openai);
assert_eq!(back[0].name, tool.name);
assert_eq!(back[0].description, tool.description);
assert_eq!(back[0].input_schema, tool.input_schema);
}
#[test]
fn claude_code_bash_tool_roundtrip() {
let tool = anthropic::Tool {
name: "Bash".into(),
description: Some("Executes a given bash command and returns its output.".into()),
input_schema: json!({
"type": "object",
"properties": {
"command": {"type": "string", "description": "The command to execute"},
"description": {"type": "string", "description": "Description of the command"},
"timeout": {"type": "number", "description": "Optional timeout in milliseconds"}
},
"required": ["command"]
}),
};
let openai = anthropic_tools_to_openai(std::slice::from_ref(&tool));
let back = openai_tools_to_anthropic(&openai);
assert_eq!(back[0].name, tool.name);
assert_eq!(back[0].input_schema, tool.input_schema);
}
#[test]
fn claude_code_edit_tool_roundtrip() {
let tool = anthropic::Tool {
name: "Edit".into(),
description: Some("Performs exact string replacements in files.".into()),
input_schema: json!({
"type": "object",
"properties": {
"file_path": {"type": "string"},
"old_string": {"type": "string"},
"new_string": {"type": "string"},
"replace_all": {"type": "boolean", "default": false}
},
"required": ["file_path", "old_string", "new_string"]
}),
};
let openai = anthropic_tools_to_openai(std::slice::from_ref(&tool));
let back = openai_tools_to_anthropic(&openai);
assert_eq!(back[0].name, tool.name);
assert_eq!(back[0].input_schema, tool.input_schema);
}
#[test]
fn claude_code_grep_tool_with_enum_roundtrip() {
let tool = anthropic::Tool {
name: "Grep".into(),
description: Some("A powerful search tool built on ripgrep.".into()),
input_schema: json!({
"type": "object",
"properties": {
"pattern": {"type": "string"},
"path": {"type": "string"},
"output_mode": {
"type": "string",
"enum": ["content", "files_with_matches", "count"]
}
},
"required": ["pattern"]
}),
};
let openai = anthropic_tools_to_openai(std::slice::from_ref(&tool));
let back = openai_tools_to_anthropic(&openai);
assert_eq!(back[0].input_schema, tool.input_schema);
}
#[test]
fn claude_code_all_six_tools_preserved() {
let tools: Vec<anthropic::Tool> = ["Read", "Write", "Edit", "Bash", "Glob", "Grep"]
.iter()
.map(|name| anthropic::Tool {
name: (*name).to_string(),
description: Some(format!("{} tool", name)),
input_schema: json!({"type": "object"}),
})
.collect();
let openai = anthropic_tools_to_openai(&tools);
assert_eq!(openai.len(), 6);
let back = openai_tools_to_anthropic(&openai);
assert_eq!(back.len(), 6);
for (orig, rt) in tools.iter().zip(back.iter()) {
assert_eq!(orig.name, rt.name);
}
}
#[test]
fn sanitize_strips_disallowed_top_level_fields() {
let schema = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"default": {},
"additionalProperties": false,
"$defs": {"myType": {"type": "string"}}
});
let result = sanitize_schema_for_gemini(schema);
assert!(result.get("$schema").is_none());
assert!(result.get("default").is_none());
assert!(result.get("$defs").is_none());
assert!(result.get("additionalProperties").is_none());
assert_eq!(result["type"], "object");
}
#[test]
fn sanitize_strips_disallowed_nested_fields() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string", "default": "unnamed", "const": "fixed"},
"count": {"anyOf": [{"type": "integer"}, {"type": "null"}]}
}
});
let result = sanitize_schema_for_gemini(schema);
let name_prop = &result["properties"]["name"];
assert!(name_prop.get("default").is_none());
assert!(name_prop.get("const").is_none());
assert_eq!(name_prop["type"], "string");
let count_prop = &result["properties"]["count"];
assert!(count_prop.get("anyOf").is_none());
}
#[test]
fn sanitize_leaves_valid_schema_unchanged() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
});
let result = sanitize_schema_for_gemini(schema.clone());
assert_eq!(result, schema);
}
}
#[cfg(test)]
mod strict_tests {
use super::*;
use serde_json::json;
#[test]
fn normalize_adds_required_and_disables_additional_props() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
}
});
let normalized = normalize_schema_for_strict(schema);
let required = normalized["required"].as_array().unwrap();
assert!(
required.iter().any(|v| v == "name"),
"name should be required"
);
assert!(
required.iter().any(|v| v == "age"),
"age should be required"
);
assert_eq!(normalized["additionalProperties"], json!(false));
}
#[test]
fn normalize_nested_object_properties() {
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"street": {"type": "string"}
}
}
}
});
let normalized = normalize_schema_for_strict(schema);
let nested = &normalized["properties"]["address"];
assert_eq!(nested["additionalProperties"], json!(false));
let nested_required = nested["required"].as_array().unwrap();
assert!(nested_required.iter().any(|v| v == "street"));
}
#[test]
fn normalize_preserves_existing_required() {
let schema = json!({
"type": "object",
"properties": {
"x": {"type": "string"},
"y": {"type": "string"}
},
"required": ["x"]
});
let normalized = normalize_schema_for_strict(schema);
let required = normalized["required"].as_array().unwrap();
assert!(required.iter().any(|v| v == "x"));
assert!(required.iter().any(|v| v == "y"));
}
#[test]
fn normalize_non_object_schema_unchanged() {
let schema = json!({"type": "string"});
let normalized = normalize_schema_for_strict(schema.clone());
assert_eq!(normalized, schema);
}
#[test]
fn apply_strict_to_forced_tool_sets_strict_flag() {
let mut tools: Vec<serde_json::Value> = vec![
serde_json::json!({
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}
}),
serde_json::json!({
"type": "function",
"function": {
"name": "send_email",
"description": "Send email",
"parameters": {
"type": "object",
"properties": {"to": {"type": "string"}, "body": {"type": "string"}}
}
}
}),
];
apply_strict_to_forced_tool(&mut tools, "send_email");
let send_email = &tools[1]["function"];
assert_eq!(send_email["strict"], serde_json::json!(true));
assert_eq!(
send_email["parameters"]["additionalProperties"],
serde_json::json!(false)
);
let get_weather = &tools[0]["function"];
assert!(get_weather
.get("strict")
.map(|v| v.is_null() || v == &serde_json::json!(false))
.unwrap_or(true));
}
#[test]
fn apply_strict_no_match_does_not_panic() {
let mut tools: Vec<serde_json::Value> = vec![serde_json::json!({
"type": "function",
"function": {"name": "foo", "parameters": {"type": "string"}}
})];
apply_strict_to_forced_tool(&mut tools, "nonexistent");
}
}