Skip to main content

reasoning_parser/parsers/
nano_v3.rs

1// NanoV3 / Nemotron reasoning parser.
2//
3// The Nemotron chat template supports an `enable_thinking` toggle that injects
4// `<think>\n` in the prefill when ON and `<think></think>` when OFF.
5// See: https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-FP8?chat_template=default
6//
7// Uses `always_in_reasoning=false` because the thinking toggle is detected at
8// runtime via `ThinkingToggle::DefaultOn` + `think_in_prefill=true`, and
9// `mark_reasoning_started()` is called when thinking is effectively ON.
10
11use crate::{
12    parsers::BaseReasoningParser,
13    traits::{ParseError, ParserConfig, ParserResult, ReasoningParser, DEFAULT_MAX_BUFFER_SIZE},
14};
15
16/// NanoV3 / Nemotron reasoning parser.
17pub struct NanoV3Parser {
18    base: BaseReasoningParser,
19}
20
21impl NanoV3Parser {
22    /// Create a new NanoV3 parser.
23    pub fn new() -> Self {
24        let config = ParserConfig {
25            think_start_token: "<think>".to_string(),
26            think_end_token: "</think>".to_string(),
27            stream_reasoning: true,
28            max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
29            always_in_reasoning: false,
30        };
31
32        Self {
33            base: BaseReasoningParser::new(config).with_model_type("nano_v3".to_string()),
34        }
35    }
36}
37
38impl Default for NanoV3Parser {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl ReasoningParser for NanoV3Parser {
45    fn detect_and_parse_reasoning(&mut self, text: &str) -> Result<ParserResult, ParseError> {
46        self.base.detect_and_parse_reasoning(text)
47    }
48
49    fn parse_reasoning_streaming_incremental(
50        &mut self,
51        text: &str,
52    ) -> Result<ParserResult, ParseError> {
53        self.base.parse_reasoning_streaming_incremental(text)
54    }
55
56    fn reset(&mut self) {
57        self.base.reset();
58    }
59
60    fn model_type(&self) -> &str {
61        self.base.model_type()
62    }
63
64    fn is_in_reasoning(&self) -> bool {
65        self.base.is_in_reasoning()
66    }
67
68    fn mark_reasoning_started(&mut self) {
69        self.base.mark_reasoning_started();
70    }
71
72    fn mark_think_start_stripped(&mut self) {
73        self.base.mark_think_start_stripped();
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_nano_v3_initial_state() {
83        let mut parser = NanoV3Parser::new();
84
85        // Without mark_reasoning_started(), text without <think> is normal content
86        let result = parser
87            .detect_and_parse_reasoning("This is normal content")
88            .unwrap();
89        assert_eq!(result.normal_text, "This is normal content");
90        assert_eq!(result.reasoning_text, "");
91    }
92
93    #[test]
94    fn test_nano_v3_with_mark_reasoning_started() {
95        let mut parser = NanoV3Parser::new();
96        parser.mark_reasoning_started();
97
98        // After mark_reasoning_started(), text is treated as reasoning
99        let result = parser
100            .detect_and_parse_reasoning("reasoning content</think>answer")
101            .unwrap();
102        assert_eq!(result.normal_text, "answer");
103        assert_eq!(result.reasoning_text, "reasoning content");
104    }
105
106    #[test]
107    fn test_nano_v3_with_both_tokens() {
108        let mut parser = NanoV3Parser::new();
109
110        let result = parser
111            .detect_and_parse_reasoning("<think>reasoning content</think>answer")
112            .unwrap();
113        assert_eq!(result.normal_text, "answer");
114        assert_eq!(result.reasoning_text, "reasoning content");
115    }
116
117    #[test]
118    fn test_nano_v3_streaming() {
119        let mut parser = NanoV3Parser::new();
120        parser.mark_reasoning_started();
121
122        let result1 = parser
123            .parse_reasoning_streaming_incremental("reasoning text ")
124            .unwrap();
125        assert_eq!(result1.normal_text, "");
126        assert_eq!(result1.reasoning_text, "reasoning text ");
127
128        let result2 = parser
129            .parse_reasoning_streaming_incremental("more reasoning</think>answer")
130            .unwrap();
131        assert_eq!(result2.normal_text, "answer");
132        assert_eq!(result2.reasoning_text, "more reasoning");
133    }
134
135    #[test]
136    fn test_model_type() {
137        let parser = NanoV3Parser::new();
138        assert_eq!(parser.model_type(), "nano_v3");
139    }
140}