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