Skip to main content

tool_parser/
factory.rs

1// Factory and pool for creating model-specific tool parsers with pooling support.
2
3use 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, Glm4MoeParser,
13        InklingParser, JsonParser, KimiK2Parser, KimiK3Parser, LlamaParser, MinimaxM2Parser,
14        MistralParser, PassthroughParser, PythonicParser, QwenParser, QwenXmlParser,
15        SarashinaParser, Step3Parser,
16    },
17    traits::ToolParser,
18};
19
20/// Type alias for pooled parser instances.
21pub type PooledParser = Arc<Mutex<Box<dyn ToolParser>>>;
22
23/// Type alias for parser creator functions.
24type ParserCreator = Arc<dyn Fn() -> Box<dyn ToolParser> + Send + Sync>;
25
26/// Function that builds the complete structural tag JSON for a set of tools.
27/// Takes (tools, at_least_one) and returns the full xgrammar structural tag value.
28type BuildStructuralTagFn = Arc<dyn Fn(&[Tool], bool) -> serde_json::Value + Send + Sync>;
29
30/// Constraint type returned by [`ParserRegistry::generate_tool_constraint`].
31#[derive(Debug, Clone)]
32pub enum ToolConstraint {
33    /// JSON schema constraint — output is pure JSON from token 0.
34    /// The model-specific parser is NOT used; the generic "json" parser handles it.
35    JsonSchema(String),
36    /// Structural tag constraint — output includes model-native framing tokens.
37    /// The model-specific parser IS used to parse the response.
38    StructuralTag(String),
39}
40
41impl ToolConstraint {
42    /// Convert to (type, value) tuple for gRPC constraint building.
43    pub fn to_tuple(&self) -> (String, String) {
44        match self {
45            ToolConstraint::JsonSchema(s) => ("json_schema".to_string(), s.clone()),
46            ToolConstraint::StructuralTag(s) => ("structural_tag".to_string(), s.clone()),
47        }
48    }
49
50    /// Returns true if this is a JSON schema constraint.
51    pub fn is_json_schema(&self) -> bool {
52        matches!(self, ToolConstraint::JsonSchema(_))
53    }
54}
55
56/// Registration entry for a parser: creator + optional structural tag builder.
57///
58/// The structural tag builder is queried at preparation time without instantiating the parser.
59/// The creator is called at response processing time.
60struct ParserEntry {
61    creator: ParserCreator,
62    build_structural_tag: Option<BuildStructuralTagFn>,
63}
64
65/// Registry for model-specific tool parsers with pooling support.
66#[derive(Clone)]
67pub struct ParserRegistry {
68    /// Parser entries (creator + metadata)
69    entries: Arc<RwLock<HashMap<String, Arc<ParserEntry>>>>,
70    /// Pooled parser instances for reuse
71    pool: Arc<RwLock<HashMap<String, PooledParser>>>,
72    /// Model pattern to parser name mappings
73    model_mapping: Arc<RwLock<HashMap<String, String>>>,
74    /// Default parser name
75    default_parser: Arc<RwLock<String>>,
76}
77
78impl ParserRegistry {
79    /// Create a new empty registry.
80    pub fn new() -> Self {
81        Self {
82            entries: Arc::new(RwLock::new(HashMap::new())),
83            pool: Arc::new(RwLock::new(HashMap::new())),
84            model_mapping: Arc::new(RwLock::new(HashMap::new())),
85            default_parser: Arc::new(RwLock::new("passthrough".to_string())),
86        }
87    }
88
89    /// Register a parser without structural tag support.
90    pub fn register_parser<F>(&self, name: &str, creator: F)
91    where
92        F: Fn() -> Box<dyn ToolParser> + Send + Sync + 'static,
93    {
94        let mut entries = self.entries.write();
95        entries.insert(
96            name.to_string(),
97            Arc::new(ParserEntry {
98                creator: Arc::new(creator),
99                build_structural_tag: None,
100            }),
101        );
102    }
103
104    /// Register a parser with a structural tag builder.
105    ///
106    /// The `build_structural_tag` function takes `(&[Tool], at_least_one)` and returns
107    /// the full xgrammar structural tag JSON for this parser's native tool-call format.
108    pub fn register_parser_with_structural_tag<F>(
109        &self,
110        name: &str,
111        creator: F,
112        build_structural_tag: fn(&[Tool], bool) -> serde_json::Value,
113    ) where
114        F: Fn() -> Box<dyn ToolParser> + Send + Sync + 'static,
115    {
116        let mut entries = self.entries.write();
117        entries.insert(
118            name.to_string(),
119            Arc::new(ParserEntry {
120                creator: Arc::new(creator),
121                build_structural_tag: Some(Arc::new(build_structural_tag)),
122            }),
123        );
124    }
125
126    /// Map a model name/pattern to a parser
127    pub fn map_model(&self, model: impl Into<String>, parser: impl Into<String>) {
128        let mut mapping = self.model_mapping.write();
129        mapping.insert(model.into(), parser.into());
130    }
131
132    /// Get a pooled parser by exact name.
133    /// Returns a shared parser instance from the pool, creating one if needed.
134    pub fn get_pooled_parser(&self, name: &str) -> Option<PooledParser> {
135        // First check if we have a pooled instance
136        {
137            let pool = self.pool.read();
138            if let Some(parser) = pool.get(name) {
139                return Some(Arc::clone(parser));
140            }
141        }
142
143        // If not in pool, create one and add to pool
144        let entries = self.entries.read();
145        if let Some(entry) = entries.get(name) {
146            let parser = Arc::new(Mutex::new((entry.creator)()));
147
148            // Add to pool for future use
149            let mut pool = self.pool.write();
150            pool.insert(name.to_string(), Arc::clone(&parser));
151
152            Some(parser)
153        } else {
154            None
155        }
156    }
157
158    /// Check if a parser with the given name is registered.
159    pub fn has_parser(&self, name: &str) -> bool {
160        let entries = self.entries.read();
161        entries.contains_key(name)
162    }
163
164    /// Create a fresh (non-pooled) parser instance by exact name.
165    /// Returns a new parser instance for each call - useful for streaming where state isolation is needed.
166    pub fn create_parser(&self, name: &str) -> Option<Box<dyn ToolParser>> {
167        let entries = self.entries.read();
168        entries.get(name).map(|entry| (entry.creator)())
169    }
170
171    /// Check if the parser supports structural tag constraints.
172    pub fn has_structural_tag(&self, name: &str) -> bool {
173        let entries = self.entries.read();
174        entries
175            .get(name)
176            .is_some_and(|e| e.build_structural_tag.is_some())
177    }
178
179    /// Check if the configured parser supports structural tags.
180    /// Returns false if no parser is configured (structural tags require explicit opt-in
181    /// via `--tool-call-parser`).
182    pub fn has_structural_tag_for_parser(&self, configured: Option<&str>) -> bool {
183        configured.is_some_and(|p| self.has_structural_tag(p))
184    }
185
186    /// Generate tool call constraint.
187    ///
188    /// If `configured_parser` supports structural tags → `StructuralTag(json)`.
189    /// Otherwise → `JsonSchema(schema)` for required/function tool_choice.
190    /// Returns `Ok(None)` for auto/none tool_choice.
191    pub fn generate_tool_constraint(
192        &self,
193        configured_parser: Option<&str>,
194        tools: &[Tool],
195        tool_choice: &ToolChoice,
196    ) -> Result<Option<ToolConstraint>, String> {
197        if tools.is_empty() {
198            return Ok(None);
199        }
200
201        let at_least_one = match tool_choice {
202            ToolChoice::Value(ToolChoiceValue::Required) => true,
203            ToolChoice::Function { .. } => true,
204            ToolChoice::AllowedTools { mode, .. } if mode == "required" => true,
205            _ => return Ok(None),
206        };
207
208        // Try structural tag from configured parser
209        if let Some(name) = configured_parser {
210            let entries = self.entries.read();
211            if let Some(entry) = entries.get(name) {
212                if let Some(build_fn) = entry.build_structural_tag.as_ref() {
213                    let tag = build_fn(tools, at_least_one);
214                    let json_str = serde_json::to_string(&tag)
215                        .map_err(|e| format!("Failed to serialize structural tag: {e}"))?;
216                    return Ok(Some(ToolConstraint::StructuralTag(json_str)));
217                }
218            }
219        }
220
221        // Fall back to generic JSON schema
222        match tool_choice {
223            ToolChoice::Function { .. } => {
224                let params_schema = serde_json::to_string(&tools[0].function.parameters)
225                    .map_err(|e| format!("Failed to serialize tool parameters: {e}"))?;
226                Ok(Some(ToolConstraint::JsonSchema(params_schema)))
227            }
228            _ => {
229                let schema = build_required_array_schema(tools)?;
230                Ok(Some(ToolConstraint::JsonSchema(schema)))
231            }
232        }
233    }
234
235    /// Resolve model name to parser name via model mappings.
236    /// Returns None if no mapping matches (caller decides fallback).
237    pub fn resolve_model_to_parser(&self, model: &str) -> Option<String> {
238        let mapping = self.model_mapping.read();
239        // Try exact match
240        if let Some(parser_name) = mapping.get(model) {
241            return Some(parser_name.clone());
242        }
243        // Case-insensitive substring matching (longest pattern wins) so namespaced
244        // and differently-cased ids (e.g. "org/Inkling-Chat") still resolve.
245        let model_lower = model.to_lowercase();
246        mapping
247            .iter()
248            .filter_map(|(pattern, parser_name)| {
249                let stem = pattern.strip_suffix('*')?;
250                model_lower
251                    .contains(&stem.to_lowercase())
252                    .then_some((stem, parser_name))
253            })
254            .max_by_key(|(stem, _)| stem.len())
255            .map(|(_, parser_name)| parser_name.clone())
256    }
257
258    /// Check if a parser can be created for a specific model without actually creating it.
259    pub fn has_parser_for_model(&self, model: &str) -> bool {
260        self.resolve_model_to_parser(model)
261            .is_some_and(|name| self.has_parser(&name))
262    }
263
264    /// Create a fresh (non-pooled) parser instance for a specific model.
265    pub fn create_for_model(&self, model: &str) -> Option<Box<dyn ToolParser>> {
266        let parser_name = self
267            .resolve_model_to_parser(model)
268            .unwrap_or_else(|| self.default_parser.read().clone());
269        self.create_parser(&parser_name)
270    }
271
272    /// Get parser for a specific model
273    pub fn get_pooled_for_model(&self, model: &str) -> Option<PooledParser> {
274        let parser_name = self
275            .resolve_model_to_parser(model)
276            .unwrap_or_else(|| self.default_parser.read().clone());
277        self.get_pooled_parser(&parser_name)
278    }
279
280    /// Clear the parser pool, forcing new instances to be created.
281    pub fn clear_pool(&self) {
282        let mut pool = self.pool.write();
283        pool.clear();
284    }
285
286    /// Set the default parser
287    pub fn set_default_parser(&self, name: impl Into<String>) {
288        let mut default = self.default_parser.write();
289        *default = name.into();
290    }
291}
292
293impl Default for ParserRegistry {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299/// Factory for creating tool parsers based on model type.
300#[derive(Clone)]
301pub struct ParserFactory {
302    registry: ParserRegistry,
303}
304
305impl ParserFactory {
306    /// Create a new factory with default parsers registered.
307    pub fn new() -> Self {
308        let registry = ParserRegistry::new();
309
310        // Register default parsers
311        registry.register_parser("passthrough", || Box::new(PassthroughParser::new()));
312        registry.register_parser("json", || Box::new(JsonParser::new()));
313        registry.register_parser_with_structural_tag(
314            "mistral",
315            || Box::new(MistralParser::new()),
316            MistralParser::build_structural_tag,
317        );
318        registry.register_parser("qwen", || Box::new(QwenParser::new()));
319        registry.register_parser("qwen_xml", || Box::new(QwenXmlParser::new()));
320        registry.register_parser("qwen_coder", || Box::new(QwenXmlParser::new()));
321        // Nemotron-3 family emits the same XML-parameter format as Qwen3-Coder;
322        // named alias so operators can select it explicitly per model.
323        registry.register_parser("nemotron", || Box::new(QwenXmlParser::new()));
324        registry.register_parser("pythonic", || Box::new(PythonicParser::new()));
325        registry.register_parser("llama", || Box::new(LlamaParser::new()));
326        registry.register_parser("deepseek", || Box::new(DeepSeekParser::new()));
327        registry.register_parser("deepseek31", || Box::new(DeepSeek31Parser::new()));
328        registry.register_parser("deepseek32", || Box::new(DeepSeekDsmlParser::v32()));
329        registry.register_parser("deepseek_v4", || Box::new(DeepSeekDsmlParser::v4()));
330        registry.register_parser("glm45_moe", || Box::new(Glm4MoeParser::glm45()));
331        registry.register_parser("glm47_moe", || Box::new(Glm4MoeParser::glm47()));
332        registry.register_parser("step3", || Box::new(Step3Parser::new()));
333        registry.register_parser("sarashina", || Box::new(SarashinaParser::new()));
334        registry.register_parser_with_structural_tag(
335            "kimik2",
336            || Box::new(KimiK2Parser::new()),
337            KimiK2Parser::build_structural_tag,
338        );
339        registry.register_parser_with_structural_tag(
340            "kimi_k3",
341            || Box::new(KimiK3Parser::new()),
342            KimiK3Parser::build_structural_tag,
343        );
344        registry.register_parser_with_structural_tag(
345            "inkling",
346            || Box::new(InklingParser::new()),
347            InklingParser::build_structural_tag,
348        );
349        registry.register_parser("minimax_m2", || Box::new(MinimaxM2Parser::new()));
350        registry.register_parser("cohere", || Box::new(CohereParser::new()));
351
352        // Register default model mappings
353        Self::register_default_mappings(&registry);
354
355        Self { registry }
356    }
357
358    fn register_default_mappings(registry: &ParserRegistry) {
359        // OpenAI models
360        registry.map_model("gpt-4*", "json");
361        registry.map_model("gpt-3.5*", "json");
362        registry.map_model("gpt-4o*", "json");
363
364        // Anthropic models
365        registry.map_model("claude-*", "json");
366
367        // Mistral models
368        registry.map_model("mistral-*", "mistral");
369        registry.map_model("mixtral-*", "mistral");
370
371        // Qwen models (more specific patterns first - longer patterns take precedence)
372        // Qwen3.5+ and Qwen3-Coder use XML format: <tool_call><function=name><parameter=key>value</parameter></function></tool_call>
373        registry.map_model("Qwen/Qwen3.5*", "qwen_xml");
374        registry.map_model("Qwen3.5*", "qwen_xml");
375        registry.map_model("qwen3.5*", "qwen_xml");
376        registry.map_model("qwen/qwen3.5*", "qwen_xml");
377        registry.map_model("Qwen/Qwen3.6*", "qwen_xml");
378        registry.map_model("Qwen3.6*", "qwen_xml");
379        registry.map_model("qwen3.6*", "qwen_xml");
380        registry.map_model("qwen/qwen3.6*", "qwen_xml");
381        registry.map_model("Qwen/Qwen3.8*", "qwen_xml");
382        registry.map_model("Qwen3.8*", "qwen_xml");
383        registry.map_model("qwen3.8*", "qwen_xml");
384        registry.map_model("qwen/qwen3.8*", "qwen_xml");
385        registry.map_model("Qwen/Qwen3-Coder*", "qwen_xml");
386        registry.map_model("Qwen3-Coder*", "qwen_xml");
387        registry.map_model("qwen3-coder*", "qwen_xml");
388        registry.map_model("qwen/qwen3-coder*", "qwen_xml");
389        // Qwen3 and earlier (including Qwen2.5-Coder) use JSON format
390        registry.map_model("qwen*", "qwen");
391        registry.map_model("Qwen*", "qwen");
392
393        // Nemotron-3 generation (Nano/Super/Ultra/3.5-Lightning): the chat
394        // template renders tool calls in the Qwen3-Coder XML-parameter format
395        // (`<tool_call>\n<function=name>\n<parameter=key>value</parameter>...`)
396        // and tool results as `<tool_response>` blocks. Scoped to `nemotron-3`
397        // so earlier Nemotron families with different formats never match
398        // (matching is case-insensitive substring, so one spelling suffices).
399        registry.map_model("nemotron-3*", "qwen_xml");
400
401        // Llama models
402        registry.map_model("llama-4*", "pythonic");
403        registry.map_model("meta-llama-4*", "pythonic");
404        registry.map_model("llama-3.2*", "llama");
405        registry.map_model("meta-llama-3.2*", "llama");
406        registry.map_model("llama-*", "json");
407        registry.map_model("meta-llama-*", "json");
408
409        // DeepSeek models
410        registry.map_model("deepseek-v3*", "deepseek");
411        registry.map_model("deepseek-ai/DeepSeek-V3*", "deepseek");
412        registry.map_model("deepseek-v3.1*", "deepseek31");
413        registry.map_model("deepseek-ai/DeepSeek-V3.1*", "deepseek31");
414        // V3.2-Exp uses V3.1 format (longer patterns take precedence)
415        registry.map_model("deepseek-v3.2-exp*", "deepseek31");
416        registry.map_model("deepseek-ai/DeepSeek-V3.2-Exp*", "deepseek31");
417        // V3.2 DSML format (outer block: function_calls)
418        registry.map_model("deepseek-v3.2*", "deepseek32");
419        registry.map_model("deepseek-ai/DeepSeek-V3.2*", "deepseek32");
420        // V4 DSML format (outer block: tool_calls — same parser as V3.2, different block name)
421        registry.map_model("deepseek-v4*", "deepseek_v4");
422        registry.map_model("deepseek-ai/DeepSeek-V4*", "deepseek_v4");
423        registry.map_model("deepseek-*", "pythonic");
424
425        // GLM models
426        registry.map_model("glm-4.5*", "glm45_moe");
427        registry.map_model("glm-4.6*", "glm45_moe");
428        registry.map_model("glm-4.7*", "glm47_moe");
429        registry.map_model("glm-5*", "glm47_moe");
430        registry.map_model("glm-*", "json");
431
432        // Sarashina models
433        registry.map_model("sarashina*", "sarashina");
434        registry.map_model("Sarashina*", "sarashina");
435
436        // Step3 models
437        registry.map_model("step3*", "step3");
438        registry.map_model("Step-3*", "step3");
439
440        // Kimi models
441        registry.map_model("kimi-k2*", "kimik2");
442        registry.map_model("Kimi-K2*", "kimik2");
443        registry.map_model("moonshot*/Kimi-K2*", "kimik2");
444        registry.map_model("kimi-k3*", "kimi_k3");
445        registry.map_model("Kimi-K3*", "kimi_k3");
446        registry.map_model("moonshot*/Kimi-K3*", "kimi_k3");
447        // Underscore spellings (e.g. `kimi_k3`, `Kimi_K3`) as used by some
448        // checkpoint ids; mirrors the reasoning-parser factory, whose glob
449        // matching is case-insensitive but still enumerates both separators.
450        registry.map_model("kimi_k3*", "kimi_k3");
451        registry.map_model("Kimi_K3*", "kimi_k3");
452        registry.map_model("moonshot*/Kimi_K3*", "kimi_k3");
453
454        // Inkling models use TML JSON tool calls.
455        registry.map_model("inkling*", "inkling");
456        registry.map_model("Inkling*", "inkling");
457
458        // MiniMax models
459        registry.map_model("minimax*", "minimax_m2");
460        registry.map_model("MiniMax*", "minimax_m2");
461
462        // Cohere models
463        registry.map_model("command-r*", "cohere");
464        registry.map_model("command-r-plus*", "cohere");
465        registry.map_model("command-a*", "cohere");
466        registry.map_model("c4ai-command*", "cohere");
467        registry.map_model("cohere*", "cohere");
468        registry.map_model("CohereForAI*", "cohere");
469
470        // Other models
471        registry.map_model("gemini-*", "json");
472        registry.map_model("palm-*", "json");
473        registry.map_model("gemma-*", "json");
474    }
475
476    /// Get a pooled parser for the given model ID.
477    /// Returns a shared instance that can be used concurrently.
478    /// Falls back to passthrough parser if model is not recognized.
479    #[expect(
480        clippy::expect_used,
481        reason = "passthrough parser is registered in new(); None indicates a bug in registration logic"
482    )]
483    pub fn get_pooled(&self, model_id: &str) -> PooledParser {
484        self.registry
485            .get_pooled_for_model(model_id)
486            .unwrap_or_else(|| {
487                // Fallback to passthrough parser (no-op, returns text unchanged)
488                self.registry
489                    .get_pooled_parser("passthrough")
490                    .expect("Passthrough parser should always be registered")
491            })
492    }
493
494    /// Get the internal registry for custom registration.
495    pub fn registry(&self) -> &ParserRegistry {
496        &self.registry
497    }
498
499    /// Clear the parser pool.
500    pub fn clear_pool(&self) {
501        self.registry.clear_pool();
502    }
503
504    /// Get a non-pooled parser for the given model ID (creates a fresh instance each time).
505    /// This is useful for benchmarks and testing where you want independent parser instances.
506    pub fn get_parser(&self, model_id: &str) -> Option<Arc<dyn ToolParser>> {
507        let parser_type = self
508            .registry
509            .resolve_model_to_parser(model_id)
510            .unwrap_or_else(|| self.registry.default_parser.read().clone());
511        let entries = self.registry.entries.read();
512        entries.get(&parser_type).map(|entry| {
513            let boxed_parser = (entry.creator)();
514            Arc::from(boxed_parser)
515        })
516    }
517
518    /// Check if a parser with the given name is registered.
519    pub fn has_parser(&self, name: &str) -> bool {
520        self.registry.entries.read().contains_key(name)
521    }
522
523    /// List all registered parsers (for compatibility with old API).
524    pub fn list_parsers(&self) -> Vec<String> {
525        self.registry.entries.read().keys().cloned().collect()
526    }
527}
528
529impl Default for ParserFactory {
530    fn default() -> Self {
531        Self::new()
532    }
533}
534
535/// Build JSON schema for required tool calls (array with minItems: 1).
536fn build_required_array_schema(tools: &[Tool]) -> Result<String, String> {
537    let mut any_of_schemas = Vec::with_capacity(tools.len());
538    for tool in tools {
539        let tool_schema = json!({
540            "properties": {
541                "name": {
542                    "type": "string",
543                    "enum": [tool.function.name]
544                },
545                "parameters": tool.function.parameters
546            },
547            "required": ["name", "parameters"]
548        });
549        any_of_schemas.push(tool_schema);
550    }
551
552    // Consolidate $defs from all tools
553    let mut all_defs: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
554    for tool in tools {
555        if let serde_json::Value::Object(params) = &tool.function.parameters {
556            if let Some(serde_json::Value::Object(defs)) = params.get("$defs") {
557                for (def_name, def_schema) in defs {
558                    if let Some(existing) = all_defs.get(def_name) {
559                        if existing != def_schema {
560                            return Err(format!(
561                                "Tool definition '{def_name}' has multiple conflicting schemas"
562                            ));
563                        }
564                    } else {
565                        all_defs.insert(def_name.clone(), def_schema.clone());
566                    }
567                }
568            }
569        }
570    }
571
572    let mut array_schema = json!({
573        "type": "array",
574        "minItems": 1,
575        "items": {
576            "type": "object",
577            "anyOf": any_of_schemas
578        }
579    });
580
581    if !all_defs.is_empty() {
582        if let serde_json::Value::Object(ref mut obj) = array_schema {
583            obj.insert("$defs".to_string(), serde_json::Value::Object(all_defs));
584        }
585    }
586
587    serde_json::to_string(&array_schema)
588        .map_err(|e| format!("Failed to serialize tool schema: {e}"))
589}