Skip to main content

reasoning_parser/parsers/
deepseek_r1.rs

1// DeepSeek-R1 specific reasoning parser.
2// This parser starts with in_reasoning=true, assuming all text is reasoning
3// until an end token is encountered.
4
5use crate::{
6    parsers::BaseReasoningParser,
7    traits::{ParseError, ParserConfig, ParserResult, ReasoningParser, DEFAULT_MAX_BUFFER_SIZE},
8};
9
10/// DeepSeek-R1 reasoning parser.
11///
12/// This parser assumes reasoning from the start of text (in_reasoning=true)
13/// and uses <think> and </think> tokens.
14pub struct DeepSeekR1Parser {
15    base: BaseReasoningParser,
16}
17
18impl DeepSeekR1Parser {
19    /// Create a new DeepSeek-R1 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: true,
27        };
28
29        Self {
30            base: BaseReasoningParser::new(config).with_model_type("deepseek_r1".to_string()),
31        }
32    }
33}
34
35impl Default for DeepSeekR1Parser {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl ReasoningParser for DeepSeekR1Parser {
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#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_deepseek_r1_initial_state() {
80        let mut parser = DeepSeekR1Parser::new();
81
82        // Should treat text as reasoning even without start token
83        let result = parser
84            .detect_and_parse_reasoning("This is reasoning content")
85            .unwrap();
86        assert_eq!(result.normal_text, "");
87        assert_eq!(result.reasoning_text, "This is reasoning content");
88    }
89
90    #[test]
91    fn test_deepseek_r1_with_end_token() {
92        let mut parser = DeepSeekR1Parser::new();
93
94        // Should extract reasoning until end token
95        let result = parser
96            .detect_and_parse_reasoning("reasoning content</think>normal content")
97            .unwrap();
98        assert_eq!(result.normal_text, "normal content");
99        assert_eq!(result.reasoning_text, "reasoning content");
100    }
101
102    #[test]
103    fn test_deepseek_r1_streaming() {
104        let mut parser = DeepSeekR1Parser::new();
105
106        // First chunk - all reasoning
107        let result1 = parser
108            .parse_reasoning_streaming_incremental("thinking about")
109            .unwrap();
110        assert_eq!(result1.reasoning_text, "thinking about");
111        assert_eq!(result1.normal_text, "");
112
113        // Second chunk - ends reasoning
114        let result2 = parser
115            .parse_reasoning_streaming_incremental(" the problem</think>answer")
116            .unwrap();
117        assert_eq!(result2.reasoning_text, "the problem"); // Text is trimmed
118        assert_eq!(result2.normal_text, "answer");
119    }
120
121    #[test]
122    fn test_model_type() {
123        let parser = DeepSeekR1Parser::new();
124        assert_eq!(parser.model_type(), "deepseek_r1");
125    }
126}