1use std::{collections::HashMap, sync::Arc};
4
5use openai_protocol::common::{Tool, ToolChoice, ToolChoiceValue};
6use parking_lot::RwLock;
7use serde_json::json;
8use tokio::sync::Mutex;
9
10use crate::{
11 parsers::{
12 CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, Gemma4Parser,
13 Glm4MoeParser, HyV4Parser, InklingParser, JsonParser, KimiK2Parser, KimiK3Parser,
14 LlamaParser, MinimaxM2Parser, MinimaxM3Parser, MistralParser, PassthroughParser,
15 PythonicParser, QwenParser, QwenXmlParser, SarashinaParser, Step3Parser,
16 },
17 traits::ToolParser,
18};
19
20pub type PooledParser = Arc<Mutex<Box<dyn ToolParser>>>;
22
23type ParserCreator = Arc<dyn Fn() -> Box<dyn ToolParser> + Send + Sync>;
25
26type BuildStructuralTagFn = Arc<dyn Fn(&[Tool], bool) -> serde_json::Value + Send + Sync>;
29
30type ReasoningPrefixFn = fn(GrammarStart) -> Option<serde_json::Value>;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum GrammarStart {
49 #[default]
53 Unknown,
54 AfterReasoning,
58 FirstToken,
62}
63
64impl GrammarStart {
65 pub const LABEL: &'static str = "constrained_decoding_start";
67 pub const AFTER_REASONING: &'static str = "after_reasoning";
69 pub const FIRST_TOKEN: &'static str = "first_token";
71
72 pub fn from_label(value: Option<&str>) -> Self {
75 match value {
76 Some(Self::AFTER_REASONING) => Self::AfterReasoning,
77 Some(Self::FIRST_TOKEN) => Self::FirstToken,
78 _ => Self::Unknown,
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub enum ToolConstraint {
86 JsonSchema(String),
89 StructuralTag(String),
92}
93
94impl ToolConstraint {
95 pub fn to_tuple(&self) -> (String, String) {
97 match self {
98 ToolConstraint::JsonSchema(s) => ("json_schema".to_string(), s.clone()),
99 ToolConstraint::StructuralTag(s) => ("structural_tag".to_string(), s.clone()),
100 }
101 }
102
103 pub fn is_json_schema(&self) -> bool {
105 matches!(self, ToolConstraint::JsonSchema(_))
106 }
107}
108
109struct ParserEntry {
114 creator: ParserCreator,
115 build_structural_tag: Option<BuildStructuralTagFn>,
116 reasoning_prefix: Option<ReasoningPrefixFn>,
117}
118
119#[derive(Clone)]
121pub struct ParserRegistry {
122 entries: Arc<RwLock<HashMap<String, Arc<ParserEntry>>>>,
124 pool: Arc<RwLock<HashMap<String, PooledParser>>>,
126 model_mapping: Arc<RwLock<HashMap<String, String>>>,
128 default_parser: Arc<RwLock<String>>,
130}
131
132impl ParserRegistry {
133 pub fn new() -> Self {
135 Self {
136 entries: Arc::new(RwLock::new(HashMap::new())),
137 pool: Arc::new(RwLock::new(HashMap::new())),
138 model_mapping: Arc::new(RwLock::new(HashMap::new())),
139 default_parser: Arc::new(RwLock::new("passthrough".to_string())),
140 }
141 }
142
143 pub fn register_parser<F>(&self, name: &str, creator: F)
145 where
146 F: Fn() -> Box<dyn ToolParser> + Send + Sync + 'static,
147 {
148 let mut entries = self.entries.write();
149 entries.insert(
150 name.to_string(),
151 Arc::new(ParserEntry {
152 creator: Arc::new(creator),
153 build_structural_tag: None,
154 reasoning_prefix: None,
155 }),
156 );
157 }
158
159 pub fn register_parser_with_structural_tag<F>(
164 &self,
165 name: &str,
166 creator: F,
167 build_structural_tag: fn(&[Tool], bool) -> serde_json::Value,
168 ) where
169 F: Fn() -> Box<dyn ToolParser> + Send + Sync + 'static,
170 {
171 let mut entries = self.entries.write();
172 entries.insert(
173 name.to_string(),
174 Arc::new(ParserEntry {
175 creator: Arc::new(creator),
176 build_structural_tag: Some(Arc::new(build_structural_tag)),
177 reasoning_prefix: None,
178 }),
179 );
180 }
181
182 pub fn register_reasoning_prefix(&self, name: &str, reasoning_prefix: ReasoningPrefixFn) {
197 let mut entries = self.entries.write();
198 let Some(existing) = entries.get(name).map(Arc::clone) else {
199 debug_assert!(false, "reasoning prefix for unregistered parser {name}");
200 return;
201 };
202 debug_assert!(
203 existing.build_structural_tag.is_some(),
204 "reasoning prefix for parser {name} without a structural tag"
205 );
206 entries.insert(
207 name.to_string(),
208 Arc::new(ParserEntry {
209 creator: Arc::clone(&existing.creator),
210 build_structural_tag: existing.build_structural_tag.clone(),
211 reasoning_prefix: Some(reasoning_prefix),
212 }),
213 );
214 }
215
216 pub fn has_reasoning_prefix(&self, configured: Option<&str>) -> bool {
220 configured.is_some_and(|name| {
221 self.entries
222 .read()
223 .get(name)
224 .is_some_and(|entry| entry.reasoning_prefix.is_some())
225 })
226 }
227
228 pub fn structural_tag_for_grammar_start(
238 &self,
239 configured_parser: Option<&str>,
240 tag: &str,
241 start: GrammarStart,
242 ) -> Result<Option<String>, String> {
243 if start == GrammarStart::Unknown {
244 return Ok(None);
245 }
246 let Some(prefix) = configured_parser.and_then(|name| {
247 self.entries
248 .read()
249 .get(name)
250 .and_then(|entry| entry.reasoning_prefix)
251 }) else {
252 return Ok(None);
253 };
254 let mut tag: serde_json::Value = serde_json::from_str(tag)
255 .map_err(|e| format!("Failed to parse structural tag: {e}"))?;
256 let Some(format) = tag.get_mut("format") else {
257 return Ok(None);
258 };
259 let wrapped = format["type"] == "sequence"
260 && format["elements"]
261 .as_array()
262 .is_some_and(|elements| elements.len() == 2)
263 && prefix(GrammarStart::Unknown)
264 .is_some_and(|unknown| format["elements"][0] == unknown);
265 if !wrapped {
266 return Ok(None);
267 }
268 let calls = format["elements"][1].take();
269 let shaped = match prefix(start) {
270 Some(prefix) => json!({
271 "format": {
272 "type": "sequence",
273 "elements": [prefix, calls],
274 }
275 }),
276 None => json!({ "format": calls }),
277 };
278 serde_json::to_string(&shaped)
279 .map(Some)
280 .map_err(|e| format!("Failed to serialize structural tag: {e}"))
281 }
282
283 pub fn map_model(&self, model: impl Into<String>, parser: impl Into<String>) {
285 let mut mapping = self.model_mapping.write();
286 mapping.insert(model.into(), parser.into());
287 }
288
289 pub fn get_pooled_parser(&self, name: &str) -> Option<PooledParser> {
292 {
294 let pool = self.pool.read();
295 if let Some(parser) = pool.get(name) {
296 return Some(Arc::clone(parser));
297 }
298 }
299
300 let entries = self.entries.read();
302 if let Some(entry) = entries.get(name) {
303 let parser = Arc::new(Mutex::new((entry.creator)()));
304
305 let mut pool = self.pool.write();
307 pool.insert(name.to_string(), Arc::clone(&parser));
308
309 Some(parser)
310 } else {
311 None
312 }
313 }
314
315 pub fn has_parser(&self, name: &str) -> bool {
317 let entries = self.entries.read();
318 entries.contains_key(name)
319 }
320
321 pub fn create_parser(&self, name: &str) -> Option<Box<dyn ToolParser>> {
324 let entries = self.entries.read();
325 entries.get(name).map(|entry| (entry.creator)())
326 }
327
328 pub fn has_structural_tag(&self, name: &str) -> bool {
330 let entries = self.entries.read();
331 entries
332 .get(name)
333 .is_some_and(|e| e.build_structural_tag.is_some())
334 }
335
336 pub fn has_structural_tag_for_parser(&self, configured: Option<&str>) -> bool {
340 configured.is_some_and(|p| self.has_structural_tag(p))
341 }
342
343 pub fn uses_native_tool_format(&self, configured: Option<&str>) -> bool {
347 self.has_structural_tag_for_parser(configured)
348 || configured.is_some_and(|p| p == "gemma4" && self.has_parser(p))
349 }
350
351 pub fn generate_tool_constraint(
364 &self,
365 configured_parser: Option<&str>,
366 tools: &[Tool],
367 tool_choice: &ToolChoice,
368 reasoning: bool,
369 ) -> Result<Option<ToolConstraint>, String> {
370 if tools.is_empty() {
371 return Ok(None);
372 }
373
374 let at_least_one = match tool_choice {
375 ToolChoice::Value(ToolChoiceValue::Required) => true,
376 ToolChoice::Function { .. } => true,
377 ToolChoice::AllowedTools { mode, .. } if mode == "required" => true,
378 _ => return Ok(None),
379 };
380
381 if configured_parser == Some("gemma4") && self.has_parser("gemma4") {
384 return Ok(None);
385 }
386
387 if let Some(name) = configured_parser {
389 let entries = self.entries.read();
390 if let Some(entry) = entries.get(name) {
391 if let Some(build_fn) = entry.build_structural_tag.as_ref() {
392 let mut tag = build_fn(tools, at_least_one);
393 if let (true, Some(prefix)) = (reasoning, entry.reasoning_prefix) {
394 if let Some(prefix) = prefix(GrammarStart::Unknown) {
395 tag = wrap_in_reasoning_prefix(tag, prefix)?;
396 }
397 }
398 let json_str = serde_json::to_string(&tag)
399 .map_err(|e| format!("Failed to serialize structural tag: {e}"))?;
400 return Ok(Some(ToolConstraint::StructuralTag(json_str)));
401 }
402 }
403 }
404
405 match tool_choice {
407 ToolChoice::Function { .. } => {
408 let params_schema = serde_json::to_string(&tools[0].function.parameters)
409 .map_err(|e| format!("Failed to serialize tool parameters: {e}"))?;
410 Ok(Some(ToolConstraint::JsonSchema(params_schema)))
411 }
412 _ => {
413 let schema = build_required_array_schema(tools)?;
414 Ok(Some(ToolConstraint::JsonSchema(schema)))
415 }
416 }
417 }
418
419 pub fn resolve_model_to_parser(&self, model: &str) -> Option<String> {
422 let mapping = self.model_mapping.read();
423 if let Some(parser_name) = mapping.get(model) {
425 return Some(parser_name.clone());
426 }
427 let model_lower = model.to_lowercase();
430 mapping
431 .iter()
432 .filter_map(|(pattern, parser_name)| {
433 let stem = pattern.strip_suffix('*')?;
434 model_lower
435 .contains(&stem.to_lowercase())
436 .then_some((stem, parser_name))
437 })
438 .max_by_key(|(stem, _)| stem.len())
439 .map(|(_, parser_name)| parser_name.clone())
440 }
441
442 pub fn has_parser_for_model(&self, model: &str) -> bool {
444 self.resolve_model_to_parser(model)
445 .is_some_and(|name| self.has_parser(&name))
446 }
447
448 pub fn create_for_model(&self, model: &str) -> Option<Box<dyn ToolParser>> {
450 let parser_name = self
451 .resolve_model_to_parser(model)
452 .unwrap_or_else(|| self.default_parser.read().clone());
453 self.create_parser(&parser_name)
454 }
455
456 pub fn get_pooled_for_model(&self, model: &str) -> Option<PooledParser> {
458 let parser_name = self
459 .resolve_model_to_parser(model)
460 .unwrap_or_else(|| self.default_parser.read().clone());
461 self.get_pooled_parser(&parser_name)
462 }
463
464 pub fn clear_pool(&self) {
466 let mut pool = self.pool.write();
467 pool.clear();
468 }
469
470 pub fn set_default_parser(&self, name: impl Into<String>) {
472 let mut default = self.default_parser.write();
473 *default = name.into();
474 }
475}
476
477impl Default for ParserRegistry {
478 fn default() -> Self {
479 Self::new()
480 }
481}
482
483#[derive(Clone)]
485pub struct ParserFactory {
486 registry: ParserRegistry,
487}
488
489impl ParserFactory {
490 pub fn new() -> Self {
492 let registry = ParserRegistry::new();
493
494 registry.register_parser("passthrough", || Box::new(PassthroughParser::new()));
496 registry.register_parser("json", || Box::new(JsonParser::new()));
497 registry.register_parser("gemma4", || Box::new(Gemma4Parser::new()));
498 registry.register_parser_with_structural_tag(
499 "mistral",
500 || Box::new(MistralParser::new()),
501 MistralParser::build_structural_tag,
502 );
503 registry.register_parser("qwen", || Box::new(QwenParser::new()));
504 registry.register_parser("qwen_xml", || Box::new(QwenXmlParser::new()));
505 registry.register_parser("qwen_coder", || Box::new(QwenXmlParser::new()));
506 registry.register_parser("nemotron", || Box::new(QwenXmlParser::new()));
509 registry.register_parser("pythonic", || Box::new(PythonicParser::new()));
510 registry.register_parser("llama", || Box::new(LlamaParser::new()));
511 registry.register_parser("deepseek", || Box::new(DeepSeekParser::new()));
512 registry.register_parser("deepseek31", || Box::new(DeepSeek31Parser::new()));
513 registry.register_parser("deepseek32", || Box::new(DeepSeekDsmlParser::v32()));
514 registry.register_parser("deepseek_v4", || Box::new(DeepSeekDsmlParser::v4()));
515 registry.register_parser_with_structural_tag(
516 "deepseek_v41",
517 || Box::new(DeepSeekDsmlParser::v41()),
518 DeepSeekDsmlParser::build_v41_structural_tag,
519 );
520 registry.register_parser("glm45_moe", || Box::new(Glm4MoeParser::glm45()));
521 registry.register_parser_with_structural_tag(
522 "glm47_moe",
523 || Box::new(Glm4MoeParser::glm47()),
524 Glm4MoeParser::build_structural_tag,
525 );
526 registry.register_reasoning_prefix("glm47_moe", Glm4MoeParser::reasoning_prefix);
527 registry.register_parser("step3", || Box::new(Step3Parser::new()));
528 registry.register_parser("sarashina", || Box::new(SarashinaParser::new()));
529 registry.register_parser_with_structural_tag(
530 "kimik2",
531 || Box::new(KimiK2Parser::new()),
532 KimiK2Parser::build_structural_tag,
533 );
534 registry.register_parser_with_structural_tag(
535 "kimi_k3",
536 || Box::new(KimiK3Parser::new()),
537 KimiK3Parser::build_structural_tag,
538 );
539 registry.register_parser_with_structural_tag(
540 "inkling",
541 || Box::new(InklingParser::new()),
542 InklingParser::build_structural_tag,
543 );
544 registry.register_parser("minimax_m2", || Box::new(MinimaxM2Parser::new()));
545 registry.register_parser_with_structural_tag(
546 "minimax_m3",
547 || Box::new(MinimaxM3Parser::new()),
548 MinimaxM3Parser::build_structural_tag,
549 );
550 registry.register_reasoning_prefix("minimax_m3", MinimaxM3Parser::reasoning_prefix);
551 registry.register_parser_with_structural_tag(
552 "hy_v4",
553 || Box::new(HyV4Parser::new()),
554 HyV4Parser::build_structural_tag,
555 );
556 registry.register_reasoning_prefix("hy_v4", HyV4Parser::reasoning_prefix);
557 registry.register_parser("cohere", || Box::new(CohereParser::new()));
558
559 Self::register_default_mappings(®istry);
561
562 Self { registry }
563 }
564
565 fn register_default_mappings(registry: &ParserRegistry) {
566 registry.map_model("gpt-4*", "json");
568 registry.map_model("gpt-3.5*", "json");
569 registry.map_model("gpt-4o*", "json");
570
571 registry.map_model("claude-*", "json");
573
574 registry.map_model("mistral-*", "mistral");
576 registry.map_model("mixtral-*", "mistral");
577
578 registry.map_model("Qwen/Qwen3.5*", "qwen_xml");
581 registry.map_model("Qwen3.5*", "qwen_xml");
582 registry.map_model("qwen3.5*", "qwen_xml");
583 registry.map_model("qwen/qwen3.5*", "qwen_xml");
584 registry.map_model("Qwen/Qwen3.6*", "qwen_xml");
585 registry.map_model("Qwen3.6*", "qwen_xml");
586 registry.map_model("qwen3.6*", "qwen_xml");
587 registry.map_model("qwen/qwen3.6*", "qwen_xml");
588 registry.map_model("Qwen/Qwen3.8*", "qwen_xml");
589 registry.map_model("Qwen3.8*", "qwen_xml");
590 registry.map_model("qwen3.8*", "qwen_xml");
591 registry.map_model("qwen/qwen3.8*", "qwen_xml");
592 registry.map_model("Qwen/Qwen3-Coder*", "qwen_xml");
593 registry.map_model("Qwen3-Coder*", "qwen_xml");
594 registry.map_model("qwen3-coder*", "qwen_xml");
595 registry.map_model("qwen/qwen3-coder*", "qwen_xml");
596 registry.map_model("qwen*", "qwen");
598 registry.map_model("Qwen*", "qwen");
599
600 registry.map_model("nemotron-3*", "qwen_xml");
607
608 registry.map_model("llama-4*", "pythonic");
610 registry.map_model("meta-llama-4*", "pythonic");
611 registry.map_model("llama-3.2*", "llama");
612 registry.map_model("meta-llama-3.2*", "llama");
613 registry.map_model("llama-*", "json");
614 registry.map_model("meta-llama-*", "json");
615
616 registry.map_model("deepseek-v3*", "deepseek");
618 registry.map_model("deepseek-ai/DeepSeek-V3*", "deepseek");
619 registry.map_model("deepseek-v3.1*", "deepseek31");
620 registry.map_model("deepseek-ai/DeepSeek-V3.1*", "deepseek31");
621 registry.map_model("deepseek-v3.2-exp*", "deepseek31");
623 registry.map_model("deepseek-ai/DeepSeek-V3.2-Exp*", "deepseek31");
624 registry.map_model("deepseek-v3.2*", "deepseek32");
626 registry.map_model("deepseek-ai/DeepSeek-V3.2*", "deepseek32");
627 registry.map_model("deepseek-v4*", "deepseek_v4");
629 registry.map_model("deepseek-ai/DeepSeek-V4*", "deepseek_v4");
630 registry.map_model("deepseek-v4.1*", "deepseek_v41");
632 registry.map_model("deepseek_v41*", "deepseek_v41");
633 registry.map_model("deepseek-v41*", "deepseek_v41");
634 registry.map_model("deepseek-ai/DeepSeek-V4.1*", "deepseek_v41");
635 registry.map_model("deepseek-ai/DeepSeek-V41*", "deepseek_v41");
638 registry.map_model("deepseek-*", "pythonic");
639
640 registry.map_model("glm-4.5*", "glm45_moe");
642 registry.map_model("glm-4.6*", "glm45_moe");
643 registry.map_model("glm-4.7*", "glm47_moe");
644 registry.map_model("glm-5*", "glm47_moe");
645 registry.map_model("glm-*", "json");
646
647 registry.map_model("sarashina*", "sarashina");
649 registry.map_model("Sarashina*", "sarashina");
650
651 registry.map_model("step3*", "step3");
653 registry.map_model("Step-3*", "step3");
654
655 registry.map_model("kimi-k2*", "kimik2");
657 registry.map_model("Kimi-K2*", "kimik2");
658 registry.map_model("moonshot*/Kimi-K2*", "kimik2");
659 registry.map_model("hy4*", "hy_v4");
660 registry.map_model("hy_v4", "hy_v4");
661 registry.map_model("kimi-k3*", "kimi_k3");
662 registry.map_model("Kimi-K3*", "kimi_k3");
663 registry.map_model("moonshot*/Kimi-K3*", "kimi_k3");
664 registry.map_model("kimi_k3*", "kimi_k3");
668 registry.map_model("Kimi_K3*", "kimi_k3");
669 registry.map_model("moonshot*/Kimi_K3*", "kimi_k3");
670
671 registry.map_model("inkling*", "inkling");
673 registry.map_model("Inkling*", "inkling");
674
675 registry.map_model("minimax*", "minimax_m2");
678 registry.map_model("MiniMax*", "minimax_m2");
679 registry.map_model("minimax-m3*", "minimax_m3");
681 registry.map_model("MiniMax-M3*", "minimax_m3");
682 registry.map_model("MiniMaxAI/MiniMax-M3*", "minimax_m3");
683 registry.map_model("mm-m3*", "minimax_m3");
684
685 registry.map_model("command-r*", "cohere");
687 registry.map_model("command-r-plus*", "cohere");
688 registry.map_model("command-a*", "cohere");
689 registry.map_model("c4ai-command*", "cohere");
690 registry.map_model("cohere*", "cohere");
691 registry.map_model("CohereForAI*", "cohere");
692
693 registry.map_model("gemini-*", "json");
695 registry.map_model("palm-*", "json");
696 registry.map_model("gemma-4*", "gemma4");
697 registry.map_model("gemma4*", "gemma4");
698 registry.map_model("gemma-*", "json");
699 }
700
701 #[expect(
705 clippy::expect_used,
706 reason = "passthrough parser is registered in new(); None indicates a bug in registration logic"
707 )]
708 pub fn get_pooled(&self, model_id: &str) -> PooledParser {
709 self.registry
710 .get_pooled_for_model(model_id)
711 .unwrap_or_else(|| {
712 self.registry
714 .get_pooled_parser("passthrough")
715 .expect("Passthrough parser should always be registered")
716 })
717 }
718
719 pub fn registry(&self) -> &ParserRegistry {
721 &self.registry
722 }
723
724 pub fn clear_pool(&self) {
726 self.registry.clear_pool();
727 }
728
729 pub fn get_parser(&self, model_id: &str) -> Option<Arc<dyn ToolParser>> {
732 let parser_type = self
733 .registry
734 .resolve_model_to_parser(model_id)
735 .unwrap_or_else(|| self.registry.default_parser.read().clone());
736 let entries = self.registry.entries.read();
737 entries.get(&parser_type).map(|entry| {
738 let boxed_parser = (entry.creator)();
739 Arc::from(boxed_parser)
740 })
741 }
742
743 pub fn has_parser(&self, name: &str) -> bool {
745 self.registry.entries.read().contains_key(name)
746 }
747
748 pub fn list_parsers(&self) -> Vec<String> {
750 self.registry.entries.read().keys().cloned().collect()
751 }
752}
753
754impl Default for ParserFactory {
755 fn default() -> Self {
756 Self::new()
757 }
758}
759
760fn wrap_in_reasoning_prefix(
764 mut tag: serde_json::Value,
765 prefix: serde_json::Value,
766) -> Result<serde_json::Value, String> {
767 let format = tag
768 .get_mut("format")
769 .map(serde_json::Value::take)
770 .ok_or_else(|| {
771 "structural tag has no `format` to wrap in a reasoning prefix".to_string()
772 })?;
773 Ok(json!({
774 "format": {
775 "type": "sequence",
776 "elements": [prefix, format],
777 }
778 }))
779}
780
781fn build_required_array_schema(tools: &[Tool]) -> Result<String, String> {
783 let mut any_of_schemas = Vec::with_capacity(tools.len());
784 for tool in tools {
785 let tool_schema = json!({
786 "properties": {
787 "name": {
788 "type": "string",
789 "enum": [tool.function.name]
790 },
791 "parameters": tool.function.parameters
792 },
793 "required": ["name", "parameters"]
794 });
795 any_of_schemas.push(tool_schema);
796 }
797
798 let mut all_defs: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
800 for tool in tools {
801 if let serde_json::Value::Object(params) = &tool.function.parameters {
802 if let Some(serde_json::Value::Object(defs)) = params.get("$defs") {
803 for (def_name, def_schema) in defs {
804 if let Some(existing) = all_defs.get(def_name) {
805 if existing != def_schema {
806 return Err(format!(
807 "Tool definition '{def_name}' has multiple conflicting schemas"
808 ));
809 }
810 } else {
811 all_defs.insert(def_name.clone(), def_schema.clone());
812 }
813 }
814 }
815 }
816 }
817
818 let mut array_schema = json!({
819 "type": "array",
820 "minItems": 1,
821 "items": {
822 "type": "object",
823 "anyOf": any_of_schemas
824 }
825 });
826
827 if !all_defs.is_empty() {
828 if let serde_json::Value::Object(ref mut obj) = array_schema {
829 obj.insert("$defs".to_string(), serde_json::Value::Object(all_defs));
830 }
831 }
832
833 serde_json::to_string(&array_schema)
834 .map_err(|e| format!("Failed to serialize tool schema: {e}"))
835}