Skip to main content

reasoning_parser/parsers/
kimi.rs

1// Kimi specific reasoning parser.
2// This parser uses Unicode tokens and starts with in_reasoning=false.
3
4use crate::{
5    parsers::BaseReasoningParser,
6    traits::{ParseError, ParserConfig, ParserResult, ReasoningParser, DEFAULT_MAX_BUFFER_SIZE},
7};
8
9/// Kimi reasoning parser.
10///
11/// This parser uses Unicode tokens (◁think▷ and ◁/think▷) and requires
12/// explicit start tokens to enter reasoning mode.
13pub struct KimiParser {
14    base: BaseReasoningParser,
15}
16
17impl KimiParser {
18    /// Create a new Kimi parser.
19    pub fn new() -> Self {
20        let config = ParserConfig {
21            think_start_token: "◁think▷".to_string(),
22            think_end_token: "◁/think▷".to_string(),
23            stream_reasoning: true,
24            max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
25            initial_in_reasoning: false, // Requires explicit start token
26        };
27
28        Self {
29            base: BaseReasoningParser::new(config).with_model_type("kimi".to_string()),
30        }
31    }
32}
33
34impl Default for KimiParser {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl ReasoningParser for KimiParser {
41    fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
42        self.base.detect_and_parse_reasoning(text)
43    }
44
45    fn parse_reasoning_streaming_incremental(
46        &mut self,
47        text: &str,
48    ) -> Result<ParserResult, ParseError> {
49        self.base.parse_reasoning_streaming_incremental(text)
50    }
51
52    fn reset(&mut self) {
53        self.base.reset();
54    }
55
56    fn model_type(&self) -> &str {
57        self.base.model_type()
58    }
59
60    fn is_in_reasoning(&self) -> bool {
61        self.base.is_in_reasoning()
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_kimi_initial_state() {
71        let mut parser = KimiParser::new();
72
73        // Should NOT treat text as reasoning without start token
74        let result = parser
75            .detect_and_parse_reasoning("This is normal content")
76            .unwrap();
77        assert_eq!(result.normal_text, "This is normal content");
78        assert_eq!(result.reasoning_text, "");
79    }
80
81    #[test]
82    fn test_kimi_with_unicode_tokens() {
83        let mut parser = KimiParser::new();
84
85        // Should extract reasoning with Unicode tokens
86        let result = parser
87            .detect_and_parse_reasoning("◁think▷reasoning content◁/think▷answer")
88            .unwrap();
89        assert_eq!(result.normal_text, "answer");
90        assert_eq!(result.reasoning_text, "reasoning content");
91    }
92
93    #[test]
94    fn test_kimi_partial_unicode() {
95        let mut parser = KimiParser::new();
96
97        let result1 = parser
98            .parse_reasoning_streaming_incremental("◁thi")
99            .unwrap();
100        assert_eq!(result1.normal_text, "");
101        assert_eq!(result1.reasoning_text, "");
102
103        // Complete the token
104        let result2 = parser
105            .parse_reasoning_streaming_incremental("nk▷reasoning")
106            .unwrap();
107        assert_eq!(result2.normal_text, "");
108        assert_eq!(result2.reasoning_text, "reasoning");
109    }
110
111    #[test]
112    fn test_kimi_streaming() {
113        let mut parser = KimiParser::new();
114
115        // Normal text first
116        let result1 = parser
117            .parse_reasoning_streaming_incremental("normal ")
118            .unwrap();
119        assert_eq!(result1.normal_text, "normal ");
120        assert_eq!(result1.reasoning_text, "");
121
122        // Enter reasoning with Unicode token
123        let result2 = parser
124            .parse_reasoning_streaming_incremental("◁think▷thinking")
125            .unwrap();
126        assert_eq!(result2.normal_text, "");
127        assert_eq!(result2.reasoning_text, "thinking");
128
129        // Exit reasoning
130        let result3 = parser
131            .parse_reasoning_streaming_incremental("◁/think▷answer")
132            .unwrap();
133        assert_eq!(result3.normal_text, "answer");
134        assert_eq!(result3.reasoning_text, ""); // Already returned in stream mode
135    }
136
137    #[test]
138    fn test_model_type() {
139        let parser = KimiParser::new();
140        assert_eq!(parser.model_type(), "kimi");
141    }
142}