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};
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
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/// Function that builds what precedes a parser's forced calls when the prompt
31/// ends inside the model's thinking block, by where the engine starts the
32/// grammar ([`GrammarStart`]): one xgrammar format element, or `None` when
33/// nothing precedes the calls.
34type ReasoningPrefixFn = fn(GrammarStart) -> Option<serde_json::Value>;
35
36/// Where the engine that runs a forced call's grammar starts applying it on a
37/// prompt that ends inside the model's thinking block, as its worker
38/// advertises it (the `constrained_decoding_start` label).
39///
40/// An engine's own server picks the grammar's shape from this: it sends
41/// xgrammar's reasoning-aware tags (the thought, its close, then the calls)
42/// when it runs no reasoning parser and the bare calls when it does, because
43/// its grammar backend then waits for the thought's end; an engine with a
44/// reasoning parser set (and `enable_in_reasoning` off) waits the same way.
45/// The gateway reads the same fact from the worker and shapes the calls'
46/// prefix accordingly.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum GrammarStart {
49    /// The worker does not say (an older servicer, an engine without the
50    /// label): free text with nothing owed at its end precedes the calls,
51    /// which fits an engine of either kind.
52    #[default]
53    Unknown,
54    /// The engine applies the grammar only once its reasoning parser has
55    /// seen the model's own `</think>`: nothing precedes the calls, so the
56    /// call is forced right at that gate, as the engine's own server does.
57    AfterReasoning,
58    /// The engine applies the grammar from the first generated token: the
59    /// thought, its close and then the calls, so the call is not forced into
60    /// the thought.
61    FirstToken,
62}
63
64impl GrammarStart {
65    /// The worker label that carries it.
66    pub const LABEL: &'static str = "constrained_decoding_start";
67    /// The label's value for [`Self::AfterReasoning`].
68    pub const AFTER_REASONING: &'static str = "after_reasoning";
69    /// The label's value for [`Self::FirstToken`].
70    pub const FIRST_TOKEN: &'static str = "first_token";
71
72    /// The start a worker's `constrained_decoding_start` label names; anything
73    /// else, the label's absence included, is [`Self::Unknown`].
74    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/// Constraint type returned by [`ParserRegistry::generate_tool_constraint`].
84#[derive(Debug, Clone)]
85pub enum ToolConstraint {
86    /// JSON schema constraint — output is pure JSON from token 0.
87    /// The model-specific parser is NOT used; the generic "json" parser handles it.
88    JsonSchema(String),
89    /// Structural tag constraint — output includes model-native framing tokens.
90    /// The model-specific parser IS used to parse the response.
91    StructuralTag(String),
92}
93
94impl ToolConstraint {
95    /// Convert to (type, value) tuple for gRPC constraint building.
96    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    /// Returns true if this is a JSON schema constraint.
104    pub fn is_json_schema(&self) -> bool {
105        matches!(self, ToolConstraint::JsonSchema(_))
106    }
107}
108
109/// Registration entry for a parser: creator + optional structural tag builder.
110///
111/// The structural tag builder is queried at preparation time without instantiating the parser.
112/// The creator is called at response processing time.
113struct ParserEntry {
114    creator: ParserCreator,
115    build_structural_tag: Option<BuildStructuralTagFn>,
116    reasoning_prefix: Option<ReasoningPrefixFn>,
117}
118
119/// Registry for model-specific tool parsers with pooling support.
120#[derive(Clone)]
121pub struct ParserRegistry {
122    /// Parser entries (creator + metadata)
123    entries: Arc<RwLock<HashMap<String, Arc<ParserEntry>>>>,
124    /// Pooled parser instances for reuse
125    pool: Arc<RwLock<HashMap<String, PooledParser>>>,
126    /// Model pattern to parser name mappings
127    model_mapping: Arc<RwLock<HashMap<String, String>>>,
128    /// Default parser name
129    default_parser: Arc<RwLock<String>>,
130}
131
132impl ParserRegistry {
133    /// Create a new empty registry.
134    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    /// Register a parser without structural tag support.
144    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    /// Register a parser with a structural tag builder.
160    ///
161    /// The `build_structural_tag` function takes `(&[Tool], at_least_one)` and returns
162    /// the full xgrammar structural tag JSON for this parser's native tool-call format.
163    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    /// Register the reasoning block for a parser that already has a structural
183    /// tag builder.
184    ///
185    /// When [`Self::generate_tool_constraint`] is called with `reasoning`
186    /// set, the parser's tag becomes `sequence[prefix, tag.format]` with the
187    /// prefix for [`GrammarStart::Unknown`]: free text, then the forced call.
188    /// Unlike xgrammar's built-in tags with `reasoning=True`, that prefix is
189    /// not closed by the thought's end token: an engine that runs a reasoning
190    /// parser applies the grammar only after the model's own
191    /// `</think>` and would hold the model to a second one. Once the worker
192    /// that runs the grammar is known,
193    /// [`Self::structural_tag_for_grammar_start`] re-shapes the prefix for
194    /// where that engine starts the grammar. Parsers without a prefix keep
195    /// their unwrapped tag whatever `reasoning` says.
196    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    /// Whether the configured parser wraps its structural tag in a reasoning
217    /// block when asked (see [`Self::register_reasoning_prefix`]). False when
218    /// no parser is configured, like [`Self::has_structural_tag_for_parser`].
219    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    /// A forced call's structural tag, as [`Self::generate_tool_constraint`]
229    /// built it on a thinking prompt, re-shaped for where the engine that
230    /// will run it starts the grammar: for [`GrammarStart::AfterReasoning`]
231    /// the prefix is dropped (the grammar starts at the engine's gate, so the
232    /// first constrained token opens a call); for [`GrammarStart::FirstToken`]
233    /// the prefix becomes the parser's thought block, closed by `</think>`,
234    /// so the call follows the thought. `Ok(None)` leaves the tag as it is:
235    /// the start is unknown, the parser has no prefix, or the tag does not
236    /// carry one (a prompt outside the thinking block).
237    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    /// Map a model name/pattern to a parser
284    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    /// Get a pooled parser by exact name.
290    /// Returns a shared parser instance from the pool, creating one if needed.
291    pub fn get_pooled_parser(&self, name: &str) -> Option<PooledParser> {
292        // First check if we have a pooled instance
293        {
294            let pool = self.pool.read();
295            if let Some(parser) = pool.get(name) {
296                return Some(Arc::clone(parser));
297            }
298        }
299
300        // If not in pool, create one and add to pool
301        let entries = self.entries.read();
302        if let Some(entry) = entries.get(name) {
303            let parser = Arc::new(Mutex::new((entry.creator)()));
304
305            // Add to pool for future use
306            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    /// Check if a parser with the given name is registered.
316    pub fn has_parser(&self, name: &str) -> bool {
317        let entries = self.entries.read();
318        entries.contains_key(name)
319    }
320
321    /// Create a fresh (non-pooled) parser instance by exact name.
322    /// Returns a new parser instance for each call - useful for streaming where state isolation is needed.
323    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    /// Check if the parser supports structural tag constraints.
329    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    /// Check if the configured parser supports structural tags.
337    /// Returns false if no parser is configured (structural tags require explicit opt-in
338    /// via `--tool-call-parser`).
339    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    /// Whether required/named responses retain model-native framing rather
344    /// than the generic JSON schema fallback. Gemma4 follows vLLM's native
345    /// choice handling without claiming structural-tag grammar support.
346    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    /// Generate tool call constraint.
352    ///
353    /// If `configured_parser` supports structural tags → `StructuralTag(json)`.
354    /// Gemma4 emits its native format without a decoding constraint.
355    /// Otherwise → `JsonSchema(schema)` for required/function tool_choice.
356    /// Returns `Ok(None)` for auto/none tool_choice.
357    ///
358    /// `reasoning` says the rendered prompt ends inside the model's thinking
359    /// block (the gateway's `chat_reasoning_starts_in_prefill`). A parser that
360    /// registered a reasoning prefix then gets its tag wrapped so the forced
361    /// call follows the reasoning instead of preempting it; every other
362    /// constraint is unchanged and applies from the first generated token.
363    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        if !tool_choice.forces_call() {
375            return Ok(None);
376        }
377
378        // Like vLLM's Gemma4EngineToolParser.adjust_request, let Gemma4 emit
379        // native calls even for required/named choices instead of forcing JSON.
380        if configured_parser == Some("gemma4") && self.has_parser("gemma4") {
381            return Ok(None);
382        }
383
384        // Try structural tag from configured parser
385        if let Some(name) = configured_parser {
386            let entries = self.entries.read();
387            if let Some(entry) = entries.get(name) {
388                if let Some(build_fn) = entry.build_structural_tag.as_ref() {
389                    // A choice that forces a call asks the grammar for at least one.
390                    let mut tag = build_fn(tools, true);
391                    if let (true, Some(prefix)) = (reasoning, entry.reasoning_prefix) {
392                        if let Some(prefix) = prefix(GrammarStart::Unknown) {
393                            tag = wrap_in_reasoning_prefix(tag, prefix)?;
394                        }
395                    }
396                    let json_str = serde_json::to_string(&tag)
397                        .map_err(|e| format!("Failed to serialize structural tag: {e}"))?;
398                    return Ok(Some(ToolConstraint::StructuralTag(json_str)));
399                }
400            }
401        }
402
403        // Fall back to generic JSON schema
404        match tool_choice {
405            ToolChoice::Function { .. } => {
406                let params_schema = serde_json::to_string(tools[0].function.parameters_schema())
407                    .map_err(|e| format!("Failed to serialize tool parameters: {e}"))?;
408                Ok(Some(ToolConstraint::JsonSchema(params_schema)))
409            }
410            _ => {
411                let schema = build_required_array_schema(tools)?;
412                Ok(Some(ToolConstraint::JsonSchema(schema)))
413            }
414        }
415    }
416
417    /// Resolve model name to parser name via model mappings.
418    /// Returns None if no mapping matches (caller decides fallback).
419    pub fn resolve_model_to_parser(&self, model: &str) -> Option<String> {
420        let mapping = self.model_mapping.read();
421        // Try exact match
422        if let Some(parser_name) = mapping.get(model) {
423            return Some(parser_name.clone());
424        }
425        // Case-insensitive substring matching (longest pattern wins) so namespaced
426        // and differently-cased ids (e.g. "org/Inkling-Chat") still resolve.
427        let model_lower = model.to_lowercase();
428        mapping
429            .iter()
430            .filter_map(|(pattern, parser_name)| {
431                let stem = pattern.strip_suffix('*')?;
432                model_lower
433                    .contains(&stem.to_lowercase())
434                    .then_some((stem, parser_name))
435            })
436            .max_by_key(|(stem, _)| stem.len())
437            .map(|(_, parser_name)| parser_name.clone())
438    }
439
440    /// Check if a parser can be created for a specific model without actually creating it.
441    pub fn has_parser_for_model(&self, model: &str) -> bool {
442        self.resolve_model_to_parser(model)
443            .is_some_and(|name| self.has_parser(&name))
444    }
445
446    /// Create a fresh (non-pooled) parser instance for a specific model.
447    pub fn create_for_model(&self, model: &str) -> Option<Box<dyn ToolParser>> {
448        let parser_name = self
449            .resolve_model_to_parser(model)
450            .unwrap_or_else(|| self.default_parser.read().clone());
451        self.create_parser(&parser_name)
452    }
453
454    /// Get parser for a specific model
455    pub fn get_pooled_for_model(&self, model: &str) -> Option<PooledParser> {
456        let parser_name = self
457            .resolve_model_to_parser(model)
458            .unwrap_or_else(|| self.default_parser.read().clone());
459        self.get_pooled_parser(&parser_name)
460    }
461
462    /// Clear the parser pool, forcing new instances to be created.
463    pub fn clear_pool(&self) {
464        let mut pool = self.pool.write();
465        pool.clear();
466    }
467
468    /// Set the default parser
469    pub fn set_default_parser(&self, name: impl Into<String>) {
470        let mut default = self.default_parser.write();
471        *default = name.into();
472    }
473}
474
475impl Default for ParserRegistry {
476    fn default() -> Self {
477        Self::new()
478    }
479}
480
481/// Factory for creating tool parsers based on model type.
482#[derive(Clone)]
483pub struct ParserFactory {
484    registry: ParserRegistry,
485}
486
487impl ParserFactory {
488    /// Create a new factory with default parsers registered.
489    pub fn new() -> Self {
490        let registry = ParserRegistry::new();
491
492        // Register default parsers
493        registry.register_parser("passthrough", || Box::new(PassthroughParser::new()));
494        registry.register_parser("json", || Box::new(JsonParser::new()));
495        registry.register_parser("gemma4", || Box::new(Gemma4Parser::new()));
496        registry.register_parser_with_structural_tag(
497            "mistral",
498            || Box::new(MistralParser::new()),
499            MistralParser::build_structural_tag,
500        );
501        registry.register_parser("qwen", || Box::new(QwenParser::new()));
502        registry.register_parser("qwen_xml", || Box::new(QwenXmlParser::new()));
503        registry.register_parser("qwen_coder", || Box::new(QwenXmlParser::new()));
504        // Nemotron-3 family emits the same XML-parameter format as Qwen3-Coder;
505        // named alias so operators can select it explicitly per model.
506        registry.register_parser("nemotron", || Box::new(QwenXmlParser::new()));
507        registry.register_parser("pythonic", || Box::new(PythonicParser::new()));
508        registry.register_parser("llama", || Box::new(LlamaParser::new()));
509        registry.register_parser("deepseek", || Box::new(DeepSeekParser::new()));
510        registry.register_parser("deepseek31", || Box::new(DeepSeek31Parser::new()));
511        registry.register_parser("deepseek32", || Box::new(DeepSeekDsmlParser::v32()));
512        registry.register_parser("deepseek_v4", || Box::new(DeepSeekDsmlParser::v4()));
513        registry.register_parser_with_structural_tag(
514            "deepseek_v41",
515            || Box::new(DeepSeekDsmlParser::v41()),
516            DeepSeekDsmlParser::build_v41_structural_tag,
517        );
518        registry.register_parser("glm45_moe", || Box::new(Glm4MoeParser::glm45()));
519        registry.register_parser_with_structural_tag(
520            "glm47_moe",
521            || Box::new(Glm4MoeParser::glm47()),
522            Glm4MoeParser::build_structural_tag,
523        );
524        registry.register_reasoning_prefix("glm47_moe", Glm4MoeParser::reasoning_prefix);
525        registry.register_parser("step3", || Box::new(Step3Parser::new()));
526        registry.register_parser("sarashina", || Box::new(SarashinaParser::new()));
527        registry.register_parser_with_structural_tag(
528            "kimik2",
529            || Box::new(KimiK2Parser::new()),
530            KimiK2Parser::build_structural_tag,
531        );
532        registry.register_parser_with_structural_tag(
533            "kimi_k3",
534            || Box::new(KimiK3Parser::new()),
535            KimiK3Parser::build_structural_tag,
536        );
537        registry.register_parser_with_structural_tag(
538            "inkling",
539            || Box::new(InklingParser::new()),
540            InklingParser::build_structural_tag,
541        );
542        registry.register_parser("minimax_m2", || Box::new(MinimaxM2Parser::new()));
543        registry.register_parser_with_structural_tag(
544            "minimax_m3",
545            || Box::new(MinimaxM3Parser::new()),
546            MinimaxM3Parser::build_structural_tag,
547        );
548        registry.register_reasoning_prefix("minimax_m3", MinimaxM3Parser::reasoning_prefix);
549        registry.register_parser_with_structural_tag(
550            "hy_v4",
551            || Box::new(HyV4Parser::new()),
552            HyV4Parser::build_structural_tag,
553        );
554        registry.register_reasoning_prefix("hy_v4", HyV4Parser::reasoning_prefix);
555        registry.register_parser("cohere", || Box::new(CohereParser::new()));
556
557        // Register default model mappings
558        Self::register_default_mappings(&registry);
559
560        Self { registry }
561    }
562
563    fn register_default_mappings(registry: &ParserRegistry) {
564        // OpenAI models
565        registry.map_model("gpt-4*", "json");
566        registry.map_model("gpt-3.5*", "json");
567        registry.map_model("gpt-4o*", "json");
568
569        // Anthropic models
570        registry.map_model("claude-*", "json");
571
572        // Mistral models
573        registry.map_model("mistral-*", "mistral");
574        registry.map_model("mixtral-*", "mistral");
575
576        // Qwen models (more specific patterns first - longer patterns take precedence)
577        // Qwen3.5+ and Qwen3-Coder use XML format: <tool_call><function=name><parameter=key>value</parameter></function></tool_call>
578        registry.map_model("Qwen/Qwen3.5*", "qwen_xml");
579        registry.map_model("Qwen3.5*", "qwen_xml");
580        registry.map_model("qwen3.5*", "qwen_xml");
581        registry.map_model("qwen/qwen3.5*", "qwen_xml");
582        registry.map_model("Qwen/Qwen3.6*", "qwen_xml");
583        registry.map_model("Qwen3.6*", "qwen_xml");
584        registry.map_model("qwen3.6*", "qwen_xml");
585        registry.map_model("qwen/qwen3.6*", "qwen_xml");
586        registry.map_model("Qwen/Qwen3.8*", "qwen_xml");
587        registry.map_model("Qwen3.8*", "qwen_xml");
588        registry.map_model("qwen3.8*", "qwen_xml");
589        registry.map_model("qwen/qwen3.8*", "qwen_xml");
590        registry.map_model("Qwen/Qwen3-Coder*", "qwen_xml");
591        registry.map_model("Qwen3-Coder*", "qwen_xml");
592        registry.map_model("qwen3-coder*", "qwen_xml");
593        registry.map_model("qwen/qwen3-coder*", "qwen_xml");
594        // Qwen3 and earlier (including Qwen2.5-Coder) use JSON format
595        registry.map_model("qwen*", "qwen");
596        registry.map_model("Qwen*", "qwen");
597
598        // Nemotron-3 generation (Nano/Super/Ultra/3.5-Lightning): the chat
599        // template renders tool calls in the Qwen3-Coder XML-parameter format
600        // (`<tool_call>\n<function=name>\n<parameter=key>value</parameter>...`)
601        // and tool results as `<tool_response>` blocks. Scoped to `nemotron-3`
602        // so earlier Nemotron families with different formats never match
603        // (matching is case-insensitive substring, so one spelling suffices).
604        registry.map_model("nemotron-3*", "qwen_xml");
605
606        // Llama models
607        registry.map_model("llama-4*", "pythonic");
608        registry.map_model("meta-llama-4*", "pythonic");
609        registry.map_model("llama-3.2*", "llama");
610        registry.map_model("meta-llama-3.2*", "llama");
611        registry.map_model("llama-*", "json");
612        registry.map_model("meta-llama-*", "json");
613
614        // DeepSeek models
615        registry.map_model("deepseek-v3*", "deepseek");
616        registry.map_model("deepseek-ai/DeepSeek-V3*", "deepseek");
617        registry.map_model("deepseek-v3.1*", "deepseek31");
618        registry.map_model("deepseek-ai/DeepSeek-V3.1*", "deepseek31");
619        // V3.2 DSML format (outer block: function_calls); V3.2-Exp reads here too: its own
620        // template spells V3.1's calls, but the engine renders the checkpoint through the V3.2
621        // encoder
622        registry.map_model("deepseek-v3.2*", "deepseek32");
623        registry.map_model("deepseek-ai/DeepSeek-V3.2*", "deepseek32");
624        // V4 DSML format (outer block: tool_calls — same parser as V3.2, different block name)
625        registry.map_model("deepseek-v4*", "deepseek_v4");
626        registry.map_model("deepseek-ai/DeepSeek-V4*", "deepseek_v4");
627        // V4.1 DSML format (spaced tags); its stems are longer than V4's, so they win
628        registry.map_model("deepseek-v4.1*", "deepseek_v41");
629        registry.map_model("deepseek_v41*", "deepseek_v41");
630        registry.map_model("deepseek-v41*", "deepseek_v41");
631        registry.map_model("deepseek-ai/DeepSeek-V4.1*", "deepseek_v41");
632        // Longest matching stem wins: without this the org-prefixed dot-less
633        // spelling would resolve through `deepseek-ai/DeepSeek-V4*` to V4.
634        registry.map_model("deepseek-ai/DeepSeek-V41*", "deepseek_v41");
635        registry.map_model("deepseek-*", "pythonic");
636
637        // GLM models
638        registry.map_model("glm-4.5*", "glm45_moe");
639        registry.map_model("glm-4.6*", "glm45_moe");
640        registry.map_model("glm-4.7*", "glm47_moe");
641        registry.map_model("glm-5*", "glm47_moe");
642        registry.map_model("glm-*", "json");
643
644        // Sarashina models
645        registry.map_model("sarashina*", "sarashina");
646        registry.map_model("Sarashina*", "sarashina");
647
648        // Step3 models
649        registry.map_model("step3*", "step3");
650        registry.map_model("Step-3*", "step3");
651
652        // Kimi models
653        registry.map_model("kimi-k2*", "kimik2");
654        registry.map_model("Kimi-K2*", "kimik2");
655        registry.map_model("moonshot*/Kimi-K2*", "kimik2");
656        registry.map_model("hy4*", "hy_v4");
657        registry.map_model("hy_v4", "hy_v4");
658        registry.map_model("kimi-k3*", "kimi_k3");
659        registry.map_model("Kimi-K3*", "kimi_k3");
660        registry.map_model("moonshot*/Kimi-K3*", "kimi_k3");
661        // Underscore spellings (e.g. `kimi_k3`, `Kimi_K3`) as used by some
662        // checkpoint ids; mirrors the reasoning-parser factory, whose glob
663        // matching is case-insensitive but still enumerates both separators.
664        registry.map_model("kimi_k3*", "kimi_k3");
665        registry.map_model("Kimi_K3*", "kimi_k3");
666        registry.map_model("moonshot*/Kimi_K3*", "kimi_k3");
667
668        // Inkling models use TML JSON tool calls.
669        registry.map_model("inkling*", "inkling");
670        registry.map_model("Inkling*", "inkling");
671
672        // MiniMax models
673        // General MiniMax IDs default to the M2 parser.
674        registry.map_model("minimax*", "minimax_m2");
675        registry.map_model("MiniMax*", "minimax_m2");
676        // M3 IDs route to the M3 parser via longer (more specific) patterns.
677        registry.map_model("minimax-m3*", "minimax_m3");
678        registry.map_model("MiniMax-M3*", "minimax_m3");
679        registry.map_model("MiniMaxAI/MiniMax-M3*", "minimax_m3");
680        registry.map_model("mm-m3*", "minimax_m3");
681
682        // Cohere models
683        registry.map_model("command-r*", "cohere");
684        registry.map_model("command-r-plus*", "cohere");
685        registry.map_model("command-a*", "cohere");
686        registry.map_model("c4ai-command*", "cohere");
687        registry.map_model("cohere*", "cohere");
688        registry.map_model("CohereForAI*", "cohere");
689
690        // Other models
691        registry.map_model("gemini-*", "json");
692        registry.map_model("palm-*", "json");
693        registry.map_model("gemma-4*", "gemma4");
694        registry.map_model("gemma4*", "gemma4");
695        registry.map_model("gemma-*", "json");
696    }
697
698    /// Get a pooled parser for the given model ID.
699    /// Returns a shared instance that can be used concurrently.
700    /// Falls back to passthrough parser if model is not recognized.
701    #[expect(
702        clippy::expect_used,
703        reason = "passthrough parser is registered in new(); None indicates a bug in registration logic"
704    )]
705    pub fn get_pooled(&self, model_id: &str) -> PooledParser {
706        self.registry
707            .get_pooled_for_model(model_id)
708            .unwrap_or_else(|| {
709                // Fallback to passthrough parser (no-op, returns text unchanged)
710                self.registry
711                    .get_pooled_parser("passthrough")
712                    .expect("Passthrough parser should always be registered")
713            })
714    }
715
716    /// Get the internal registry for custom registration.
717    pub fn registry(&self) -> &ParserRegistry {
718        &self.registry
719    }
720
721    /// Clear the parser pool.
722    pub fn clear_pool(&self) {
723        self.registry.clear_pool();
724    }
725
726    /// Get a non-pooled parser for the given model ID (creates a fresh instance each time).
727    /// This is useful for benchmarks and testing where you want independent parser instances.
728    pub fn get_parser(&self, model_id: &str) -> Option<Arc<dyn ToolParser>> {
729        let parser_type = self
730            .registry
731            .resolve_model_to_parser(model_id)
732            .unwrap_or_else(|| self.registry.default_parser.read().clone());
733        let entries = self.registry.entries.read();
734        entries.get(&parser_type).map(|entry| {
735            let boxed_parser = (entry.creator)();
736            Arc::from(boxed_parser)
737        })
738    }
739
740    /// Check if a parser with the given name is registered.
741    pub fn has_parser(&self, name: &str) -> bool {
742        self.registry.entries.read().contains_key(name)
743    }
744
745    /// List all registered parsers (for compatibility with old API).
746    pub fn list_parsers(&self) -> Vec<String> {
747        self.registry.entries.read().keys().cloned().collect()
748    }
749}
750
751impl Default for ParserFactory {
752    fn default() -> Self {
753        Self::new()
754    }
755}
756
757/// `sequence[prefix, tag.format]`: the free text a forced call follows on a
758/// thinking prompt, then the calls the builder produced. Needs the extended structural-tag shape (a top-level
759/// `format`), which every builder in this crate emits.
760fn wrap_in_reasoning_prefix(
761    mut tag: serde_json::Value,
762    prefix: serde_json::Value,
763) -> Result<serde_json::Value, String> {
764    let format = tag
765        .get_mut("format")
766        .map(serde_json::Value::take)
767        .ok_or_else(|| {
768            "structural tag has no `format` to wrap in a reasoning prefix".to_string()
769        })?;
770    Ok(json!({
771        "format": {
772            "type": "sequence",
773            "elements": [prefix, format],
774        }
775    }))
776}
777
778/// Build JSON schema for required tool calls (array with minItems: 1).
779fn build_required_array_schema(tools: &[Tool]) -> Result<String, String> {
780    let mut any_of_schemas = Vec::with_capacity(tools.len());
781    for tool in tools {
782        let tool_schema = json!({
783            "properties": {
784                "name": {
785                    "type": "string",
786                    "enum": [tool.function.name]
787                },
788                "parameters": tool.function.parameters_schema()
789            },
790            "required": ["name", "parameters"]
791        });
792        any_of_schemas.push(tool_schema);
793    }
794
795    // Consolidate $defs from all tools
796    let mut all_defs: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
797    for tool in tools {
798        if let serde_json::Value::Object(params) = tool.function.parameters_schema() {
799            if let Some(serde_json::Value::Object(defs)) = params.get("$defs") {
800                for (def_name, def_schema) in defs {
801                    if let Some(existing) = all_defs.get(def_name) {
802                        if existing != def_schema {
803                            return Err(format!(
804                                "Tool definition '{def_name}' has multiple conflicting schemas"
805                            ));
806                        }
807                    } else {
808                        all_defs.insert(def_name.clone(), def_schema.clone());
809                    }
810                }
811            }
812        }
813    }
814
815    let mut array_schema = json!({
816        "type": "array",
817        "minItems": 1,
818        "items": {
819            "type": "object",
820            "anyOf": any_of_schemas
821        }
822    });
823
824    if !all_defs.is_empty() {
825        if let serde_json::Value::Object(ref mut obj) = array_schema {
826            obj.insert("$defs".to_string(), serde_json::Value::Object(all_defs));
827        }
828    }
829
830    serde_json::to_string(&array_schema)
831        .map_err(|e| format!("Failed to serialize tool schema: {e}"))
832}