1use serde_json::Value;
20
21use crate::provider::{LLMError, ToolDefinition};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum ProviderFamily {
26 OpenAIChat,
28 OpenAIResponses,
30 Anthropic,
32 Gemini,
34 OpenAICompatible,
36}
37
38impl ProviderFamily {
39 #[must_use]
44 fn from_provider_id(provider_id: &str) -> Self {
45 match provider_id.to_ascii_lowercase().as_str() {
46 "openai" => Self::OpenAIChat,
47 "openai-responses" | "openai_responses" => Self::OpenAIResponses,
48 "anthropic" | "claude" => Self::Anthropic,
49 "gemini" | "google" | "google-gemini" => Self::Gemini,
50 _ => Self::OpenAICompatible,
51 }
52 }
53}
54
55pub trait ProviderToolFormatter: Send + Sync {
60 fn family(&self) -> ProviderFamily;
62
63 fn supported_extensions(&self) -> &'static [&'static str];
67
68 fn supports(&self, tool: &ToolDefinition) -> bool;
70
71 fn format_tools(&self, tools: &[ToolDefinition], model: &str) -> Result<Option<Value>, LLMError>;
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub struct AnthropicFormatter;
82
83impl ProviderToolFormatter for AnthropicFormatter {
84 fn family(&self) -> ProviderFamily {
85 ProviderFamily::Anthropic
86 }
87
88 fn supported_extensions(&self) -> &'static [&'static str] {
89 &[
90 "input_examples",
91 "strict",
92 "allowed_callers",
93 "defer_loading",
94 "web_search_options",
95 "tool_search",
96 "code_execution",
97 "memory",
98 ]
99 }
100
101 fn supports(&self, tool: &ToolDefinition) -> bool {
102 tool.is_tool_search()
103 || tool.is_anthropic_web_search()
104 || tool.is_anthropic_code_execution()
105 || tool.is_anthropic_memory_tool()
106 || tool.function.is_some()
107 }
108
109 fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Result<Option<Value>, LLMError> {
110 super::anthropic::request_builder::tools::build_tools_via_formatter(tools)
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub struct OpenAIChatFormatter;
121
122impl ProviderToolFormatter for OpenAIChatFormatter {
123 fn family(&self) -> ProviderFamily {
124 ProviderFamily::OpenAIChat
125 }
126
127 fn supported_extensions(&self) -> &'static [&'static str] {
128 &["function_only"]
129 }
130
131 fn supports(&self, tool: &ToolDefinition) -> bool {
132 tool.tool_type == "function" || tool.tool_type == "web_search"
133 }
134
135 fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Result<Option<Value>, LLMError> {
136 Ok(super::common::serialize_tools_openai_format(tools).map(Value::Array))
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
142pub struct OpenAIResponsesFormatter;
143
144impl ProviderToolFormatter for OpenAIResponsesFormatter {
145 fn family(&self) -> ProviderFamily {
146 ProviderFamily::OpenAIResponses
147 }
148
149 fn supported_extensions(&self) -> &'static [&'static str] {
150 &[
151 "defer_loading",
152 "shell",
153 "apply_patch",
154 "custom",
155 "grammar",
156 "tool_search",
157 "hosted_web_search",
158 "hosted_file_search",
159 "hosted_mcp",
160 ]
161 }
162
163 fn supports(&self, _tool: &ToolDefinition) -> bool {
164 true
167 }
168
169 fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Result<Option<Value>, LLMError> {
170 Ok(super::openai::tool_serialization::serialize_tools_for_responses(tools, None))
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177pub struct GeminiFormatter;
178
179impl ProviderToolFormatter for GeminiFormatter {
180 fn family(&self) -> ProviderFamily {
181 ProviderFamily::Gemini
182 }
183
184 fn supported_extensions(&self) -> &'static [&'static str] {
185 &[
186 "google_search",
187 "google_maps",
188 "url_context",
189 "code_execution",
190 "function_declarations",
191 ]
192 }
193
194 fn supports(&self, tool: &ToolDefinition) -> bool {
195 matches!(
196 tool.tool_type.as_str(),
197 "function" | "google_search" | "google_maps" | "url_context" | "code_execution"
198 ) || tool.function.is_some()
199 }
200
201 fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Result<Option<Value>, LLMError> {
202 super::gemini::helpers::serialize_gemini_tools(tools)
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212pub struct OpenAICompatibleFormatter;
213
214impl ProviderToolFormatter for OpenAICompatibleFormatter {
215 fn family(&self) -> ProviderFamily {
216 ProviderFamily::OpenAICompatible
217 }
218
219 fn supported_extensions(&self) -> &'static [&'static str] {
220 &["function_only"]
221 }
222
223 fn supports(&self, tool: &ToolDefinition) -> bool {
224 tool.tool_type == "function" || tool.tool_type == "web_search"
225 }
226
227 fn format_tools(&self, tools: &[ToolDefinition], _model: &str) -> Result<Option<Value>, LLMError> {
228 Ok(super::common::serialize_tools_openai_format(tools).map(Value::Array))
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245pub enum ProviderFormatter {
246 OpenAIChat(OpenAIChatFormatter),
248 OpenAIResponses(OpenAIResponsesFormatter),
250 Anthropic(AnthropicFormatter),
252 Gemini(GeminiFormatter),
254 OpenAICompatible(OpenAICompatibleFormatter),
256}
257
258impl ProviderToolFormatter for ProviderFormatter {
259 fn family(&self) -> ProviderFamily {
260 match self {
261 Self::OpenAIChat(_) => ProviderFamily::OpenAIChat,
262 Self::OpenAIResponses(_) => ProviderFamily::OpenAIResponses,
263 Self::Anthropic(_) => ProviderFamily::Anthropic,
264 Self::Gemini(_) => ProviderFamily::Gemini,
265 Self::OpenAICompatible(_) => ProviderFamily::OpenAICompatible,
266 }
267 }
268
269 fn supported_extensions(&self) -> &'static [&'static str] {
270 match self {
271 Self::OpenAIChat(f) => f.supported_extensions(),
272 Self::OpenAIResponses(f) => f.supported_extensions(),
273 Self::Anthropic(f) => f.supported_extensions(),
274 Self::Gemini(f) => f.supported_extensions(),
275 Self::OpenAICompatible(f) => f.supported_extensions(),
276 }
277 }
278
279 fn supports(&self, tool: &ToolDefinition) -> bool {
280 match self {
281 Self::OpenAIChat(f) => f.supports(tool),
282 Self::OpenAIResponses(f) => f.supports(tool),
283 Self::Anthropic(f) => f.supports(tool),
284 Self::Gemini(f) => f.supports(tool),
285 Self::OpenAICompatible(f) => f.supports(tool),
286 }
287 }
288
289 fn format_tools(&self, tools: &[ToolDefinition], model: &str) -> Result<Option<Value>, LLMError> {
290 match self {
291 Self::OpenAIChat(f) => f.format_tools(tools, model),
292 Self::OpenAIResponses(f) => f.format_tools(tools, model),
293 Self::Anthropic(f) => f.format_tools(tools, model),
294 Self::Gemini(f) => f.format_tools(tools, model),
295 Self::OpenAICompatible(f) => f.format_tools(tools, model),
296 }
297 }
298}
299
300#[must_use]
306pub fn formatter(family: ProviderFamily) -> ProviderFormatter {
307 match family {
308 ProviderFamily::OpenAIChat => ProviderFormatter::OpenAIChat(OpenAIChatFormatter),
309 ProviderFamily::OpenAIResponses => ProviderFormatter::OpenAIResponses(OpenAIResponsesFormatter),
310 ProviderFamily::Anthropic => ProviderFormatter::Anthropic(AnthropicFormatter),
311 ProviderFamily::Gemini => ProviderFormatter::Gemini(GeminiFormatter),
312 ProviderFamily::OpenAICompatible => ProviderFormatter::OpenAICompatible(OpenAICompatibleFormatter),
313 }
314}
315
316#[must_use]
322fn formatter_for_family(family: ProviderFamily) -> Box<dyn ProviderToolFormatter> {
323 Box::new(formatter(family))
324}
325
326#[must_use]
329fn formatter_for_provider(provider_id: &str) -> Box<dyn ProviderToolFormatter> {
330 formatter_for_family(ProviderFamily::from_provider_id(provider_id))
331}
332
333#[must_use]
337pub fn formatter_for(family: ProviderFamily) -> Box<dyn ProviderToolFormatter> {
338 formatter_for_family(family)
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use serde_json::json;
345
346 fn sample_function_tool() -> ToolDefinition {
347 ToolDefinition::function(
348 "search_docs".to_owned(),
349 "Search documentation".to_owned(),
350 json!({
351 "type": "object",
352 "properties": {
353 "query": {"type": "string"}
354 },
355 "required": ["query"]
356 }),
357 )
358 }
359
360 #[test]
361 fn provider_family_resolution_handles_known_ids() {
362 assert_eq!(ProviderFamily::from_provider_id("openai"), ProviderFamily::OpenAIChat);
363 assert_eq!(ProviderFamily::from_provider_id("anthropic"), ProviderFamily::Anthropic);
364 assert_eq!(ProviderFamily::from_provider_id("gemini"), ProviderFamily::Gemini);
365 assert_eq!(ProviderFamily::from_provider_id("deepseek"), ProviderFamily::OpenAICompatible);
366 assert_eq!(ProviderFamily::from_provider_id("unknown"), ProviderFamily::OpenAICompatible);
367 }
368
369 #[test]
370 fn formatter_for_provider_returns_trait_object() {
371 let f = formatter_for_provider("anthropic");
372 assert_eq!(f.family(), ProviderFamily::Anthropic);
373
374 let f = formatter_for_provider("openai");
375 assert_eq!(f.family(), ProviderFamily::OpenAIChat);
376
377 let f = formatter_for_provider("deepseek");
378 assert_eq!(f.family(), ProviderFamily::OpenAICompatible);
379 }
380
381 #[test]
382 fn empty_tool_slice_formats_to_none() {
383 let f = formatter_for_provider("anthropic");
386 assert!(
387 f.format_tools(&[], "claude-opus-4-7")
388 .expect("empty formatting should succeed")
389 .is_none()
390 );
391
392 let f = formatter_for_provider("deepseek");
393 assert!(
394 f.format_tools(&[], "deepseek-chat")
395 .expect("empty formatting should succeed")
396 .is_none()
397 );
398 }
399
400 #[test]
401 fn openai_compatible_formatter_silently_drops_extensions() {
402 let tool = sample_function_tool().with_strict(true).with_defer_loading(true);
405
406 let f = formatter_for_provider("deepseek");
407 let value = f
408 .format_tools(std::slice::from_ref(&tool), "deepseek-chat")
409 .expect("formatter should serialize")
410 .expect("formatter should yield a value for a non-empty slice");
411 let arr = value.as_array().expect("expected array");
412 assert_eq!(arr.len(), 1);
413 let serialized = &arr[0];
414 assert!(serialized.get("defer_loading").is_none(), "openai-compatible formatter must drop defer_loading");
415 assert!(serialized.get("strict").is_none(), "openai-compatible formatter must drop strict");
416 }
417
418 #[test]
419 fn anthropic_formatter_preserves_function_extension_fields() {
420 let tool = sample_function_tool().with_strict(true).with_input_examples(vec![json!({
423 "input": "Find Rust docs",
424 "tool_use": { "query": "rust" }
425 })]);
426
427 let f = formatter_for_provider("anthropic");
428 assert!(f.supports(&tool));
429
430 let value = f
431 .format_tools(std::slice::from_ref(&tool), "claude-opus-4-7")
432 .expect("formatter should yield a value");
433 let serialized = value.expect("non-empty tool should serialize").to_string();
434 assert!(serialized.contains("strict"), "anthropic wire payload missing strict: {serialized}");
435 assert!(serialized.contains("input_examples"), "anthropic wire payload missing input_examples: {serialized}");
436 }
437
438 #[test]
439 fn static_formatter_matches_boxed_formatter_for_every_family() {
440 for family in [
443 ProviderFamily::OpenAIChat,
444 ProviderFamily::OpenAIResponses,
445 ProviderFamily::Anthropic,
446 ProviderFamily::Gemini,
447 ProviderFamily::OpenAICompatible,
448 ] {
449 let statically = formatter(family);
450 let boxed = formatter_for_family(family);
451 assert_eq!(statically.family(), family);
452 assert_eq!(statically.family(), boxed.family());
453 assert_eq!(statically.supported_extensions(), boxed.supported_extensions());
454 }
455 }
456
457 #[test]
458 fn static_formatter_has_no_wide_pointer_overhead() {
459 assert_eq!(size_of::<ProviderFormatter>(), 1);
463 assert_eq!(size_of::<Box<dyn ProviderToolFormatter>>(), 2 * size_of::<usize>());
464 }
465
466 #[test]
467 fn formatter_extensions_are_non_empty_for_every_family() {
468 for family in [
471 ProviderFamily::OpenAIChat,
472 ProviderFamily::OpenAIResponses,
473 ProviderFamily::Anthropic,
474 ProviderFamily::Gemini,
475 ProviderFamily::OpenAICompatible,
476 ] {
477 let f = formatter_for_family(family);
478 assert!(!f.supported_extensions().is_empty(), "{family:?} must report at least one extension");
479 }
480 }
481}