Skip to content

Commit 3cdec20

Browse files
authored
[router] add minmax m2 reasoning parser (#13137)
1 parent d28caaf commit 3cdec20

File tree

4 files changed

+187
-2
lines changed

4 files changed

+187
-2
lines changed

sgl-router/src/reasoning_parser/factory.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use tokio::sync::Mutex;
1010

1111
use crate::reasoning_parser::{
1212
parsers::{
13-
BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, Qwen3Parser,
13+
BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, MiniMaxParser, Qwen3Parser,
1414
QwenThinkingParser, Step3Parser,
1515
},
1616
traits::{ParseError, ParserConfig, ReasoningParser},
@@ -189,6 +189,9 @@ impl ParserFactory {
189189
// Register Step3 parser (same format as DeepSeek-R1 but separate for debugging)
190190
registry.register_parser("step3", || Box::new(Step3Parser::new()));
191191

192+
// Register MiniMax parser (appends <think> token at the beginning)
193+
registry.register_parser("minimax", || Box::new(MiniMaxParser::new()));
194+
192195
// Register model patterns
193196
registry.register_pattern("deepseek-r1", "deepseek_r1");
194197
registry.register_pattern("qwen3-thinking", "qwen3_thinking");
@@ -198,6 +201,9 @@ impl ParserFactory {
198201
registry.register_pattern("glm45", "glm45");
199202
registry.register_pattern("kimi", "kimi");
200203
registry.register_pattern("step3", "step3");
204+
registry.register_pattern("minimax", "minimax");
205+
registry.register_pattern("minimax-m2", "minimax");
206+
registry.register_pattern("mm-m2", "minimax");
201207

202208
Self { registry }
203209
}
@@ -330,6 +336,17 @@ mod tests {
330336
assert_eq!(glm45.model_type(), "glm45");
331337
}
332338

339+
#[test]
340+
fn test_minimax_model() {
341+
let factory = ParserFactory::new();
342+
let minimax = factory.create("minimax-m2").unwrap();
343+
assert_eq!(minimax.model_type(), "minimax");
344+
345+
// Also test alternate patterns
346+
let mm = factory.create("mm-m2-chat").unwrap();
347+
assert_eq!(mm.model_type(), "minimax");
348+
}
349+
333350
#[tokio::test]
334351
async fn test_pooled_parser_reuse() {
335352
let factory = ParserFactory::new();

sgl-router/src/reasoning_parser/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub mod traits;
44

55
pub use factory::{ParserFactory, ParserRegistry, PooledParser};
66
pub use parsers::{
7-
BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, Qwen3Parser,
7+
BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, MiniMaxParser, Qwen3Parser,
88
QwenThinkingParser, Step3Parser,
99
};
1010
pub use traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// MiniMax M2 specific reasoning parser.
2+
// This parser automatically appends <think> token at the beginning of text,
3+
// similar to the Python MiniMaxAppendThinkDetector.
4+
5+
use crate::reasoning_parser::{
6+
parsers::BaseReasoningParser,
7+
traits::{ParseError, ParserConfig, ParserResult, ReasoningParser},
8+
};
9+
10+
/// MiniMax M2 reasoning parser.
11+
///
12+
/// This parser automatically appends <think> token at the beginning of the first chunk
13+
/// and uses <think> and </think> tokens for reasoning blocks.
14+
pub struct MiniMaxParser {
15+
base: BaseReasoningParser,
16+
is_first_chunk: bool,
17+
}
18+
19+
impl MiniMaxParser {
20+
/// Create a new MiniMax M2 parser.
21+
pub fn new() -> Self {
22+
let config = ParserConfig {
23+
think_start_token: "<think>".to_string(),
24+
think_end_token: "</think>".to_string(),
25+
stream_reasoning: true,
26+
max_buffer_size: 65536,
27+
initial_in_reasoning: false, // Start with false, we'll add <think> manually
28+
};
29+
30+
Self {
31+
base: BaseReasoningParser::new(config).with_model_type("minimax".to_string()),
32+
is_first_chunk: true,
33+
}
34+
}
35+
}
36+
37+
impl Default for MiniMaxParser {
38+
fn default() -> Self {
39+
Self::new()
40+
}
41+
}
42+
43+
impl ReasoningParser for MiniMaxParser {
44+
fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
45+
// For one-shot parsing, prepend <think> token to the text
46+
let modified_text = format!("<think>{}", text);
47+
self.base.detect_and_parse_reasoning(&modified_text)
48+
}
49+
50+
fn parse_reasoning_streaming_incremental(
51+
&mut self,
52+
text: &str,
53+
) -> Result<ParserResult, ParseError> {
54+
// For the first chunk, prepend <think> token
55+
let modified_text = if self.is_first_chunk {
56+
self.is_first_chunk = false;
57+
format!("<think>{}", text)
58+
} else {
59+
text.to_string()
60+
};
61+
62+
self.base
63+
.parse_reasoning_streaming_incremental(&modified_text)
64+
}
65+
66+
fn reset(&mut self) {
67+
self.base.reset();
68+
self.is_first_chunk = true; // Reset the first chunk flag
69+
}
70+
71+
fn model_type(&self) -> &str {
72+
self.base.model_type()
73+
}
74+
75+
fn is_in_reasoning(&self) -> bool {
76+
self.base.is_in_reasoning()
77+
}
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use super::*;
83+
84+
#[test]
85+
fn test_minimax_append_think_oneshot() {
86+
let mut parser = MiniMaxParser::new();
87+
88+
// Should automatically prepend <think> and parse as reasoning
89+
let result = parser
90+
.detect_and_parse_reasoning("reasoning content</think>normal content")
91+
.unwrap();
92+
assert_eq!(result.normal_text, "normal content");
93+
assert_eq!(result.reasoning_text, "reasoning content");
94+
}
95+
96+
#[test]
97+
fn test_minimax_without_end_token() {
98+
let mut parser = MiniMaxParser::new();
99+
100+
// Should treat all content as reasoning when no end token
101+
let result = parser
102+
.detect_and_parse_reasoning("all reasoning content")
103+
.unwrap();
104+
assert_eq!(result.normal_text, "");
105+
assert_eq!(result.reasoning_text, "all reasoning content");
106+
}
107+
108+
#[test]
109+
fn test_minimax_streaming_first_chunk() {
110+
let mut parser = MiniMaxParser::new();
111+
112+
// First chunk should have <think> prepended
113+
let result1 = parser
114+
.parse_reasoning_streaming_incremental("thinking about")
115+
.unwrap();
116+
assert_eq!(result1.reasoning_text, "thinking about");
117+
assert_eq!(result1.normal_text, "");
118+
119+
// Second chunk should not have <think> prepended
120+
let result2 = parser
121+
.parse_reasoning_streaming_incremental(" the problem</think>answer")
122+
.unwrap();
123+
assert_eq!(result2.reasoning_text, "the problem"); // Text is trimmed
124+
assert_eq!(result2.normal_text, "answer");
125+
}
126+
127+
#[test]
128+
fn test_minimax_reset() {
129+
let mut parser = MiniMaxParser::new();
130+
131+
// First use
132+
let result1 = parser
133+
.parse_reasoning_streaming_incremental("first")
134+
.unwrap();
135+
assert_eq!(result1.reasoning_text, "first");
136+
137+
// Reset the parser
138+
parser.reset();
139+
140+
// After reset, should be first chunk again
141+
let result2 = parser
142+
.parse_reasoning_streaming_incremental("second")
143+
.unwrap();
144+
assert_eq!(result2.reasoning_text, "second");
145+
}
146+
147+
#[test]
148+
fn test_minimax_already_has_think() {
149+
let mut parser = MiniMaxParser::new();
150+
151+
// Even if text already has <think>, it will add another one
152+
// This mimics the Python behavior
153+
let result = parser
154+
.detect_and_parse_reasoning("<think>content</think>answer")
155+
.unwrap();
156+
// The double <think> gets handled by the base parser which removes duplicates
157+
assert_eq!(result.normal_text, "answer");
158+
assert_eq!(result.reasoning_text, "content");
159+
}
160+
161+
#[test]
162+
fn test_model_type() {
163+
let parser = MiniMaxParser::new();
164+
assert_eq!(parser.model_type(), "minimax");
165+
}
166+
}

sgl-router/src/reasoning_parser/parsers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ pub mod base;
22
pub mod deepseek_r1;
33
pub mod glm45;
44
pub mod kimi;
5+
pub mod minimax;
56
pub mod qwen3;
67
pub mod step3;
78

89
pub use base::BaseReasoningParser;
910
pub use deepseek_r1::DeepSeekR1Parser;
1011
pub use glm45::Glm45Parser;
1112
pub use kimi::KimiParser;
13+
pub use minimax::MiniMaxParser;
1214
pub use qwen3::{Qwen3Parser, QwenThinkingParser};
1315
pub use step3::Step3Parser;

0 commit comments

Comments
 (0)