[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(&[