use serde_json::Value;
use crate::provider::ToolDefinition;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProviderFamily {
OpenAIChat,
OpenAIResponses,
Anthropic,
Gemini,
OpenAICompatible,
}
impl ProviderFamily {
#[must_use]
pub fn from_provider_id(provider_id: &str) -> Self {
match provider_id.to_ascii_lowercase().as_str() {
"openai" => Self::OpenAIChat,
"openai-responses" | "openai_responses" => Self::OpenAIResponses,
"anthropic" | "claude" => Self::Anthropic,
"gemini" | "google" | "google-gemini" => Self::Gemini,
_ => Self::OpenAICompatible,
}
}
}
pub trait ProviderToolFormatter: Send + Sync {
fn family(&self) -> ProviderFamily;
fn supported_extensions(&self) -> &'static [&'static str];
fn supports(&self, tool: &ToolDefinition) -> bool;
fn format_tools(&self, tools: &[ToolDefinition], model: &str) -> Option<Value>;
}
pub struct AnthropicFormatter;
impl ProviderToolFormatter for AnthropicFormatter {
fn family(&self) -> ProviderFamily {
ProviderFamily::Anthropic
}
fn supported_extensions(&self) -> &'static [&'static str] {
&[
"input_examples",
"strict",
"allowed_callers",
"defer_loading",
"web_search_options",
"tool_search",
"code_execution",
"memory",
]
}
fn supports(&self, tool: &ToolDefinition) -> bool {
tool.is_tool_search()
|| tool.is_anthropic_web_search()
|| tool.is_anthropic_code_execution()
|| tool.is_anthropic_memory_tool()
|| tool.function.is_some()
}
fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Option<Value> {
super::anthropic::request_builder::tools::build_tools_via_formatter(tools)
}
}
pub struct OpenAIChatFormatter;
impl ProviderToolFormatter for OpenAIChatFormatter {
fn family(&self) -> ProviderFamily {
ProviderFamily::OpenAIChat
}
fn supported_extensions(&self) -> &'static [&'static str] {
&["function_only"]
}
fn supports(&self, tool: &ToolDefinition) -> bool {
tool.tool_type == "function" || tool.tool_type == "web_search"
}
fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Option<Value> {
super::common::serialize_tools_openai_format(tools).map(Value::Array)
}
}
pub struct OpenAIResponsesFormatter;
impl ProviderToolFormatter for OpenAIResponsesFormatter {
fn family(&self) -> ProviderFamily {
ProviderFamily::OpenAIResponses
}
fn supported_extensions(&self) -> &'static [&'static str] {
&[
"defer_loading",
"shell",
"apply_patch",
"custom",
"grammar",
"tool_search",
"hosted_web_search",
"hosted_file_search",
"hosted_mcp",
]
}
fn supports(&self, _tool: &ToolDefinition) -> bool {
true
}
fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Option<Value> {
super::openai::tool_serialization::serialize_tools_for_responses(tools, None)
}
}
pub struct GeminiFormatter;
impl ProviderToolFormatter for GeminiFormatter {
fn family(&self) -> ProviderFamily {
ProviderFamily::Gemini
}
fn supported_extensions(&self) -> &'static [&'static str] {
&[
"google_search",
"google_maps",
"url_context",
"code_execution",
"function_declarations",
]
}
fn supports(&self, tool: &ToolDefinition) -> bool {
matches!(
tool.tool_type.as_str(),
"function" | "google_search" | "google_maps" | "url_context" | "code_execution"
) || tool.function.is_some()
}
fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Option<Value> {
super::gemini::helpers::serialize_gemini_tools(tools)
}
}
pub struct OpenAICompatibleFormatter;
impl ProviderToolFormatter for OpenAICompatibleFormatter {
fn family(&self) -> ProviderFamily {
ProviderFamily::OpenAICompatible
}
fn supported_extensions(&self) -> &'static [&'static str] {
&["function_only"]
}
fn supports(&self, tool: &ToolDefinition) -> bool {
tool.tool_type == "function" || tool.tool_type == "web_search"
}
fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Option<Value> {
super::common::serialize_tools_openai_format(tools).map(Value::Array)
}
}
#[must_use]
pub fn formatter_for_family(family: ProviderFamily) -> Box<dyn ProviderToolFormatter> {
match family {
ProviderFamily::OpenAIChat => Box::new(OpenAIChatFormatter),
ProviderFamily::OpenAIResponses => Box::new(OpenAIResponsesFormatter),
ProviderFamily::Anthropic => Box::new(AnthropicFormatter),
ProviderFamily::Gemini => Box::new(GeminiFormatter),
ProviderFamily::OpenAICompatible => Box::new(OpenAICompatibleFormatter),
}
}
#[must_use]
pub fn formatter_for_provider(provider_id: &str) -> Box<dyn ProviderToolFormatter> {
formatter_for_family(ProviderFamily::from_provider_id(provider_id))
}
#[must_use]
pub fn formatter_for(family: ProviderFamily) -> Box<dyn ProviderToolFormatter> {
formatter_for_family(family)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_function_tool() -> ToolDefinition {
ToolDefinition::function(
"search_docs".to_owned(),
"Search documentation".to_owned(),
json!({
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}),
)
}
#[test]
fn provider_family_resolution_handles_known_ids() {
assert_eq!(ProviderFamily::from_provider_id("openai"), ProviderFamily::OpenAIChat);
assert_eq!(ProviderFamily::from_provider_id("anthropic"), ProviderFamily::Anthropic);
assert_eq!(ProviderFamily::from_provider_id("gemini"), ProviderFamily::Gemini);
assert_eq!(ProviderFamily::from_provider_id("deepseek"), ProviderFamily::OpenAICompatible);
assert_eq!(ProviderFamily::from_provider_id("unknown"), ProviderFamily::OpenAICompatible);
}
#[test]
fn formatter_for_provider_returns_trait_object() {
let f = formatter_for_provider("anthropic");
assert_eq!(f.family(), ProviderFamily::Anthropic);
let f = formatter_for_provider("openai");
assert_eq!(f.family(), ProviderFamily::OpenAIChat);
let f = formatter_for_provider("deepseek");
assert_eq!(f.family(), ProviderFamily::OpenAICompatible);
}
#[test]
fn empty_tool_slice_formats_to_none() {
let f = formatter_for_provider("anthropic");
assert!(f.format_tools(&[], "claude-opus-4-7").is_none());
let f = formatter_for_provider("deepseek");
assert!(f.format_tools(&[], "deepseek-chat").is_none());
}
#[test]
fn openai_compatible_formatter_silently_drops_extensions() {
let tool = sample_function_tool().with_strict(true).with_defer_loading(true);
let f = formatter_for_provider("deepseek");
let value = f
.format_tools(std::slice::from_ref(&tool), "deepseek-chat")
.expect("formatter should yield a value for a non-empty slice");
let arr = value.as_array().expect("expected array");
assert_eq!(arr.len(), 1);
let serialized = &arr[0];
assert!(serialized.get("defer_loading").is_none(), "openai-compatible formatter must drop defer_loading");
assert!(serialized.get("strict").is_none(), "openai-compatible formatter must drop strict");
}
#[test]
fn anthropic_formatter_preserves_function_extension_fields() {
let tool = sample_function_tool().with_strict(true).with_input_examples(vec![json!({
"input": "Find Rust docs",
"tool_use": { "query": "rust" }
})]);
let f = formatter_for_provider("anthropic");
assert!(f.supports(&tool));
let value = f
.format_tools(std::slice::from_ref(&tool), "claude-opus-4-7")
.expect("formatter should yield a value");
let serialized = value.to_string();
assert!(serialized.contains("strict"), "anthropic wire payload missing strict: {serialized}");
assert!(serialized.contains("input_examples"), "anthropic wire payload missing input_examples: {serialized}");
}
#[test]
fn formatter_extensions_are_non_empty_for_every_family() {
for family in [
ProviderFamily::OpenAIChat,
ProviderFamily::OpenAIResponses,
ProviderFamily::Anthropic,
ProviderFamily::Gemini,
ProviderFamily::OpenAICompatible,
] {
let f = formatter_for_family(family);
assert!(!f.supported_extensions().is_empty(), "{family:?} must report at least one extension");
}
}
}