diff --git a/sgl-router/src/tool_parser/factory.rs b/sgl-router/src/tool_parser/factory.rs
index 3f3a6fba4..c22b2f8c6 100644
--- a/sgl-router/src/tool_parser/factory.rs
+++ b/sgl-router/src/tool_parser/factory.rs
@@ -9,8 +9,8 @@ use tokio::sync::Mutex;
use crate::tool_parser::{
parsers::{
- DeepSeekParser, Glm4MoeParser, JsonParser, KimiK2Parser, LlamaParser, MistralParser,
- PassthroughParser, PythonicParser, QwenParser, Step3Parser,
+ DeepSeekParser, Glm4MoeParser, JsonParser, KimiK2Parser, LlamaParser, MinimaxM2Parser,
+ MistralParser, PassthroughParser, PythonicParser, QwenParser, Step3Parser,
},
traits::ToolParser,
};
@@ -242,6 +242,7 @@ impl ParserFactory {
registry.register_parser("glm4_moe", || Box::new(Glm4MoeParser::new()));
registry.register_parser("step3", || Box::new(Step3Parser::new()));
registry.register_parser("kimik2", || Box::new(KimiK2Parser::new()));
+ registry.register_parser("minimax_m2", || Box::new(MinimaxM2Parser::new()));
// Register default model mappings
Self::register_default_mappings(®istry);
@@ -293,6 +294,10 @@ impl ParserFactory {
registry.map_model("Kimi-K2*", "kimik2");
registry.map_model("moonshot*/Kimi-K2*", "kimik2");
+ // MiniMax models
+ registry.map_model("minimax*", "minimax_m2");
+ registry.map_model("MiniMax*", "minimax_m2");
+
// Other models
registry.map_model("gemini-*", "json");
registry.map_model("palm-*", "json");
diff --git a/sgl-router/src/tool_parser/mod.rs b/sgl-router/src/tool_parser/mod.rs
index da7a703a4..7cfd1ffc4 100644
--- a/sgl-router/src/tool_parser/mod.rs
+++ b/sgl-router/src/tool_parser/mod.rs
@@ -20,8 +20,8 @@ pub use errors::{ParserError, ParserResult};
pub use factory::{ParserFactory, ParserRegistry, PooledParser};
// Re-export parsers for convenience
pub use parsers::{
- DeepSeekParser, Glm4MoeParser, JsonParser, KimiK2Parser, LlamaParser, MistralParser,
- PythonicParser, QwenParser, Step3Parser,
+ DeepSeekParser, Glm4MoeParser, JsonParser, KimiK2Parser, LlamaParser, MinimaxM2Parser,
+ MistralParser, PythonicParser, QwenParser, Step3Parser,
};
pub use traits::{PartialJsonParser, ToolParser};
pub use types::{FunctionCall, PartialToolCall, StreamingParseResult, ToolCall};
diff --git a/sgl-router/src/tool_parser/parsers/minimax_m2.rs b/sgl-router/src/tool_parser/parsers/minimax_m2.rs
new file mode 100644
index 000000000..eff92ba67
--- /dev/null
+++ b/sgl-router/src/tool_parser/parsers/minimax_m2.rs
@@ -0,0 +1,563 @@
+use std::{collections::HashMap, fmt::Write as FmtWrite};
+
+use async_trait::async_trait;
+use regex::Regex;
+use serde_json::Value;
+
+use crate::{
+ protocols::common::Tool,
+ tool_parser::{
+ errors::{ParserError, ParserResult},
+ parsers::helpers,
+ traits::ToolParser,
+ types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
+ },
+};
+
+/// MiniMax M2 format parser for tool calls
+///
+/// Implements the MiniMax-M2 model's XML-based tool calling format as specified in the
+/// official chat template. The M2 model uses a structured XML format for function invocations
+/// to ensure reliable parsing and execution.
+///
+/// ## Format Reference
+/// - HuggingFace Model: https://huggingface.co/MiniMaxAI/MiniMax-M2
+/// - Chat Template: https://huggingface.co/MiniMaxAI/MiniMax-M2?chat_template=default
+///
+/// ## Tool Call Structure
+/// ```xml
+///
+///
+/// value1
+/// value2
+///
+///
+/// ```
+///
+/// ## Key Features
+/// - **Namespaced XML tags**: Uses `minimax:` namespace to avoid conflicts
+/// - **Structured invocation**: Functions wrapped in `` tags
+/// - **Named parameters**: Each parameter uses `value`
+/// - **Incremental streaming**: Converts XML to JSON progressively during streaming
+/// - **XML entity decoding**: Handles encoded entities (`<`, `>`, etc.) in parameter values
+pub struct MinimaxM2Parser {
+ // Regex patterns
+ tool_call_extractor: Regex,
+ invoke_extractor: Regex,
+ param_extractor: Regex,
+
+ // Streaming state
+ buffer: String,
+ prev_tool_call_arr: Vec,
+ current_tool_id: i32,
+ streamed_args_for_tool: Vec,
+ current_function_name: String,
+ current_parameters: HashMap,
+ in_tool_call: bool,
+ function_name_sent: bool,
+ waiting_for_tool_call_end: bool,
+
+ // Token configuration
+ tool_call_start_token: &'static str,
+ tool_call_end_token: &'static str,
+ invoke_end_token: &'static str,
+}
+
+impl MinimaxM2Parser {
+ /// Parse a value from string with consistent logic
+ #[inline]
+ fn parse_value(text: &str) -> Value {
+ // Try parsing as common literals first
+ match text {
+ "true" | "True" => return Value::Bool(true),
+ "false" | "False" => return Value::Bool(false),
+ "null" | "None" => return Value::Null,
+ _ => {}
+ }
+
+ // Try parsing as number
+ if let Ok(num) = text.parse::() {
+ return Value::Number(num.into());
+ }
+
+ if let Ok(num) = text.parse::() {
+ if let Some(n) = serde_json::Number::from_f64(num) {
+ return Value::Number(n);
+ }
+ }
+
+ // Default to string
+ Value::String(text.to_string())
+ }
+
+ /// Create a new MiniMax M2 parser
+ pub fn new() -> Self {
+ // Use (?s) flag for DOTALL mode to handle newlines
+ let tool_call_pattern = r"(?s).*?";
+ let tool_call_extractor = Regex::new(tool_call_pattern).expect("Valid regex pattern");
+
+ let invoke_pattern = r#"(?s)(.*?)"#;
+ let invoke_extractor = Regex::new(invoke_pattern).expect("Valid regex pattern");
+
+ let param_pattern = r#"(?s)(.*?)"#;
+ let param_extractor = Regex::new(param_pattern).expect("Valid regex pattern");
+
+ Self {
+ tool_call_extractor,
+ invoke_extractor,
+ param_extractor,
+ buffer: String::new(),
+ prev_tool_call_arr: Vec::new(),
+ current_tool_id: -1,
+ streamed_args_for_tool: Vec::new(),
+ current_function_name: String::new(),
+ current_parameters: HashMap::new(),
+ in_tool_call: false,
+ function_name_sent: false,
+ waiting_for_tool_call_end: false,
+ tool_call_start_token: "",
+ tool_call_end_token: "",
+ invoke_end_token: "",
+ }
+ }
+
+ /// Parse parameters from parameter tags
+ fn parse_parameters(&self, params_text: &str) -> ParserResult> {
+ let mut parameters = serde_json::Map::new();
+
+ for capture in self.param_extractor.captures_iter(params_text) {
+ let key = capture.get(1).map_or("", |m| m.as_str()).trim();
+ let value_str = capture.get(2).map_or("", |m| m.as_str());
+
+ // Decode XML entities and parse value
+ let decoded_value = self.decode_xml_entities(value_str);
+
+ // Note: We keep JSON-like strings as strings (not parsed JSON)
+ // This matches the behavior of other parsers like GLM4 MOE
+ let value = Self::parse_value(&decoded_value);
+
+ parameters.insert(key.to_string(), value);
+ }
+
+ Ok(parameters)
+ }
+
+ /// Decode common XML entities
+ fn decode_xml_entities(&self, text: &str) -> String {
+ text.replace("<", "<")
+ .replace(">", ">")
+ .replace("&", "&")
+ .replace(""", "\"")
+ .replace("'", "'")
+ }
+
+ /// Parse a single tool call block
+ fn parse_tool_call(&self, block: &str) -> ParserResult