Skip to main content

reasoning_parser/parsers/
minimax.rs

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
5use crate::{
6    parsers::BaseReasoningParser,
7    traits::{ParseError, ParserConfig, ParserResult, ReasoningParser, DEFAULT_MAX_BUFFER_SIZE},
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.
14pub struct MiniMaxParser {
15    base: BaseReasoningParser,
16    is_first_chunk: bool,
17}
18
19impl 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: DEFAULT_MAX_BUFFER_SIZE,
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
37impl Default for MiniMaxParser {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl 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)]
81mod 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}