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},
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: 65536,
26            initial_in_reasoning: false, // Requires explicit start token
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
66/// QwenThinking parser - variant that assumes reasoning from start.
67///
68/// This is for qwen*thinking models that behave like DeepSeek-R1.
69pub struct QwenThinkingParser {
70    base: BaseReasoningParser,
71}
72
73impl QwenThinkingParser {
74    /// Create a new QwenThinking parser.
75    pub fn new() -> Self {
76        let config = ParserConfig {
77            think_start_token: "<think>".to_string(),
78            think_end_token: "</think>".to_string(),
79            stream_reasoning: true,
80            max_buffer_size: 65536,
81            initial_in_reasoning: true, // Assumes reasoning from start
82        };
83
84        Self {
85            base: BaseReasoningParser::new(config).with_model_type("qwen_thinking".to_string()),
86        }
87    }
88}
89
90impl Default for QwenThinkingParser {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl ReasoningParser for QwenThinkingParser {
97    fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
98        self.base.detect_and_parse_reasoning(text)
99    }
100
101    fn parse_reasoning_streaming_incremental(
102        &mut self,
103        text: &str,
104    ) -> Result<ParserResult, ParseError> {
105        self.base.parse_reasoning_streaming_incremental(text)
106    }
107
108    fn reset(&mut self) {
109        self.base.reset()
110    }
111
112    fn model_type(&self) -> &str {
113        self.base.model_type()
114    }
115
116    fn is_in_reasoning(&self) -> bool {
117        self.base.is_in_reasoning()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_qwen3_initial_state() {
127        let mut parser = Qwen3Parser::new();
128
129        // Should NOT treat text as reasoning without start token
130        let result = parser
131            .detect_and_parse_reasoning("This is normal content")
132            .unwrap();
133        assert_eq!(result.normal_text, "This is normal content");
134        assert_eq!(result.reasoning_text, "");
135    }
136
137    #[test]
138    fn test_qwen3_with_tokens() {
139        let mut parser = Qwen3Parser::new();
140
141        // Should extract reasoning with proper tokens
142        let result = parser
143            .detect_and_parse_reasoning("<think>reasoning</think>answer")
144            .unwrap();
145        assert_eq!(result.normal_text, "answer");
146        assert_eq!(result.reasoning_text, "reasoning");
147    }
148
149    #[test]
150    fn test_qwen_thinking_initial_state() {
151        let mut parser = QwenThinkingParser::new();
152
153        // Should treat text as reasoning even without start token
154        let result = parser
155            .detect_and_parse_reasoning("This is reasoning content")
156            .unwrap();
157        assert_eq!(result.normal_text, "");
158        assert_eq!(result.reasoning_text, "This is reasoning content");
159    }
160
161    #[test]
162    fn test_qwen3_streaming() {
163        let mut parser = Qwen3Parser::new();
164
165        // First chunk - normal text (no start token yet)
166        let result1 = parser
167            .parse_reasoning_streaming_incremental("normal text ")
168            .unwrap();
169        assert_eq!(result1.normal_text, "normal text ");
170        assert_eq!(result1.reasoning_text, "");
171
172        // Second chunk - enters reasoning
173        let result2 = parser
174            .parse_reasoning_streaming_incremental("<think>reasoning")
175            .unwrap();
176        assert_eq!(result2.normal_text, "");
177        assert_eq!(result2.reasoning_text, "reasoning");
178    }
179
180    #[test]
181    fn test_model_types() {
182        let qwen3 = Qwen3Parser::new();
183        assert_eq!(qwen3.model_type(), "qwen3");
184
185        let qwen_thinking = QwenThinkingParser::new();
186        assert_eq!(qwen_thinking.model_type(), "qwen_thinking");
187    }
188}