Skip to main content

reasoning_parser/parsers/
qwen3.rs

1// Qwen3 specific reasoning parser.
2// This parser starts with in_reasoning=false, requiring an explicit
3// start token to enter reasoning mode.
4
5use crate::{
6    parsers::BaseReasoningParser,
7    traits::{ParseError, ParserConfig, ParserResult, ReasoningParser, DEFAULT_MAX_BUFFER_SIZE},
8};
9
10/// Qwen3 reasoning parser.
11///
12/// This parser requires explicit <think> tokens to enter reasoning mode
13/// (in_reasoning=false initially).
14pub struct Qwen3Parser {
15    base: BaseReasoningParser,
16}
17
18impl Qwen3Parser {
19    /// Create a new Qwen3 parser.
20    pub fn new() -> Self {
21        let config = ParserConfig {
22            think_start_token: "<think>".to_string(),
23            think_end_token: "</think>".to_string(),
24            stream_reasoning: true,
25            max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
26            always_in_reasoning: false,
27        };
28
29        Self {
30            base: BaseReasoningParser::new(config).with_model_type("qwen3".to_string()),
31        }
32    }
33}
34
35impl Default for Qwen3Parser {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl ReasoningParser for Qwen3Parser {
42    fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
43        self.base.detect_and_parse_reasoning(text)
44    }
45
46    fn parse_reasoning_streaming_incremental(
47        &mut self,
48        text: &str,
49    ) -> Result<ParserResult, ParseError> {
50        self.base.parse_reasoning_streaming_incremental(text)
51    }
52
53    fn reset(&mut self) {
54        self.base.reset();
55    }
56
57    fn model_type(&self) -> &str {
58        self.base.model_type()
59    }
60
61    fn is_in_reasoning(&self) -> bool {
62        self.base.is_in_reasoning()
63    }
64
65    fn mark_reasoning_started(&mut self) {
66        self.base.mark_reasoning_started();
67    }
68
69    fn mark_think_start_stripped(&mut self) {
70        self.base.mark_think_start_stripped();
71    }
72}
73
74/// QwenThinking parser - variant that assumes reasoning from start.
75///
76/// This is for qwen*thinking models that behave like DeepSeek-R1.
77pub struct QwenThinkingParser {
78    base: BaseReasoningParser,
79}
80
81impl QwenThinkingParser {
82    /// Create a new QwenThinking parser.
83    pub fn new() -> Self {
84        let config = ParserConfig {
85            think_start_token: "<think>".to_string(),
86            think_end_token: "</think>".to_string(),
87            stream_reasoning: true,
88            max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
89            always_in_reasoning: true,
90        };
91
92        Self {
93            base: BaseReasoningParser::new(config).with_model_type("qwen_thinking".to_string()),
94        }
95    }
96}
97
98impl Default for QwenThinkingParser {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl ReasoningParser for QwenThinkingParser {
105    fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
106        self.base.detect_and_parse_reasoning(text)
107    }
108
109    fn parse_reasoning_streaming_incremental(
110        &mut self,
111        text: &str,
112    ) -> Result<ParserResult, ParseError> {
113        self.base.parse_reasoning_streaming_incremental(text)
114    }
115
116    fn reset(&mut self) {
117        self.base.reset();
118    }
119
120    fn model_type(&self) -> &str {
121        self.base.model_type()
122    }
123
124    fn is_in_reasoning(&self) -> bool {
125        self.base.is_in_reasoning()
126    }
127
128    fn mark_reasoning_started(&mut self) {
129        self.base.mark_reasoning_started();
130    }
131
132    fn mark_think_start_stripped(&mut self) {
133        self.base.mark_think_start_stripped();
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_qwen3_initial_state() {
143        let mut parser = Qwen3Parser::new();
144
145        // Should NOT treat text as reasoning without start token
146        let result = parser
147            .detect_and_parse_reasoning("This is normal content")
148            .unwrap();
149        assert_eq!(result.normal_text, "This is normal content");
150        assert_eq!(result.reasoning_text, "");
151    }
152
153    #[test]
154    fn test_qwen3_with_tokens() {
155        let mut parser = Qwen3Parser::new();
156
157        // Should extract reasoning with proper tokens
158        let result = parser
159            .detect_and_parse_reasoning("<think>reasoning</think>answer")
160            .unwrap();
161        assert_eq!(result.normal_text, "answer");
162        assert_eq!(result.reasoning_text, "reasoning");
163    }
164
165    #[test]
166    fn test_qwen_thinking_initial_state() {
167        let mut parser = QwenThinkingParser::new();
168
169        // Should treat text as reasoning even without start token
170        let result = parser
171            .detect_and_parse_reasoning("This is reasoning content")
172            .unwrap();
173        assert_eq!(result.normal_text, "");
174        assert_eq!(result.reasoning_text, "This is reasoning content");
175    }
176
177    #[test]
178    fn test_qwen3_streaming() {
179        let mut parser = Qwen3Parser::new();
180
181        // First chunk - normal text (no start token yet)
182        let result1 = parser
183            .parse_reasoning_streaming_incremental("normal text ")
184            .unwrap();
185        assert_eq!(result1.normal_text, "normal text ");
186        assert_eq!(result1.reasoning_text, "");
187
188        // Second chunk - enters reasoning
189        let result2 = parser
190            .parse_reasoning_streaming_incremental("<think>reasoning")
191            .unwrap();
192        assert_eq!(result2.normal_text, "");
193        assert_eq!(result2.reasoning_text, "reasoning");
194    }
195
196    #[test]
197    fn test_model_types() {
198        let qwen3 = Qwen3Parser::new();
199        assert_eq!(qwen3.model_type(), "qwen3");
200
201        let qwen_thinking = QwenThinkingParser::new();
202        assert_eq!(qwen_thinking.model_type(), "qwen_thinking");
203    }
204}