[Rust Frontend] Simplify reasoning parser initialization and test organization (#55411)

Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>

Bugen Zhao committed Sep 9, 2026 at 12:57 UTC 050f29caf1c7dc4648cfa454b987da62b837660d
10 files changed +167 -172
rust/src/parser/src/reasoning/cohere_cmd.rs
-1
@@ -19,7 +19,6 @@ impl CohereCmdReasoningParser {
19 tokenizer,
20 "<|START_THINKING|>",
21 "<|END_THINKING|>",
22 - false,
22 )?,
23 })
24 }
rust/src/parser/src/reasoning/deepseek_r1.rs
+19 -5
@@ -6,10 +6,6 @@ use vllm_tokenizer::{DecodedText, DynTokenizer};
6 use super::{DelimitedReasoningParser, ReasoningDelta, ReasoningParser, Result};
7
8 /// Reasoning parser for DeepSeek R1 style outputs.
9 -///
10 -/// DeepSeek R1 may begin generating directly inside a reasoning span and only
11 -/// emit the closing `</think>` delimiter, so the no-boundary fallback defaults
12 -/// to `in_reasoning = true`.
9 pub struct DeepSeekR1ReasoningParser {
10 inner: DelimitedReasoningParser,
11 }
@@ -19,7 +15,7 @@ impl DeepSeekR1ReasoningParser {
15 /// machine.
16 pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
17 Ok(Self {
22 - inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", true)?,
18 + inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>")?,
19 })
20 }
21 }
@@ -45,3 +41,21 @@ impl ReasoningParser for DeepSeekR1ReasoningParser {
41 Ok(self.inner.finish())
42 }
43 }
44 +
45 +#[cfg(test)]
46 +mod tests {
47 + use std::sync::Arc;
48 +
49 + use super::DeepSeekR1ReasoningParser;
50 + use crate::reasoning::tests::{content_str, fake_tokenizer, push_str};
51 +
52 + #[test]
53 + fn deepseek_r1_without_prompt_markers_expects_start_token() {
54 + let tokenizer = Arc::new(fake_tokenizer());
55 + let mut parser = DeepSeekR1ReasoningParser::new(tokenizer).unwrap();
56 +
57 + let delta = push_str(&mut parser, "reason</think>answer");
58 + assert_eq!(delta.reasoning, None);
59 + assert_eq!(content_str(&delta), Some("reason</think>answer"));
60 + }
61 +}
rust/src/parser/src/reasoning/delimited.rs
+2 -9
@@ -24,20 +24,14 @@ pub(crate) struct DelimitedReasoningParser {
24 end_token: String,
25 start_token_id: u32,
26 end_token_id: u32,
27 - default_in_reasoning: bool,
27 }
28
29 impl DelimitedReasoningParser {
30 /// Create one delimited parser state machine.
32 - ///
33 - /// `default_in_reasoning` is only used when prompt initialization sees no
34 - /// reasoning boundary token at all. If the prompt contains either the
35 - /// start or end delimiter, that prompt boundary always wins.
31 pub(crate) fn new(
32 tokenizer: DynTokenizer,
33 start_token: impl Into<String>,
34 end_token: impl Into<String>,
40 - default_in_reasoning: bool,
35 ) -> Result<Self> {
36 let start_token = start_token.into();
37 let end_token = end_token.into();
@@ -54,13 +48,12 @@ impl DelimitedReasoningParser {
48
49 Ok(Self {
50 tokenizer,
57 - current_in_reasoning: default_in_reasoning,
51 + current_in_reasoning: false,
52 buffer: DecodedText::default(),
53 start_token,
54 end_token,
55 start_token_id,
56 end_token_id,
63 - default_in_reasoning,
57 })
58 }
59
@@ -72,7 +65,7 @@ impl DelimitedReasoningParser {
65 self.end_token_id,
66 self.tokenizer.as_ref(),
67 )
75 - .unwrap_or(self.default_in_reasoning);
68 + .unwrap_or(false);
69 }
70
71 /// Return whether the parser is currently inside a reasoning section.
rust/src/parser/src/reasoning/hy.rs
-1
@@ -18,7 +18,6 @@ impl HyReasoningParser {
18 tokenizer,
19 format!("<think{suffix}>"),
20 format!("</think{suffix}>"),
21 - false,
21 )?,
22 })
23 }
rust/src/parser/src/reasoning/kimi.rs
+1 -1
@@ -14,7 +14,7 @@ impl KimiReasoningParser {
14 /// Create a Kimi parser backed by the shared delimited state machine.
15 pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
16 Ok(Self {
17 - inner: DelimitedReasoningParser::new(tokenizer, "◁think▷", "◁/think▷", false)?,
17 + inner: DelimitedReasoningParser::new(tokenizer, "◁think▷", "◁/think▷")?,
18 })
19 }
20 }
rust/src/parser/src/reasoning/minimax_m3.rs
+75 -1
@@ -27,7 +27,7 @@ impl MiniMaxM3ReasoningParser {
27 /// Create a MiniMax M3 parser backed by the shared delimited state machine.
28 pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
29 Ok(Self {
30 - inner: DelimitedReasoningParser::new(tokenizer, M3_THINK_START, M3_THINK_END, false)?,
30 + inner: DelimitedReasoningParser::new(tokenizer, M3_THINK_START, M3_THINK_END)?,
31 at_response_start: true,
32 leading_end_buffer: DecodedText::default(),
33 })
@@ -98,3 +98,77 @@ impl ReasoningParser for MiniMaxM3ReasoningParser {
98 Ok(delta)
99 }
100 }
101 +
102 +#[cfg(test)]
103 +mod tests {
104 + use std::sync::Arc;
105 +
106 + use super::MiniMaxM3ReasoningParser;
107 + use crate::reasoning::ReasoningParser;
108 + use crate::reasoning::tests::{
109 + MM_THINK_END_ID, MM_THINK_START_ID, content_str, fake_tokenizer, push_str, reasoning_str,
110 + };
111 +
112 + #[test]
113 + fn minimax_m3_handles_explicit_think_delimiters() {
114 + let tokenizer = Arc::new(fake_tokenizer());
115 + let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
116 +
117 + let delta = push_str(&mut parser, "<mm:think>reason</mm:think>answer");
118 + assert_eq!(reasoning_str(&delta), Some("reason"));
119 + assert_eq!(content_str(&delta), Some("answer"));
120 + }
121 +
122 + #[test]
123 + fn minimax_m3_drops_leading_end_marker() {
124 + let tokenizer = Arc::new(fake_tokenizer());
125 + let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
126 +
127 + let delta = push_str(&mut parser, "</mm:think>answer");
128 + assert_eq!(delta.reasoning, None);
129 + assert_eq!(content_str(&delta), Some("answer"));
130 + }
131 +
132 + #[test]
133 + fn minimax_m3_preserves_non_leading_end_marker() {
134 + let tokenizer = Arc::new(fake_tokenizer());
135 + let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
136 +
137 + let delta = push_str(&mut parser, "XXX</mm:think>YYY");
138 + assert_eq!(delta.reasoning, None);
139 + assert_eq!(content_str(&delta), Some("XXX</mm:think>YYY"));
140 + }
141 +
142 + #[test]
143 + fn minimax_m3_drops_split_leading_end_marker() {
144 + let tokenizer = Arc::new(fake_tokenizer());
145 + let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
146 +
147 + assert!(push_str(&mut parser, "</mm").is_empty());
148 + let delta = push_str(&mut parser, ":think>answer");
149 + assert_eq!(delta.reasoning, None);
150 + assert_eq!(content_str(&delta), Some("answer"));
151 + }
152 +
153 + #[test]
154 + fn minimax_m3_uses_prompt_prefilled_start_marker() {
155 + let tokenizer = Arc::new(fake_tokenizer());
156 + let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
157 + parser.initialize(&[MM_THINK_START_ID]).unwrap();
158 +
159 + let delta = push_str(&mut parser, "reason</mm:think>answer");
160 + assert_eq!(reasoning_str(&delta), Some("reason"));
161 + assert_eq!(content_str(&delta), Some("answer"));
162 + }
163 +
164 + #[test]
165 + fn minimax_m3_uses_prompt_prefilled_end_marker() {
166 + let tokenizer = Arc::new(fake_tokenizer());
167 + let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
168 + parser.initialize(&[MM_THINK_END_ID]).unwrap();
169 +
170 + let delta = push_str(&mut parser, "answer");
171 + assert_eq!(delta.reasoning, None);
172 + assert_eq!(content_str(&delta), Some("answer"));
173 + }
174 +}
rust/src/parser/src/reasoning/qwen3.rs
+62 -1
@@ -18,7 +18,7 @@ impl Qwen3ReasoningParser {
18 /// Create a Qwen3 parser backed by the shared delimited state machine.
19 pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
20 Ok(Self {
21 - inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false)?,
21 + inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>")?,
22 })
23 }
24 }
@@ -44,3 +44,64 @@ impl ReasoningParser for Qwen3ReasoningParser {
44 Ok(self.inner.finish())
45 }
46 }
47 +
48 +#[cfg(test)]
49 +mod tests {
50 + use std::sync::Arc;
51 +
52 + use super::Qwen3ReasoningParser;
53 + use crate::reasoning::ReasoningParser;
54 + use crate::reasoning::tests::{
55 + SPECIAL_BOUNDARY_ID, THINK_END_ID, THINK_START_ID, content_str, fake_tokenizer, push_str,
56 + reasoning_str,
57 + };
58 +
59 + #[test]
60 + fn qwen3_without_prompt_markers_expects_start_token() {
61 + let tokenizer = Arc::new(fake_tokenizer());
62 + let mut parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
63 +
64 + let delta = push_str(&mut parser, "reason</think>answer");
65 + assert_eq!(delta.reasoning, None);
66 + assert_eq!(content_str(&delta), Some("reason</think>answer"));
67 + }
68 +
69 + #[test]
70 + fn qwen3_prompt_end_marker_starts_in_content() {
71 + let tokenizer = Arc::new(fake_tokenizer());
72 + let mut parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
73 + parser.initialize(&[THINK_END_ID]).unwrap();
74 +
75 + let delta = push_str(&mut parser, "answer");
76 + assert_eq!(delta.reasoning, None);
77 + assert_eq!(content_str(&delta), Some("answer"));
78 + }
79 +
80 + #[test]
81 + fn qwen3_tolerates_old_and_new_formats() {
82 + let tokenizer = Arc::new(fake_tokenizer());
83 +
84 + let mut old_parser = Qwen3ReasoningParser::new(tokenizer.clone()).unwrap();
85 + let old = push_str(&mut old_parser, "<think>reason</think>answer");
86 + assert_eq!(reasoning_str(&old), Some("reason"));
87 + assert_eq!(content_str(&old), Some("answer"));
88 +
89 + let mut new_parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
90 + new_parser.initialize(&[THINK_START_ID]).unwrap();
91 + let new = push_str(&mut new_parser, "reason</think>answer");
92 + assert_eq!(reasoning_str(&new), Some("reason"));
93 + assert_eq!(content_str(&new), Some("answer"));
94 + }
95 +
96 + #[test]
97 + fn qwen3_stops_scanning_at_last_special_token() {
98 + let tokenizer = Arc::new(fake_tokenizer());
99 + let mut parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
100 +
101 + parser.initialize(&[THINK_START_ID, SPECIAL_BOUNDARY_ID]).unwrap();
102 +
103 + let delta = push_str(&mut parser, "answer");
104 + assert_eq!(delta.reasoning, None);
105 + assert_eq!(content_str(&delta), Some("answer"));
106 + }
107 +}
rust/src/parser/src/reasoning/seed_oss.rs
+1 -6
@@ -15,12 +15,7 @@ impl SeedOssReasoningParser {
15 /// Create a SeedOSS parser backed by the shared delimited state machine.
16 pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
17 Ok(Self {
18 - inner: DelimitedReasoningParser::new(
19 - tokenizer,
20 - "<seed:think>",
21 - "</seed:think>",
22 - false,
23 - )?,
18 + inner: DelimitedReasoningParser::new(tokenizer, "<seed:think>", "</seed:think>")?,
19 })
20 }
21 }
rust/src/parser/src/reasoning/step3p5.rs
+1 -1
@@ -26,7 +26,7 @@ impl Step3p5ReasoningParser {
26 /// Create a Step3p5 parser backed by the shared delimited state machine.
27 pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
28 Ok(Self {
29 - inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false)?,
29 + inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>")?,
30 pending_reasoning_newline: None,
31 just_ended_reasoning: false,
32 })
rust/src/parser/src/reasoning/tests.rs
+6 -146
@@ -57,8 +57,7 @@ pub(crate) fn content_str(delta: &ReasoningDelta) -> Option<&str> {
57 #[test]
58 fn delimited_content_only_stream() {
59 let tokenizer = Arc::new(fake_tokenizer());
60 - let mut parser =
61 - DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false).unwrap();
60 + let mut parser = DelimitedReasoningParser::new(tokenizer, "<think>", "</think>").unwrap();
61
62 let delta = parser.push(DecodedText::unattributed("plain content"));
63 assert_eq!(content_str(&delta), Some("plain content"));
@@ -67,8 +66,7 @@ fn delimited_content_only_stream() {
66 #[test]
67 fn delimited_single_chunk_with_reasoning_and_content() {
68 let tokenizer = Arc::new(fake_tokenizer());
70 - let mut parser =
71 - DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false).unwrap();
69 + let mut parser = DelimitedReasoningParser::new(tokenizer, "<think>", "</think>").unwrap();
70
71 let delta = parser.push(DecodedText::unattributed("<think>reason</think>answer"));
72 assert_eq!(reasoning_str(&delta), Some("reason"));
@@ -78,8 +76,7 @@ fn delimited_single_chunk_with_reasoning_and_content() {
76 #[test]
77 fn delimited_partial_tokens_across_chunks() {
78 let tokenizer = Arc::new(fake_tokenizer());
81 - let mut parser =
82 - DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false).unwrap();
79 + let mut parser = DelimitedReasoningParser::new(tokenizer, "<think>", "</think>").unwrap();
80
81 assert!(parser.push(DecodedText::unattributed("<thi")).is_empty());
82 let delta = parser.push(DecodedText::unattributed("nk>reason</think>answer"));
@@ -90,8 +87,7 @@ fn delimited_partial_tokens_across_chunks() {
87 #[test]
88 fn delimited_finish_flushes_buffer() {
89 let tokenizer = Arc::new(fake_tokenizer());
93 - let mut parser =
94 - DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false).unwrap();
90 + let mut parser = DelimitedReasoningParser::new(tokenizer, "<think>", "</think>").unwrap();
91 parser.initialize(&[THINK_START_ID]);
92
93 let delta = parser.push(DecodedText::unattributed("unfinished</thi"));
@@ -100,145 +96,10 @@ fn delimited_finish_flushes_buffer() {
96 assert_eq!(reasoning_str(&final_delta), Some("</thi"));
97 }
98
103 -#[test]
104 -fn qwen3_without_prompt_markers_expects_start_token() {
105 - let tokenizer = Arc::new(fake_tokenizer());
106 - let mut parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
107 -
108 - let delta = push_str(&mut parser, "reason</think>answer");
109 - assert_eq!(delta.reasoning, None);
110 - assert_eq!(content_str(&delta), Some("reason</think>answer"));
111 -}
112 -
113 -#[test]
114 -fn qwen3_prompt_end_marker_starts_in_content() {
115 - let tokenizer = Arc::new(fake_tokenizer());
116 - let mut parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
117 - parser.initialize(&[THINK_END_ID]).unwrap();
118 -
119 - let delta = push_str(&mut parser, "answer");
120 - assert_eq!(delta.reasoning, None);
121 - assert_eq!(content_str(&delta), Some("answer"));
122 -}
123 -
124 -#[test]
125 -fn qwen3_tolerates_old_and_new_formats() {
126 - let tokenizer = Arc::new(fake_tokenizer());
127 -
128 - let mut old_parser = Qwen3ReasoningParser::new(tokenizer.clone()).unwrap();
129 - let old = push_str(&mut old_parser, "<think>reason</think>answer");
130 - assert_eq!(reasoning_str(&old), Some("reason"));
131 - assert_eq!(content_str(&old), Some("answer"));
132 -
133 - let mut new_parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
134 - new_parser.initialize(&[THINK_START_ID]).unwrap();
135 - let new = push_str(&mut new_parser, "reason</think>answer");
136 - assert_eq!(reasoning_str(&new), Some("reason"));
137 - assert_eq!(content_str(&new), Some("answer"));
138 -}
139 -
140 -#[test]
141 -fn qwen3_stops_scanning_at_last_special_token() {
142 - let tokenizer = Arc::new(fake_tokenizer());
143 - let mut parser = Qwen3ReasoningParser::new(tokenizer).unwrap();
144 -
145 - parser.initialize(&[THINK_START_ID, SPECIAL_BOUNDARY_ID]).unwrap();
146 -
147 - let delta = push_str(&mut parser, "answer");
148 - assert_eq!(delta.reasoning, None);
149 - assert_eq!(content_str(&delta), Some("answer"));
150 -}
151 -
152 -#[test]
153 -fn deepseek_r1_defaults_to_reasoning_without_prompt_boundary() {
154 - let tokenizer = Arc::new(fake_tokenizer());
155 - let mut parser = DeepSeekR1ReasoningParser::new(tokenizer).unwrap();
156 -
157 - let delta = push_str(&mut parser, "reason</think>answer");
158 - assert_eq!(reasoning_str(&delta), Some("reason"));
159 - assert_eq!(content_str(&delta), Some("answer"));
160 -}
161 -
162 -#[test]
163 -fn deepseek_r1_stops_scanning_at_last_special_token() {
164 - let tokenizer = Arc::new(fake_tokenizer());
165 - let mut parser = DeepSeekR1ReasoningParser::new(tokenizer).unwrap();
166 -
167 - parser.initialize(&[THINK_END_ID, SPECIAL_BOUNDARY_ID]).unwrap();
168 -
169 - let delta = push_str(&mut parser, "reason</think>answer");
170 - assert_eq!(reasoning_str(&delta), Some("reason"));
171 - assert_eq!(content_str(&delta), Some("answer"));
172 -}
173 -
174 -#[test]
175 -fn minimax_m3_handles_explicit_think_delimiters() {
176 - let tokenizer = Arc::new(fake_tokenizer());
177 - let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
178 -
179 - let delta = push_str(&mut parser, "<mm:think>reason</mm:think>answer");
180 - assert_eq!(reasoning_str(&delta), Some("reason"));
181 - assert_eq!(content_str(&delta), Some("answer"));
182 -}
183 -
184 -#[test]
185 -fn minimax_m3_drops_leading_end_marker() {
186 - let tokenizer = Arc::new(fake_tokenizer());
187 - let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
188 -
189 - let delta = push_str(&mut parser, "</mm:think>answer");
190 - assert_eq!(delta.reasoning, None);
191 - assert_eq!(content_str(&delta), Some("answer"));
192 -}
193 -
194 -#[test]
195 -fn minimax_m3_preserves_non_leading_end_marker() {
196 - let tokenizer = Arc::new(fake_tokenizer());
197 - let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
198 -
199 - let delta = push_str(&mut parser, "XXX</mm:think>YYY");
200 - assert_eq!(delta.reasoning, None);
201 - assert_eq!(content_str(&delta), Some("XXX</mm:think>YYY"));
202 -}
203 -
204 -#[test]
205 -fn minimax_m3_drops_split_leading_end_marker() {
206 - let tokenizer = Arc::new(fake_tokenizer());
207 - let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
208 -
209 - assert!(push_str(&mut parser, "</mm").is_empty());
210 - let delta = push_str(&mut parser, ":think>answer");
211 - assert_eq!(delta.reasoning, None);
212 - assert_eq!(content_str(&delta), Some("answer"));
213 -}
214 -
215 -#[test]
216 -fn minimax_m3_uses_prompt_prefilled_start_marker() {
217 - let tokenizer = Arc::new(fake_tokenizer());
218 - let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
219 - parser.initialize(&[MM_THINK_START_ID]).unwrap();
220 -
221 - let delta = push_str(&mut parser, "reason</mm:think>answer");
222 - assert_eq!(reasoning_str(&delta), Some("reason"));
223 - assert_eq!(content_str(&delta), Some("answer"));
224 -}
225 -
226 -#[test]
227 -fn minimax_m3_uses_prompt_prefilled_end_marker() {
228 - let tokenizer = Arc::new(fake_tokenizer());
229 - let mut parser = MiniMaxM3ReasoningParser::new(tokenizer).unwrap();
230 - parser.initialize(&[MM_THINK_END_ID]).unwrap();
231 -
232 - let delta = push_str(&mut parser, "answer");
233 - assert_eq!(delta.reasoning, None);
234 - assert_eq!(content_str(&delta), Some("answer"));
235 -}
236 -
99 #[test]
100 fn delimited_zero_width_only_piece_is_attributed_to_current_state() {
101 let tokenizer = Arc::new(fake_tokenizer());
240 - let mut parser =
241 - DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false).unwrap();
102 + let mut parser = DelimitedReasoningParser::new(tokenizer, "<think>", "</think>").unwrap();
103 parser.initialize(&[THINK_START_ID]);
104
105 // A filtered special token produces a zero-width attribution with no text;
@@ -269,8 +130,7 @@ fn delimited_zero_width_only_piece_is_attributed_to_current_state() {
130 #[test]
131 fn delimited_marker_tokens_are_dropped_from_attributions() {
132 let tokenizer = Arc::new(fake_tokenizer());
272 - let mut parser =
273 - DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false).unwrap();
133 + let mut parser = DelimitedReasoningParser::new(tokenizer, "<think>", "</think>").unwrap();
134
135 let mut collected = CollectedAttributions::default();
136 for chunk in attributed_chunks(&[