[Rust Frontend] Introduce unified parser interface & combined parser (#46583)
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Bugen Zhao committed
Jun 25, 2026 at 11:17 UTC
76c3c4ff63d6f8ffb27a4a6895f97bca2c60ec87
74 files changed
+1204
-1745
pyproject.toml
+2
-2
@@ -129,8 +129,8 @@ extend-exclude = ["tests/models/fixtures/*", "tests/prompts/*", "tests/tokenizer
129
"docs/governance/process.md", "docs/assets/contributing/vllm_bench_serve_timeline.html",
130
"tests/v1/engine/test_fast_incdec_prefix_err.py", ".git/*", "csrc/cpu/sgl-kernels/*",
131
"rust/src/chat/src/renderer/deepseek_v32/fixtures/*",
132
- "rust/src/tool-parser/src/gemma4.rs", "rust/src/text/src/output/decoded.rs",
133
- "rust/src/tokenizer/src/incremental.rs", "rust/src/reasoning-parser/src/tests.rs"]
132
+ "rust/src/parser/src/tool/gemma4.rs", "rust/src/text/src/output/decoded.rs",
133
+ "rust/src/tokenizer/src/incremental.rs", "rust/src/parser/src/reasoning/tests.rs"]
134
ignore-hidden = false
135
136
[tool.typos.default]
rust/Cargo.lock
+14
-22
@@ -5733,10 +5733,9 @@ dependencies = [
5733
"uuid",
5734
"vllm-engine-core-client",
5735
"vllm-llm",
5736
- "vllm-reasoning-parser",
5736
+ "vllm-parser",
5737
"vllm-text",
5738
"vllm-tokenizer",
5739
- "vllm-tool-parser",
5739
"xgrammar-structural-tag",
5740
"zeromq",
5741
]
@@ -5872,11 +5871,22 @@ dependencies = [
5871
]
5872
5873
[[package]]
5875
-name = "vllm-reasoning-parser"
5874
+name = "vllm-parser"
5875
version = "0.1.0"
5876
dependencies = [
5877
+ "criterion",
5878
+ "easy-ext",
5879
+ "expect-test",
5880
+ "futures",
5881
+ "openai-protocol",
5882
+ "serde",
5883
+ "serde_json",
5884
"thiserror 2.0.18",
5885
+ "thiserror-ext",
5886
+ "tool-parser",
5887
"vllm-tokenizer",
5888
+ "winnow",
5889
+ "xgrammar-structural-tag",
5890
]
5891
5892
[[package]]
@@ -5978,24 +5988,6 @@ dependencies = [
5988
"tracing",
5989
]
5990
5981
-[[package]]
5982
-name = "vllm-tool-parser"
5983
-version = "0.1.0"
5984
-dependencies = [
5985
- "criterion",
5986
- "easy-ext",
5987
- "expect-test",
5988
- "futures",
5989
- "openai-protocol",
5990
- "serde",
5991
- "serde_json",
5992
- "thiserror 2.0.18",
5993
- "thiserror-ext",
5994
- "tool-parser",
5995
- "winnow",
5996
- "xgrammar-structural-tag",
5997
-]
5998
-
5991
[[package]]
5992
name = "vllm-tool-parser-py"
5993
version = "0.1.0"
@@ -6004,7 +5996,7 @@ dependencies = [
5996
"pythonize",
5997
"serde_json",
5998
"thiserror-ext",
6007
- "vllm-tool-parser",
5999
+ "vllm-parser",
6000
]
6001
6002
[[package]]
rust/Cargo.toml
+3
-5
@@ -7,12 +7,11 @@ members = [
7
"src/managed-engine",
8
"src/metrics",
9
"src/mock-engine",
10
- "src/reasoning-parser",
10
+ "src/parser",
11
+ "src/parser/python",
12
"src/server",
13
"src/text",
14
"src/tokenizer",
14
- "src/tool-parser",
15
- "src/tool-parser/python",
15
]
16
resolver = "3"
17
@@ -117,11 +116,10 @@ vllm-engine-core-client = { path = "src/engine-core-client" }
116
vllm-llm = { path = "src/llm" }
117
vllm-managed-engine = { path = "src/managed-engine" }
118
vllm-metrics = { path = "src/metrics" }
120
-vllm-reasoning-parser = { path = "src/reasoning-parser" }
119
+vllm-parser = { path = "src/parser" }
120
vllm-server = { path = "src/server" }
121
vllm-text = { path = "src/text" }
122
vllm-tokenizer = { path = "src/tokenizer" }
124
-vllm-tool-parser = { path = "src/tool-parser" }
123
winnow = "1.0.2"
124
xgrammar-structural-tag = "0.1.0"
125
zeromq = { version = "0.6.0", default-features = false, features = [
rust/src/chat/Cargo.toml
+1
-2
@@ -30,10 +30,9 @@ trait-set.workspace = true
30
uuid.workspace = true
31
vllm-engine-core-client.workspace = true
32
vllm-llm.workspace = true
33
-vllm-reasoning-parser.workspace = true
33
+vllm-parser.workspace = true
34
vllm-text.workspace = true
35
vllm-tokenizer.workspace = true
36
-vllm-tool-parser.workspace = true
36
xgrammar-structural-tag.workspace = true
37
38
[dev-dependencies]
rust/src/chat/src/output/default/mod.rs
+22
-43
@@ -1,44 +1,34 @@
1
//! Default output processing pipeline.
2
3
-mod reasoning;
3
mod structural_tag;
5
-mod tool;
4
+mod unified;
5
6
use std::sync::Once;
7
9
-use futures::{Stream, StreamExt as _};
8
+use futures::StreamExt as _;
9
use tracing::info;
11
-use trait_set::trait_set;
10
+use vllm_parser::unified::{CombinedParser, UnifiedParser};
11
use vllm_text::tokenizer::DynTokenizer;
12
14
-use self::reasoning::reasoning_event_stream;
13
use self::structural_tag::apply_structural_tag_constraint;
16
-use self::tool::tool_event_stream;
14
+use self::unified::unified_event_stream;
15
use super::structured::structured_chat_event_stream;
16
use crate::error::Result;
19
-use crate::output::{
20
- AssistantEvent, ChatOutputProcessor, ContentEvent, DynChatEventStream,
21
- DynDecodedTextEventStream,
22
-};
17
+use crate::output::{ChatOutputProcessor, DynChatEventStream, DynDecodedTextEventStream};
18
use crate::parser::ParserSelection;
19
use crate::parser::reasoning::{ReasoningParser, ReasoningParserFactory};
20
use crate::parser::tool::{ToolParser, ToolParserFactory};
21
use crate::request::ChatRequest;
22
use crate::{Error, Result as ChatResult};
23
29
-trait_set! {
30
- trait ContentEventStream = Stream<Item = Result<ContentEvent>> + Send + 'static;
31
-}
32
-
24
/// Default request-scoped output processor used by Hugging Face style chat
25
/// backends.
26
///
27
/// This implementation assumes the backend already emitted decoded text deltas,
37
-/// then optionally layers reasoning parsing and tool-call parsing before
28
+/// then optionally layers unified reasoning and tool-call parsing before
29
/// assembling final structured chat events.
30
pub struct DefaultChatOutputProcessor {
40
- reasoning_parser: Option<Box<dyn ReasoningParser>>,
41
- tool_parser: Option<Box<dyn ToolParser>>,
31
+ parser: Box<dyn UnifiedParser>,
32
parallel_tool_calls: bool,
33
}
34
@@ -66,16 +56,17 @@ impl DefaultChatOutputProcessor {
56
} else {
57
None
58
};
69
- let reasoning_parser = Self::resolve_optional_reasoning_parser(
70
- request,
71
- model_id,
72
- tokenizer,
73
- reasoning_parser,
74
- )?;
59
+ let reasoning_parser =
60
+ Self::resolve_optional_reasoning_parser(model_id, tokenizer, reasoning_parser)?;
61
+ let parser: Box<dyn UnifiedParser> =
62
+ Box::new(CombinedParser::new(reasoning_parser, tool_parser));
63
+
64
+ if parser.preserve_special_tokens() {
65
+ request.decode_options.skip_special_tokens = false;
66
+ }
67
68
Ok(Self {
77
- reasoning_parser,
78
- tool_parser,
69
+ parser,
70
parallel_tool_calls: request.parallel_tool_calls,
71
})
72
}
@@ -87,8 +78,7 @@ impl DefaultChatOutputProcessor {
78
/// content is treated as opaque text.
79
pub fn plain_text_only() -> Self {
80
Self {
90
- reasoning_parser: None,
91
- tool_parser: None,
81
+ parser: Box::new(CombinedParser::plain_text_only()),
82
parallel_tool_calls: true,
83
}
84
}
@@ -112,10 +102,6 @@ impl DefaultChatOutputProcessor {
102
103
let parser = factory.create(parser_name, &request.tools)?;
104
115
- if parser.preserve_special_tokens() {
116
- request.decode_options.skip_special_tokens = false;
117
- }
118
-
105
apply_structural_tag_constraint(request, parser.as_ref())?;
106
107
TOOL_PARSER_LOG_ONCE.call_once(|| info!(parser_name, "using tool parser"));
@@ -123,7 +109,6 @@ impl DefaultChatOutputProcessor {
109
}
110
111
fn resolve_optional_reasoning_parser(
126
- request: &mut ChatRequest,
112
model_id: &str,
113
tokenizer: DynTokenizer,
114
selection: &ParserSelection,
@@ -142,10 +127,6 @@ impl DefaultChatOutputProcessor {
127
128
let parser = factory.create(parser_name, tokenizer)?;
129
145
- if parser.preserve_special_tokens() {
146
- request.decode_options.skip_special_tokens = false;
147
- }
148
-
130
REASONING_PARSER_LOG_ONCE.call_once(|| info!(parser_name, "using reasoning parser"));
131
Ok(Some(parser))
132
}
@@ -156,16 +137,14 @@ static REASONING_PARSER_LOG_ONCE: Once = Once::new();
137
138
impl ChatOutputProcessor for DefaultChatOutputProcessor {
139
/// Transforms a raw generate-output token stream into structured chat
159
- /// events through three sequential stages once text decoding has
140
+ /// events through two sequential stages once text decoding has
141
/// already happened:
142
///
162
- /// 1. [`reasoning_event_stream`] — reasoning/content separation
163
- /// 2. [`tool_event_stream`] — tool-call parsing
164
- /// 3. [`structured_chat_event_stream`] — final block assembly
143
+ /// 1. [`unified_event_stream`] — reasoning and tool-call parsing
144
+ /// 2. [`structured_chat_event_stream`] — final block assembly
145
fn process(self: Box<Self>, decoded: DynDecodedTextEventStream) -> Result<DynChatEventStream> {
166
- let reasoning = reasoning_event_stream(decoded, self.reasoning_parser);
167
- let tool = tool_event_stream(reasoning, self.tool_parser);
168
- let structured = structured_chat_event_stream(tool, self.parallel_tool_calls);
146
+ let parsed = unified_event_stream(decoded, self.parser);
147
+ let structured = structured_chat_event_stream(parsed, self.parallel_tool_calls);
148
149
Ok(structured.boxed())
150
}
rust/src/chat/src/output/default/reasoning.rs
deleted
-509
@@ -1,509 +0,0 @@
1
-//! Adapts decoded text updates into reasoning-aware assistant deltas.
2
-//!
3
-//! This stage sits between low-level token decoding and final block assembly.
4
-//! It is the only place in the new pipeline that understands reasoning
5
-//! separation: `decoded.rs` still only produces plain text deltas, while later
6
-//! stages consume the semantic `Text` / `Reasoning` split emitted here.
7
-
8
-use asynk_strim_attr::{TryYielder, try_stream};
9
-use futures::{StreamExt as _, pin_mut};
10
-use thiserror_ext::AsReport;
11
-use tracing::warn;
12
-use vllm_text::output::DecodedTextEvent;
13
-
14
-use super::ContentEvent;
15
-use crate::Result;
16
-use crate::error::Error;
17
-use crate::event::AssistantBlockKind;
18
-use crate::output::DecodedTextEventStream;
19
-use crate::parser::reasoning::{ReasoningDelta, ReasoningParser};
20
-
21
-/// Per-stream reasoning parsing state.
22
-struct ReasoningState {
23
- /// Reasoning parser for the current model family.
24
- parser: Box<dyn ReasoningParser>,
25
- /// Whether reasoning parsing has already failed for this stream.
26
- parser_failed: bool,
27
-}
28
-
29
-impl ReasoningState {
30
- /// Create one fresh reasoning-adaptation state for a new streamed response.
31
- fn new(parser: Box<dyn ReasoningParser>) -> Self {
32
- Self {
33
- parser,
34
- parser_failed: false,
35
- }
36
- }
37
-
38
- /// Convert one decoded text delta into zero or more semantic assistant
39
- /// deltas.
40
- fn process_delta(&mut self, delta: String) -> Vec<ContentEvent> {
41
- // If the parser has already failed, skip parsing and return plain text deltas.
42
- if self.parser_failed {
43
- return vec![ContentEvent::TextDelta {
44
- kind: AssistantBlockKind::Text,
45
- delta,
46
- }];
47
- }
48
-
49
- let mut events = Vec::new();
50
-
51
- match self.parser.push(&delta) {
52
- Ok(result) => {
53
- push_reasoning_delta(&mut events, result);
54
- }
55
- Err(error) => {
56
- if !self.parser_failed {
57
- warn!(
58
- error = %error.as_report(),
59
- "reasoning parser failed; falling back to plain text deltas"
60
- );
61
- self.parser_failed = true;
62
- }
63
- push_text_delta(&mut events, AssistantBlockKind::Text, delta);
64
- }
65
- }
66
-
67
- events
68
- }
69
-
70
- /// Initialize parser state once prompt token IDs are available.
71
- fn initialize(&mut self, prompt_token_ids: &[u32]) {
72
- if self.parser_failed {
73
- return;
74
- }
75
-
76
- match self.parser.initialize(prompt_token_ids) {
77
- Ok(()) => {}
78
- Err(error) => {
79
- warn!(
80
- error = %error.as_report(),
81
- "failed to initialize reasoning parser; falling back to plain text deltas"
82
- );
83
- self.parser_failed = true;
84
- }
85
- }
86
- }
87
-
88
- /// Flush any parser-held partial delimiter state at end of stream.
89
- fn finish(&mut self) -> Vec<ContentEvent> {
90
- if self.parser_failed {
91
- return Vec::new();
92
- }
93
-
94
- match self.parser.finish() {
95
- Ok(result) => {
96
- let mut events = Vec::new();
97
- push_reasoning_delta(&mut events, result);
98
- events
99
- }
100
- Err(error) => {
101
- warn!(error = %error.as_report(), "failed to flush reasoning parser state");
102
- Vec::new()
103
- }
104
- }
105
- }
106
-}
107
-
108
-/// Push one semantic text delta if it is non-empty.
109
-fn push_text_delta(events: &mut Vec<ContentEvent>, kind: AssistantBlockKind, delta: String) {
110
- if delta.is_empty() {
111
- return;
112
- }
113
- events.push(ContentEvent::TextDelta { kind, delta });
114
-}
115
-
116
-/// Convert one parsed reasoning delta into zero or more content events.
117
-fn push_reasoning_delta(events: &mut Vec<ContentEvent>, delta: ReasoningDelta) {
118
- if let Some(reasoning) = delta.reasoning {
119
- push_text_delta(events, AssistantBlockKind::Reasoning, reasoning);
120
- }
121
- if let Some(content) = delta.content {
122
- push_text_delta(events, AssistantBlockKind::Text, content);
123
- }
124
-}
125
-
126
-/// Wrap one decoded-text stream into the internal reasoning event stream.
127
-#[try_stream]
128
-pub(crate) async fn reasoning_event_stream(
129
- decoded_stream: impl DecodedTextEventStream,
130
- reasoning_parser: Option<Box<dyn ReasoningParser>>,
131
- mut y: TryYielder<ContentEvent, Error>,
132
-) -> Result<()> {
133
- pin_mut!(decoded_stream);
134
-
135
- // Without a parser, pass through as plain text deltas.
136
- let Some(reasoning_parser) = reasoning_parser else {
137
- while let Some(event) = decoded_stream.next().await.transpose()? {
138
- for next in ContentEvent::from_decoded_plain_text(event) {
139
- y.yield_ok(next).await;
140
- }
141
- }
142
- return Ok(());
143
- };
144
-
145
- let mut state = ReasoningState::new(reasoning_parser);
146
-
147
- while let Some(event) = decoded_stream.next().await.transpose()? {
148
- match event {
149
- DecodedTextEvent::Start {
150
- prompt_token_ids,
151
- prompt_logprobs,
152
- } => {
153
- state.initialize(&prompt_token_ids);
154
- y.yield_ok(ContentEvent::Start {
155
- prompt_token_ids,
156
- prompt_logprobs,
157
- })
158
- .await;
159
- }
160
- DecodedTextEvent::TextDelta {
161
- delta,
162
- token_ids,
163
- logprobs,
164
- finished,
165
- } => {
166
- for next in state.process_delta(delta) {
167
- y.yield_ok(next).await;
168
- }
169
- if logprobs.is_some() || !token_ids.is_empty() {
170
- y.yield_ok(ContentEvent::LogprobsDelta {
171
- logprobs,
172
- token_ids,
173
- })
174
- .await;
175
- }
176
- if let Some(finished) = finished {
177
- for next in state.finish() {
178
- y.yield_ok(next).await;
179
- }
180
- y.yield_ok(ContentEvent::Done {
181
- usage: finished.usage,
182
- finish_reason: finished.finish_reason,
183
- kv_transfer_params: finished.kv_transfer_params,
184
- })
185
- .await;
186
- }
187
- }
188
- }
189
- }
190
- Ok(())
191
-}
192
-
193
-#[cfg(test)]
194
-mod tests {
195
-
196
- use std::sync::Arc;
197
-
198
- use futures::{StreamExt as _, stream};
199
- use vllm_llm::FinishReason;
200
- use vllm_text::output::{
201
- DecodedLogprobs, DecodedPositionLogprobs, DecodedTextEvent, DecodedTokenLogprob,
202
- };
203
- use vllm_tokenizer::{DynTokenizer, Tokenizer};
204
-
205
- use super::super::ContentEvent;
206
- use super::reasoning_event_stream;
207
- use crate::event::AssistantBlockKind;
208
- use crate::parser::reasoning::{
209
- ReasoningDelta, ReasoningError, ReasoningParser, ReasoningParserFactory, names,
210
- };
211
-
212
- struct FakeTokenizer;
213
-
214
- impl Tokenizer for FakeTokenizer {
215
- fn encode(
216
- &self,
217
- text: &str,
218
- _add_special_tokens: bool,
219
- ) -> vllm_tokenizer::Result<Vec<u32>> {
220
- Ok(text.chars().map(u32::from).collect())
221
- }
222
-
223
- fn decode(
224
- &self,
225
- token_ids: &[u32],
226
- _skip_special_tokens: bool,
227
- ) -> vllm_tokenizer::Result<String> {
228
- Ok(token_ids
229
- .iter()
230
- .map(|token_id| char::from_u32(*token_id).unwrap_or('\u{FFFD}'))
231
- .collect())
232
- }
233
-
234
- fn token_to_id(&self, token: &str) -> Option<u32> {
235
- match token {
236
- "<think>" => Some(1),
237
- "</think>" => Some(2),
238
- _ => None,
239
- }
240
- }
241
- }
242
-
243
- struct FailingReasoningParser {
244
- fail_next: bool,
245
- }
246
-
247
- impl ReasoningParser for FailingReasoningParser {
248
- fn create(_tokenizer: DynTokenizer) -> Result<Box<dyn ReasoningParser>, ReasoningError>
249
- where
250
- Self: Sized + 'static,
251
- {
252
- Ok(Box::new(Self { fail_next: true }))
253
- }
254
-
255
- fn push(&mut self, _text: &str) -> Result<ReasoningDelta, ReasoningError> {
256
- if self.fail_next {
257
- self.fail_next = false;
258
- return Err(ReasoningError::MissingToken {
259
- token: "<think>".to_string(),
260
- });
261
- }
262
- Ok(ReasoningDelta::default())
263
- }
264
- }
265
-
266
- fn test_reasoning_parser(factory: &mut ReasoningParserFactory) -> Box<dyn ReasoningParser> {
267
- factory.register_parser::<FailingReasoningParser>("failing");
268
-
269
- factory.create("failing", Arc::new(FakeTokenizer)).unwrap()
270
- }
271
-
272
- #[tokio::test]
273
- async fn reasoning_parser_failure_falls_back_to_plain_text() {
274
- let mut factory = ReasoningParserFactory::new();
275
- let events = stream::iter(vec![
276
- Ok(DecodedTextEvent::Start {
277
- prompt_token_ids: vec![1, 2, 3].into(),
278
- prompt_logprobs: None,
279
- }),
280
- Ok(DecodedTextEvent::TextDelta {
281
- delta: "abc".to_string(),
282
- token_ids: vec![],
283
- logprobs: None,
284
- finished: None,
285
- }),
286
- Ok(DecodedTextEvent::TextDelta {
287
- delta: "def".to_string(),
288
- token_ids: vec![],
289
- logprobs: None,
290
- finished: Some(vllm_text::Finished {
291
- usage: vllm_llm::TokenUsage {
292
- prompt_token_count: 3,
293
- output_token_count: 0,
294
- cached_token_count: 0,
295
- },
296
- finish_reason: FinishReason::stop_eos(),
297
- kv_transfer_params: None,
298
- }),
299
- }),
300
- ]);
301
-
302
- let collected = reasoning_event_stream(events, Some(test_reasoning_parser(&mut factory)))
303
- .collect::<Vec<_>>()
304
- .await;
305
-
306
- let events = collected
307
- .into_iter()
308
- .collect::<crate::Result<Vec<_>>>()
309
- .expect("reasoning stream should not fail");
310
-
311
- assert_eq!(
312
- events,
313
- vec![
314
- ContentEvent::Start {
315
- prompt_token_ids: vec![1, 2, 3].into(),
316
- prompt_logprobs: None,
317
- },
318
- ContentEvent::TextDelta {
319
- kind: AssistantBlockKind::Text,
320
- delta: "abc".to_string(),
321
- },
322
- ContentEvent::TextDelta {
323
- kind: AssistantBlockKind::Text,
324
- delta: "def".to_string(),
325
- },
326
- ContentEvent::Done {
327
- usage: vllm_llm::TokenUsage {
328
- prompt_token_count: 3,
329
- output_token_count: 0,
330
- cached_token_count: 0,
331
- },
332
- finish_reason: FinishReason::stop_eos(),
333
- kv_transfer_params: None,
334
- },
335
- ]
336
- );
337
- }
338
-
339
- #[tokio::test]
340
- async fn reasoning_stream_preserves_logprobs_delta() {
341
- let events = stream::iter(vec![
342
- Ok(DecodedTextEvent::Start {
343
- prompt_token_ids: vec![1].into(),
344
- prompt_logprobs: None,
345
- }),
346
- Ok(DecodedTextEvent::TextDelta {
347
- delta: "abc".to_string(),
348
- token_ids: vec![],
349
- logprobs: Some(DecodedLogprobs {
350
- positions: vec![DecodedPositionLogprobs {
351
- entries: vec![DecodedTokenLogprob {
352
- token_id: 0,
353
- token: "a".to_string(),
354
- logprob: -0.1,
355
- rank: 1,
356
- }],
357
- }],
358
- }),
359
- finished: None,
360
- }),
361
- ]);
362
-
363
- let collected = reasoning_event_stream(events, None)
364
- .collect::<Vec<_>>()
365
- .await
366
- .into_iter()
367
- .collect::<crate::Result<Vec<_>>>()
368
- .unwrap();
369
-
370
- assert_eq!(
371
- collected,
372
- vec![
373
- ContentEvent::Start {
374
- prompt_token_ids: vec![1].into(),
375
- prompt_logprobs: None,
376
- },
377
- ContentEvent::TextDelta {
378
- kind: AssistantBlockKind::Text,
379
- delta: "abc".to_string(),
380
- },
381
- ContentEvent::LogprobsDelta {
382
- logprobs: Some(DecodedLogprobs {
383
- positions: vec![DecodedPositionLogprobs {
384
- entries: vec![DecodedTokenLogprob {
385
- token_id: 0,
386
- token: "a".to_string(),
387
- logprob: -0.1,
388
- rank: 1,
389
- }],
390
- }],
391
- }),
392
- token_ids: vec![],
393
- },
394
- ]
395
- );
396
- }
397
-
398
- #[tokio::test]
399
- async fn qwen3_parser_uses_prompt_end_marker_to_switch_to_content() {
400
- let tokenizer = Arc::new(FakeTokenizer);
401
- let events = stream::iter(vec![
402
- Ok(DecodedTextEvent::Start {
403
- prompt_token_ids: vec![2].into(),
404
- prompt_logprobs: None,
405
- }),
406
- Ok(DecodedTextEvent::TextDelta {
407
- delta: "thought ".to_string(),
408
- token_ids: vec![],
409
- logprobs: None,
410
- finished: None,
411
- }),
412
- Ok(DecodedTextEvent::TextDelta {
413
- delta: "done</think>OK".to_string(),
414
- token_ids: vec![],
415
- logprobs: None,
416
- finished: None,
417
- }),
418
- ]);
419
-
420
- let factory = ReasoningParserFactory::new();
421
- let collected = reasoning_event_stream(
422
- events,
423
- Some(factory.create(names::QWEN3, tokenizer).unwrap()),
424
- )
425
- .collect::<Vec<_>>()
426
- .await;
427
-
428
- let events = collected
429
- .into_iter()
430
- .collect::<crate::Result<Vec<_>>>()
431
- .expect("reasoning stream should not fail");
432
-
433
- assert_eq!(
434
- events,
435
- vec![
436
- ContentEvent::Start {
437
- prompt_token_ids: vec![2].into(),
438
- prompt_logprobs: None,
439
- },
440
- ContentEvent::TextDelta {
441
- kind: AssistantBlockKind::Text,
442
- delta: "thought ".to_string(),
443
- },
444
- ContentEvent::TextDelta {
445
- kind: AssistantBlockKind::Text,
446
- delta: "done</think>OK".to_string(),
447
- },
448
- ]
449
- );
450
- }
451
-
452
- #[tokio::test]
453
- async fn qwen3_parser_tolerates_prompt_prefill_reasoning() {
454
- let tokenizer = Arc::new(FakeTokenizer);
455
- let events = stream::iter(vec![
456
- Ok(DecodedTextEvent::Start {
457
- prompt_token_ids: vec![1].into(),
458
- prompt_logprobs: None,
459
- }),
460
- Ok(DecodedTextEvent::TextDelta {
461
- delta: "thought ".to_string(),
462
- token_ids: vec![],
463
- logprobs: None,
464
- finished: None,
465
- }),
466
- Ok(DecodedTextEvent::TextDelta {
467
- delta: "done</think>OK".to_string(),
468
- token_ids: vec![],
469
- logprobs: None,
470
- finished: None,
471
- }),
472
- ]);
473
-
474
- let factory = ReasoningParserFactory::new();
475
- let collected = reasoning_event_stream(
476
- events,
477
- Some(factory.create(names::QWEN3, tokenizer).unwrap()),
478
- )
479
- .collect::<Vec<_>>()
480
- .await;
481
-
482
- let events = collected
483
- .into_iter()
484
- .collect::<crate::Result<Vec<_>>>()
485
- .expect("reasoning stream should not fail");
486
-
487
- assert_eq!(
488
- events,
489
- vec![
490
- ContentEvent::Start {
491
- prompt_token_ids: vec![1].into(),
492
- prompt_logprobs: None,
493
- },
494
- ContentEvent::TextDelta {
495
- kind: AssistantBlockKind::Reasoning,
496
- delta: "thought ".to_string(),
497
- },
498
- ContentEvent::TextDelta {
499
- kind: AssistantBlockKind::Reasoning,
500
- delta: "done".to_string(),
501
- },
502
- ContentEvent::TextDelta {
503
- kind: AssistantBlockKind::Text,
504
- delta: "OK".to_string(),
505
- },
506
- ]
507
- );
508
- }
509
-}
rust/src/chat/src/output/default/structural_tag.rs
+1
-1
@@ -77,7 +77,7 @@ fn structural_tag_tool_choice(request: &ChatRequest) -> Option<StructuralTagTool
77
mod tests {
78
use serde_json::{Value, json};
79
use vllm_engine_core_client::protocol::{StructuredOutputBackend, StructuredOutputsParams};
80
- use vllm_tool_parser::{Qwen3CoderToolParser, Tool};
80
+ use vllm_parser::tool::{Qwen3CoderToolParser, Tool};
81
82
use super::*;
83
rust/src/chat/src/output/default/tool.rs
deleted
-985
@@ -1,985 +0,0 @@
1
-//! Adapts plain assistant text deltas into tool-call-aware assistant updates.
2
-//!
3
-//! This stage runs after reasoning separation and before final block assembly.
4
-//! It only inspects normal assistant text, leaves reasoning deltas untouched,
5
-//! and translates incremental tool parsing output into internal tool-call
6
-//! events while preserving plain-text fallback behavior.
7
-
8
-use asynk_strim_attr::{TryYielder, try_stream};
9
-use futures::{StreamExt as _, pin_mut};
10
-use thiserror_ext::AsReport;
11
-use tracing::warn;
12
-
13
-use super::{AssistantEvent, ContentEvent, ContentEventStream};
14
-use crate::Result;
15
-use crate::error::Error;
16
-use crate::event::AssistantBlockKind;
17
-use crate::output::generate_tool_call_id;
18
-use crate::parser::tool::{ToolCallDelta, ToolParser, ToolParserOutput};
19
-
20
-/// Per-stream tool parsing state.
21
-struct ToolState {
22
- /// Parser for the current model family.
23
- parser: Box<dyn ToolParser>,
24
- /// Whether tool parsing has already failed for this stream.
25
- parser_failed: bool,
26
- /// The parser-local index of the currently open tool call, if any.
27
- // NOTE: We only allow single open tool call at a time right now, since that's what all
28
- // supported parsers currently emit. Change this to a `BTreeMap` if we need to support multiple
29
- // interleaved calls in the future.
30
- open_call_index: Option<usize>,
31
-}
32
-
33
-impl ToolState {
34
- /// Create one fresh tool-parsing state for a new streamed response.
35
- fn new(parser: Box<dyn ToolParser>) -> Self {
36
- Self {
37
- parser,
38
- parser_failed: false,
39
- open_call_index: None,
40
- }
41
- }
42
-
43
- /// Convert one semantic assistant text delta into zero or more tool-aware
44
- /// internal events.
45
- fn process_text_delta(
46
- &mut self,
47
- kind: AssistantBlockKind,
48
- delta: String,
49
- ) -> Result<Vec<AssistantEvent>> {
50
- let mut events = Vec::new();
51
-
52
- // Only normal assistant text is eligible for tool parsing. Reasoning
53
- // blocks and plain-text fallback should pass through unchanged.
54
- if kind != AssistantBlockKind::Text || self.parser_failed {
55
- self.open_call_index = None;
56
- events.push(AssistantEvent::TextDelta { kind, delta });
57
- return Ok(events);
58
- }
59
-
60
- let mut output = ToolParserOutput::default();
61
- let parse_result = self.parser.parse_into(&delta, &mut output);
62
-
63
- match parse_result {
64
- Ok(()) => self.process_parser_output(kind, output, &mut events)?,
65
- Err(error) => {
66
- warn!(
67
- error = %error.as_report(),
68
- "tool parser failed; falling back to plain text deltas"
69
- );
70
- // Permanently mark this parser as failed.
71
- // TODO: we may consider recovering from parsing errors in the future.
72
- self.parser_failed = true;
73
-
74
- // On parsing failure, we still apply the partial parser output if any, but we close
75
- // any open tool calls and emit the remaining buffered text as a plain-text delta to
76
- // preserve as much of the output as possible.
77
- self.process_parser_output(kind, output, &mut events)?;
78
- self.open_call_index = None;
79
- push_text_delta(&mut events, kind, self.parser.reset());
80
- }
81
- }
82
-
83
- Ok(events)
84
- }
85
-
86
- /// Apply one parsed tool output to the current stream state.
87
- fn process_parser_output(
88
- &mut self,
89
- kind: AssistantBlockKind,
90
- output: ToolParserOutput,
91
- events: &mut Vec<AssistantEvent>,
92
- ) -> Result<()> {
93
- // When we are not currently streaming a tool call, preserve plain
94
- // text first and then surface any new tool call items.
95
- if self.open_call_index.is_none() {
96
- push_text_delta(events, kind, output.normal_text);
97
- self.process_tool_items(output.calls, events)?;
98
- } else {
99
- // Once a tool call is open, prioritize tool deltas first. If the
100
- // parser emits normal text again, close the tool call and resume
101
- // plain text output.
102
- self.process_tool_items(output.calls, events)?;
103
- if !output.normal_text.is_empty() {
104
- self.open_call_index = None;
105
- push_text_delta(events, kind, output.normal_text);
106
- }
107
- }
108
- Ok(())
109
- }
110
-
111
- /// Apply one batch of parsed tool-call deltas emitted by the parser.
112
- fn process_tool_items(
113
- &mut self,
114
- items: Vec<ToolCallDelta>,
115
- events: &mut Vec<AssistantEvent>,
116
- ) -> Result<()> {
117
- for item in items {
118
- if let Some(name) = item.name {
119
- let is_new_tool = match self.open_call_index {
120
- Some(open_call_index) => open_call_index != item.tool_index,
121
- None => true,
122
- };
123
- if is_new_tool {
124
- let id = self
125
- .parser
126
- .tool_call_id(item.tool_index)
127
- .map(str::to_string)
128
- .unwrap_or_else(generate_tool_call_id);
129
- self.open_call_index = Some(item.tool_index);
130
- events.push(AssistantEvent::ToolCallStart { id, name });
131
- }
132
- }
133
-
134
- if item.arguments.is_empty() {
135
- // No arguments delta to apply.
136
- continue;
137
- }
138
- let Some(open_call_index) = self.open_call_index else {
139
- return Err(Error::ToolCallStreamInvariant {
140
- message: format!(
141
- "received arguments for tool index {} before any tool-call start",
142
- item.tool_index
143
- ),
144
- });
145
- };
146
- if open_call_index != item.tool_index {
147
- return Err(Error::ToolCallStreamInvariant {
148
- message: format!(
149
- "received arguments for tool index {} while tool index {} is open",
150
- item.tool_index, open_call_index
151
- ),
152
- });
153
- }
154
-
155
- events.push(AssistantEvent::ToolCallArgumentsDelta {
156
- delta: item.arguments,
157
- });
158
- }
159
- Ok(())
160
- }
161
-
162
- /// Flush parser state at end-of-stream and close any remaining open calls.
163
- fn finish(&mut self) -> Result<Vec<AssistantEvent>> {
164
- let mut events = Vec::new();
165
-
166
- if self.parser_failed {
167
- return Ok(events);
168
- }
169
-
170
- match self.parser.finish() {
171
- Ok(output) => {
172
- self.process_parser_output(AssistantBlockKind::Text, output, &mut events)?
173
- }
174
- Err(error) => {
175
- warn!(
176
- error = %error.as_report(),
177
- "tool parser finish failed; closing open tool calls with buffered state"
178
- );
179
- self.parser_failed = true;
180
- }
181
- }
182
-
183
- Ok(events)
184
- }
185
-}
186
-
187
-/// Push one plain-text delta if it is non-empty.
188
-fn push_text_delta(events: &mut Vec<AssistantEvent>, kind: AssistantBlockKind, delta: String) {
189
- if delta.is_empty() {
190
- return;
191
- }
192
- events.push(AssistantEvent::TextDelta { kind, delta });
193
-}
194
-
195
-/// Wrap one semantic assistant stream into the internal tool-aware assistant
196
-/// stream.
197
-#[try_stream]
198
-pub(crate) async fn tool_event_stream(
199
- stream: impl ContentEventStream,
200
- parser: Option<Box<dyn ToolParser>>,
201
- mut y: TryYielder<AssistantEvent, Error>,
202
-) -> Result<()> {
203
- // Without a parser, pass through the input stream unchanged.
204
- let Some(parser) = parser else {
205
- pin_mut!(stream);
206
- while let Some(event) = stream.next().await.transpose()? {
207
- y.yield_ok(event.into()).await;
208
- }
209
- return Ok(());
210
- };
211
-
212
- pin_mut!(stream);
213
- let mut state = ToolState::new(parser);
214
-
215
- while let Some(event) = stream.next().await.transpose()? {
216
- match event {
217
- ContentEvent::Start {
218
- prompt_token_ids,
219
- prompt_logprobs,
220
- } => {
221
- y.yield_ok(AssistantEvent::Start {
222
- prompt_token_ids,
223
- prompt_logprobs,
224
- })
225
- .await;
226
- }
227
- ContentEvent::TextDelta { kind, delta } => {
228
- for next in state.process_text_delta(kind, delta)? {
229
- y.yield_ok(next).await;
230
- }
231
- }
232
- ContentEvent::LogprobsDelta {
233
- logprobs,
234
- token_ids,
235
- } => {
236
- y.yield_ok(AssistantEvent::LogprobsDelta {
237
- logprobs,
238
- token_ids,
239
- })
240
- .await;
241
- }
242
- ContentEvent::Done {
243
- usage,
244
- finish_reason,
245
- kv_transfer_params,
246
- } => {
247
- for next in state.finish()? {
248
- y.yield_ok(next).await;
249
- }
250
-
251
- y.yield_ok(AssistantEvent::Done {
252
- usage,
253
- finish_reason,
254
- kv_transfer_params,
255
- })
256
- .await;
257
- }
258
- }
259
- }
260
- Ok(())
261
-}
262
-
263
-#[cfg(test)]
264
-mod tests {
265
-
266
- use futures::{StreamExt as _, stream};
267
- use vllm_llm::FinishReason;
268
- use vllm_text::{DecodedLogprobs, DecodedPositionLogprobs, DecodedTokenLogprob};
269
- use vllm_tool_parser::Result;
270
-
271
- use super::super::{AssistantEvent, ContentEvent};
272
- use super::tool_event_stream;
273
- use crate::error::Error;
274
- use crate::event::{AssistantBlockKind, AssistantMessageExt as _};
275
- use crate::output::structured::structured_chat_event_stream;
276
- use crate::parser::tool::{
277
- DeepSeekV4ToolParser, ToolParser, ToolParserError, ToolParserOutput,
278
- };
279
- use crate::request::ChatTool;
280
- use crate::stream::{ChatEventStream, CollectedAssistantMessage};
281
-
282
- struct FailingParser {
283
- fail_next: bool,
284
- buffered: String,
285
- }
286
-
287
- struct ScriptedParser {
288
- push_outputs: Vec<ToolParserOutput>,
289
- finish_output: ToolParserOutput,
290
- }
291
-
292
- struct PartialThenFailParser {
293
- buffered: String,
294
- }
295
-
296
- struct IdScriptedParser {
297
- output: ToolParserOutput,
298
- tool_call_id: Option<String>,
299
- }
300
-
301
- impl ToolParser for FailingParser {
302
- fn create(_tools: &[ChatTool]) -> vllm_tool_parser::Result<Box<dyn ToolParser>>
303
- where
304
- Self: Sized + 'static,
305
- {
306
- Ok(Box::new(Self {
307
- fail_next: false,
308
- buffered: String::new(),
309
- }))
310
- }
311
-
312
- fn parse_into(&mut self, chunk: &str, _output: &mut ToolParserOutput) -> Result<()> {
313
- self.buffered.push_str(chunk);
314
- if self.fail_next {
315
- self.fail_next = false;
316
- return Err(ToolParserError::ParsingFailed {
317
- message: "boom".to_string(),
318
- });
319
- }
320
-
321
- self.buffered.clear();
322
- Ok(())
323
- }
324
-
325
- fn finish(&mut self) -> Result<ToolParserOutput> {
326
- Ok(ToolParserOutput::default())
327
- }
328
-
329
- fn reset(&mut self) -> String {
330
- std::mem::take(&mut self.buffered)
331
- }
332
- }
333
-
334
- impl ToolParser for ScriptedParser {
335
- fn create(_tools: &[ChatTool]) -> vllm_tool_parser::Result<Box<dyn ToolParser>>
336
- where
337
- Self: Sized + 'static,
338
- {
339
- Ok(Box::new(Self {
340
- push_outputs: Vec::new(),
341
- finish_output: ToolParserOutput::default(),
342
- }))
343
- }
344
-
345
- fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
346
- let mut next = self.push_outputs.pop().unwrap_or_default();
347
- output.normal_text.push_str(&next.normal_text);
348
- output.calls.append(&mut next.calls);
349
- Ok(())
350
- }
351
-
352
- fn finish(&mut self) -> Result<ToolParserOutput> {
353
- Ok(std::mem::take(&mut self.finish_output))
354
- }
355
-
356
- fn reset(&mut self) -> String {
357
- String::new()
358
- }
359
- }
360
-
361
- impl ToolParser for IdScriptedParser {
362
- fn create(_tools: &[ChatTool]) -> vllm_tool_parser::Result<Box<dyn ToolParser>>
363
- where
364
- Self: Sized + 'static,
365
- {
366
- Ok(Box::new(Self {
367
- output: ToolParserOutput::default(),
368
- tool_call_id: None,
369
- }))
370
- }
371
-
372
- fn tool_call_id(&self, tool_index: usize) -> Option<&str> {
373
- (tool_index == 0).then_some(self.tool_call_id.as_deref()).flatten()
374
- }
375
-
376
- fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
377
- output.append(std::mem::take(&mut self.output));
378
- Ok(())
379
- }
380
-
381
- fn finish(&mut self) -> Result<ToolParserOutput> {
382
- Ok(ToolParserOutput::default())
383
- }
384
-
385
- fn reset(&mut self) -> String {
386
- String::new()
387
- }
388
- }
389
-
390
- impl ToolParser for PartialThenFailParser {
391
- fn create(_tools: &[ChatTool]) -> vllm_tool_parser::Result<Box<dyn ToolParser>>
392
- where
393
- Self: Sized + 'static,
394
- {
395
- Ok(Box::new(Self {
396
- buffered: String::new(),
397
- }))
398
- }
399
-
400
- fn parse_into(&mut self, _chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
401
- output.calls.extend([
402
- crate::parser::tool::ToolCallDelta {
403
- tool_index: 0,
404
- name: Some("get_weather".to_string()),
405
- arguments: String::new(),
406
- },
407
- crate::parser::tool::ToolCallDelta {
408
- tool_index: 0,
409
- name: None,
410
- arguments: r#"{"location":"SF"}"#.to_string(),
411
- },
412
- ]);
413
- self.buffered.push_str(" trailing text");
414
- Err(ToolParserError::ParsingFailed {
415
- message: "boom".to_string(),
416
- })
417
- }
418
-
419
- fn finish(&mut self) -> Result<ToolParserOutput> {
420
- Ok(ToolParserOutput::default())
421
- }
422
-
423
- fn reset(&mut self) -> String {
424
- std::mem::take(&mut self.buffered)
425
- }
426
- }
427
-
428
- fn deepseek_v4_test_tools() -> Vec<ChatTool> {
429
- vec![
430
- ChatTool {
431
- name: "get_weather".to_string(),
432
- description: None,
433
- parameters: serde_json::json!({
434
- "type": "object",
435
- "properties": {
436
- "location": { "type": "string" }
437
- }
438
- }),
439
- strict: None,
440
- },
441
- ChatTool {
442
- name: "add".to_string(),
443
- description: None,
444
- parameters: serde_json::json!({
445
- "type": "object",
446
- "properties": {
447
- "x": { "type": "integer" },
448
- "y": { "type": "integer" }
449
- }
450
- }),
451
- strict: None,
452
- },
453
- ]
454
- }
455
-
456
- async fn collect_deepseek_v4_message(chunks: Vec<String>) -> CollectedAssistantMessage {
457
- let events = chunks
458
- .into_iter()
459
- .map(|delta| {
460
- Ok(ContentEvent::TextDelta {
461
- kind: AssistantBlockKind::Text,
462
- delta,
463
- })
464
- })
465
- .chain(std::iter::once(Ok(ContentEvent::Done {
466
- usage: vllm_llm::TokenUsage {
467
- prompt_token_count: 1,
468
- output_token_count: 1,
469
- cached_token_count: 0,
470
- },
471
- finish_reason: FinishReason::stop_eos(),
472
- kv_transfer_params: None,
473
- })));
474
- let parser = DeepSeekV4ToolParser::create(&deepseek_v4_test_tools()).unwrap();
475
- let assistant_events = tool_event_stream(stream::iter(events), Some(parser));
476
- let chat_events = structured_chat_event_stream(assistant_events, true);
477
-
478
- ChatEventStream::new("req_deepseek_v4".to_string(), Box::pin(chat_events))
479
- .collect_message()
480
- .await
481
- .unwrap()
482
- }
483
-
484
- fn message_tool_projection(
485
- message: &CollectedAssistantMessage,
486
- ) -> (String, Vec<(String, serde_json::Value)>) {
487
- (
488
- message.message.text(),
489
- message
490
- .message
491
- .tool_calls()
492
- .map(|call| {
493
- (
494
- call.name.clone(),
495
- serde_json::from_str(&call.arguments).unwrap(),
496
- )
497
- })
498
- .collect(),
499
- )
500
- }
501
-
502
- #[tokio::test]
503
- async fn tool_parser_error_preserves_partial_output_and_flushes_buffer() {
504
- let events = stream::iter(vec![
505
- Ok(ContentEvent::TextDelta {
506
- kind: AssistantBlockKind::Text,
507
- delta: "ignored".to_string(),
508
- }),
509
- Ok(ContentEvent::Done {
510
- usage: vllm_llm::TokenUsage {
511
- prompt_token_count: 1,
512
- output_token_count: 1,
513
- cached_token_count: 0,
514
- },
515
- finish_reason: FinishReason::stop_eos(),
516
- kv_transfer_params: None,
517
- }),
518
- ]);
519
-
520
- let events = tool_event_stream(
521
- events,
522
- Some(Box::new(PartialThenFailParser {
523
- buffered: String::new(),
524
- })),
525
- )
526
- .collect::<Vec<_>>()
527
- .await
528
- .into_iter()
529
- .collect::<crate::Result<Vec<_>>>()
530
- .unwrap();
531
-
532
- assert!(matches!(
533
- &events[0],
534
- AssistantEvent::ToolCallStart { name, .. } if name == "get_weather"
535
- ));
536
- assert!(matches!(
537
- &events[1],
538
- AssistantEvent::ToolCallArgumentsDelta { delta } if delta == r#"{"location":"SF"}"#
539
- ));
540
- assert_eq!(
541
- events[2],
542
- AssistantEvent::TextDelta {
543
- kind: AssistantBlockKind::Text,
544
- delta: " trailing text".to_string(),
545
- }
546
- );
547
- assert!(matches!(events[3], AssistantEvent::Done { .. }));
548
- }
549
-
550
- #[tokio::test]
551
- async fn tool_stream_preserves_parser_provided_tool_call_id() {
552
- let events = stream::iter(vec![Ok(ContentEvent::TextDelta {
553
- kind: AssistantBlockKind::Text,
554
- delta: "ignored".to_string(),
555
- })]);
556
- let parser = IdScriptedParser {
557
- output: ToolParserOutput {
558
- normal_text: String::new(),
559
- calls: vec![crate::parser::tool::ToolCallDelta {
560
- tool_index: 0,
561
- name: Some("get_weather".to_string()),
562
- arguments: "{}".to_string(),
563
- }],
564
- },
565
- tool_call_id: Some("functions.get_weather:0".to_string()),
566
- };
567
-
568
- let events = tool_event_stream(events, Some(Box::new(parser)))
569
- .collect::<Vec<_>>()
570
- .await
571
- .into_iter()
572
- .collect::<crate::Result<Vec<_>>>()
573
- .unwrap();
574
-
575
- assert!(matches!(
576
- &events[0],
577
- AssistantEvent::ToolCallStart { id, name }
578
- if id == "functions.get_weather:0" && name == "get_weather"
579
- ));
580
- }
581
-
582
- #[tokio::test]
583
- async fn tool_stream_generates_tool_call_id_when_parser_omits_one() {
584
- let events = stream::iter(vec![Ok(ContentEvent::TextDelta {
585
- kind: AssistantBlockKind::Text,
586
- delta: "ignored".to_string(),
587
- })]);
588
- let parser = IdScriptedParser {
589
- output: ToolParserOutput {
590
- normal_text: String::new(),
591
- calls: vec![crate::parser::tool::ToolCallDelta {
592
- tool_index: 0,
593
- name: Some("get_weather".to_string()),
594
- arguments: "{}".to_string(),
595
- }],
596
- },
597
- tool_call_id: None,
598
- };
599
-
600
- let events = tool_event_stream(events, Some(Box::new(parser)))
601
- .collect::<Vec<_>>()
602
- .await
603
- .into_iter()
604
- .collect::<crate::Result<Vec<_>>>()
605
- .unwrap();
606
-
607
- assert!(matches!(
608
- &events[0],
609
- AssistantEvent::ToolCallStart { id, name }
610
- if id.starts_with("call_") && name == "get_weather"
611
- ));
612
- }
613
-
614
- #[tokio::test]
615
- async fn real_buffered_parser_error_matches_streaming_and_non_streaming() {
616
- let prefix = "I will check both.\n";
617
- let first_tool_call = concat!(
618
- "<|DSML|tool_calls>\n",
619
- "<|DSML|invoke name=\"get_weather\">\n",
620
- "<|DSML|parameter name=\"location\" string=\"true\">Tokyo</|DSML|parameter>\n",
621
- "</|DSML|invoke>",
622
- );
623
- let malformed_second_tool_call = concat!(
624
- "\n<|DSML|invoke name=\"add\">\n",
625
- "not a parameter\n",
626
- "</|DSML|invoke>\n",
627
- "</|DSML|tool_calls>",
628
- );
629
- let streaming_chunks = vec![
630
- prefix.to_string(),
631
- first_tool_call.to_string(),
632
- malformed_second_tool_call.to_string(),
633
- ];
634
- let full_output = streaming_chunks.concat();
635
-
636
- let streaming = collect_deepseek_v4_message(streaming_chunks).await;
637
- let non_streaming = collect_deepseek_v4_message(vec![full_output]).await;
638
-
639
- let expected = (
640
- format!("{prefix}{malformed_second_tool_call}"),
641
- vec![(
642
- "get_weather".to_string(),
643
- serde_json::json!({ "location": "Tokyo" }),
644
- )],
645
- );
646
- assert_eq!(message_tool_projection(&streaming), expected);
647
- assert_eq!(message_tool_projection(&non_streaming), expected);
648
- }
649
-
650
- #[tokio::test]
651
- async fn tool_parser_failure_falls_back_to_plain_text() {
652
- let events = stream::iter(vec![
653
- Ok(ContentEvent::Start {
654
- prompt_token_ids: vec![1, 2, 3].into(),
655
- prompt_logprobs: None,
656
- }),
657
- Ok(ContentEvent::TextDelta {
658
- kind: AssistantBlockKind::Text,
659
- delta: "abc".to_string(),
660
- }),
661
- Ok(ContentEvent::TextDelta {
662
- kind: AssistantBlockKind::Text,
663
- delta: "def".to_string(),
664
- }),
665
- Ok(ContentEvent::Done {
666
- usage: vllm_llm::TokenUsage {
667
- prompt_token_count: 3,
668
- output_token_count: 0,
669
- cached_token_count: 0,
670
- },
671
- finish_reason: FinishReason::stop_eos(),
672
- kv_transfer_params: None,
673
- }),
674
- ]);
675
-
676
- let collected = tool_event_stream(
677
- events,
678
- Some(Box::new(FailingParser {
679
- fail_next: true,
680
- buffered: String::new(),
681
- })),
682
- )
683
- .collect::<Vec<_>>()
684
- .await;
685
-
686
- let events = collected
687
- .into_iter()
688
- .collect::<crate::Result<Vec<_>>>()
689
- .expect("tool stream should not fail");
690
-
691
- assert_eq!(
692
- events,
693
- vec![
694
- AssistantEvent::Start {
695
- prompt_token_ids: vec![1, 2, 3].into(),
696
- prompt_logprobs: None,
697
- },
698
- AssistantEvent::TextDelta {
699
- kind: AssistantBlockKind::Text,
700
- delta: "abc".to_string(),
701
- },
702
- AssistantEvent::TextDelta {
703
- kind: AssistantBlockKind::Text,
704
- delta: "def".to_string(),
705
- },
706
- AssistantEvent::Done {
707
- usage: vllm_llm::TokenUsage {
708
- prompt_token_count: 3,
709
- output_token_count: 0,
710
- cached_token_count: 0,
711
- },
712
- finish_reason: FinishReason::stop_eos(),
713
- kv_transfer_params: None,
714
- },
715
- ]
716
- );
717
-
718
- let message = ChatEventStream::new(
719
- "req_fallback".to_string(),
720
- Box::pin(structured_chat_event_stream(
721
- stream::iter(events.into_iter().map(Ok)),
722
- true,
723
- )),
724
- )
725
- .collect_message()
726
- .await
727
- .expect("collect_message should succeed");
728
- assert_eq!(message.message.text(), "abcdef");
729
- assert!(message.message.tool_calls().next().is_none());
730
- }
731
-
732
- #[tokio::test]
733
- async fn tool_stream_preserves_logprobs_delta() {
734
- let events = stream::iter(vec![
735
- Ok(ContentEvent::Start {
736
- prompt_token_ids: vec![1].into(),
737
- prompt_logprobs: None,
738
- }),
739
- Ok(ContentEvent::LogprobsDelta {
740
- logprobs: Some(DecodedLogprobs {
741
- positions: vec![DecodedPositionLogprobs {
742
- entries: vec![DecodedTokenLogprob {
743
- token_id: 0,
744
- token: "a".to_string(),
745
- logprob: -0.2,
746
- rank: 1,
747
- }],
748
- }],
749
- }),
750
- token_ids: vec![],
751
- }),
752
- Ok(ContentEvent::Done {
753
- usage: vllm_llm::TokenUsage {
754
- prompt_token_count: 1,
755
- output_token_count: 0,
756
- cached_token_count: 0,
757
- },
758
- finish_reason: FinishReason::stop_eos(),
759
- kv_transfer_params: None,
760
- }),
761
- ]);
762
- let events = tool_event_stream(
763
- events,
764
- Some(Box::new(FailingParser {
765
- fail_next: false,
766
- buffered: String::new(),
767
- })),
768
- )
769
- .collect::<Vec<_>>()
770
- .await
771
- .into_iter()
772
- .collect::<crate::Result<Vec<_>>>()
773
- .unwrap();
774
-
775
- assert_eq!(
776
- events,
777
- vec![
778
- AssistantEvent::Start {
779
- prompt_token_ids: vec![1].into(),
780
- prompt_logprobs: None,
781
- },
782
- AssistantEvent::LogprobsDelta {
783
- logprobs: Some(DecodedLogprobs {
784
- positions: vec![DecodedPositionLogprobs {
785
- entries: vec![DecodedTokenLogprob {
786
- token_id: 0,
787
- token: "a".to_string(),
788
- logprob: -0.2,
789
- rank: 1,
790
- }],
791
- }],
792
- }),
793
- token_ids: vec![],
794
- },
795
- AssistantEvent::Done {
796
- usage: vllm_llm::TokenUsage {
797
- prompt_token_count: 1,
798
- output_token_count: 0,
799
- cached_token_count: 0,
800
- },
801
- finish_reason: FinishReason::stop_eos(),
802
- kv_transfer_params: None,
803
- },
804
- ]
805
- );
806
- }
807
-
808
- #[tokio::test]
809
- async fn tool_stream_rejects_interleaved_tool_indices() {
810
- let events = stream::iter(vec![
811
- Ok(ContentEvent::TextDelta {
812
- kind: AssistantBlockKind::Text,
813
- delta: "ignored".to_string(),
814
- }),
815
- Ok(ContentEvent::Done {
816
- usage: vllm_llm::TokenUsage {
817
- prompt_token_count: 1,
818
- output_token_count: 1,
819
- cached_token_count: 0,
820
- },
821
- finish_reason: FinishReason::stop_eos(),
822
- kv_transfer_params: None,
823
- }),
824
- ]);
825
-
826
- let parser = ScriptedParser {
827
- push_outputs: vec![ToolParserOutput {
828
- normal_text: String::new(),
829
- calls: vec![
830
- crate::parser::tool::ToolCallDelta {
831
- tool_index: 0,
832
- name: Some("first".to_string()),
833
- arguments: String::new(),
834
- },
835
- crate::parser::tool::ToolCallDelta {
836
- tool_index: 1,
837
- name: None,
838
- arguments: "{}".to_string(),
839
- },
840
- ],
841
- }],
842
- finish_output: ToolParserOutput::default(),
843
- };
844
-
845
- let err = tool_event_stream(events, Some(Box::new(parser)))
846
- .collect::<Vec<_>>()
847
- .await
848
- .into_iter()
849
- .find_map(|output| output.err())
850
- .expect("expected invariant error");
851
-
852
- assert!(matches!(err, Error::ToolCallStreamInvariant { .. }));
853
- }
854
-
855
- #[tokio::test]
856
- async fn tool_stream_resets_open_tool_when_normal_text_interrupts_it() {
857
- let events = stream::iter(vec![
858
- Ok(ContentEvent::TextDelta {
859
- kind: AssistantBlockKind::Text,
860
- delta: "start".to_string(),
861
- }),
862
- Ok(ContentEvent::TextDelta {
863
- kind: AssistantBlockKind::Text,
864
- delta: "text".to_string(),
865
- }),
866
- Ok(ContentEvent::TextDelta {
867
- kind: AssistantBlockKind::Text,
868
- delta: "args".to_string(),
869
- }),
870
- ]);
871
-
872
- let parser = ScriptedParser {
873
- push_outputs: vec![
874
- ToolParserOutput {
875
- normal_text: String::new(),
876
- calls: vec![crate::parser::tool::ToolCallDelta {
877
- tool_index: 0,
878
- name: None,
879
- arguments: "}".to_string(),
880
- }],
881
- },
882
- ToolParserOutput {
883
- normal_text: "plain text".to_string(),
884
- calls: Vec::new(),
885
- },
886
- ToolParserOutput {
887
- normal_text: String::new(),
888
- calls: vec![crate::parser::tool::ToolCallDelta {
889
- tool_index: 0,
890
- name: Some("first".to_string()),
891
- arguments: "{".to_string(),
892
- }],
893
- },
894
- ],
895
- finish_output: ToolParserOutput::default(),
896
- };
897
-
898
- let err = tool_event_stream(events, Some(Box::new(parser)))
899
- .collect::<Vec<_>>()
900
- .await
901
- .into_iter()
902
- .find_map(|output| output.err())
903
- .expect("expected invariant error");
904
-
905
- assert!(matches!(
906
- err,
907
- Error::ToolCallStreamInvariant { message }
908
- if message == "received arguments for tool index 0 before any tool-call start"
909
- ));
910
- }
911
-
912
- #[tokio::test]
913
- async fn tool_stream_emits_start_and_args_for_terminal_text() {
914
- let events = stream::iter(vec![
915
- Ok(ContentEvent::Start {
916
- prompt_token_ids: vec![1].into(),
917
- prompt_logprobs: None,
918
- }),
919
- Ok(ContentEvent::TextDelta {
920
- kind: AssistantBlockKind::Text,
921
- delta: "ignored".to_string(),
922
- }),
923
- Ok(ContentEvent::Done {
924
- usage: vllm_llm::TokenUsage {
925
- prompt_token_count: 1,
926
- output_token_count: 1,
927
- cached_token_count: 0,
928
- },
929
- finish_reason: FinishReason::stop_eos(),
930
- kv_transfer_params: None,
931
- }),
932
- ]);
933
-
934
- let parser = ScriptedParser {
935
- push_outputs: vec![ToolParserOutput {
936
- normal_text: String::new(),
937
- calls: vec![
938
- crate::parser::tool::ToolCallDelta {
939
- tool_index: 0,
940
- name: Some("first".to_string()),
941
- arguments: r#"{"a":1}"#.to_string(),
942
- },
943
- crate::parser::tool::ToolCallDelta {
944
- tool_index: 1,
945
- name: Some("second".to_string()),
946
- arguments: r#"{"b":2}"#.to_string(),
947
- },
948
- ],
949
- }],
950
- finish_output: ToolParserOutput::default(),
951
- };
952
-
953
- let events = tool_event_stream(events, Some(Box::new(parser)))
954
- .collect::<Vec<_>>()
955
- .await
956
- .into_iter()
957
- .collect::<crate::Result<Vec<_>>>()
958
- .unwrap();
959
-
960
- assert!(matches!(events[1], AssistantEvent::ToolCallStart { .. }));
961
- assert!(matches!(
962
- events[2],
963
- AssistantEvent::ToolCallArgumentsDelta { .. }
964
- ));
965
- assert!(matches!(events[3], AssistantEvent::ToolCallStart { .. }));
966
- assert!(matches!(
967
- events[4],
968
- AssistantEvent::ToolCallArgumentsDelta { .. }
969
- ));
970
- let collected = ChatEventStream::new(
971
- "req_final_only".to_string(),
972
- Box::pin(structured_chat_event_stream(
973
- stream::iter(events.into_iter().map(Ok)),
974
- true,
975
- )),
976
- )
977
- .collect_message()
978
- .await
979
- .unwrap();
980
- let tool_calls = collected.message.tool_calls().collect::<Vec<_>>();
981
- assert_eq!(tool_calls.len(), 2);
982
- assert_eq!(tool_calls[0].name, "first");
983
- assert_eq!(tool_calls[1].name, "second");
984
- }
985
-}
rust/src/chat/src/output/default/unified.rs
new
+582
@@ -0,0 +1,582 @@
1
+//! Adapts decoded text updates into parsed assistant deltas.
2
+//!
3
+//! This stage sits between low-level token decoding and final block assembly.
4
+//! It drives one unified parser that may emit normal text, reasoning text, or
5
+//! tool-call deltas, then normalizes those parser events into internal
6
+//! assistant events.
7
+
8
+use asynk_strim_attr::{TryYielder, try_stream};
9
+use futures::{StreamExt as _, pin_mut};
10
+use thiserror_ext::AsReport;
11
+use tracing::warn;
12
+use vllm_parser::unified::{UnifiedParser, UnifiedParserEvent, UnifiedParserOutput};
13
+use vllm_text::output::DecodedTextEvent;
14
+
15
+use crate::Result;
16
+use crate::error::Error;
17
+use crate::event::AssistantBlockKind;
18
+use crate::output::{AssistantEvent, DecodedTextEventStream, generate_tool_call_id};
19
+
20
+/// Per-stream unified parsing state.
21
+struct UnifiedParserState {
22
+ /// Parser for the current request stream.
23
+ parser: Box<dyn UnifiedParser>,
24
+ /// Whether unified parsing has already failed for this stream.
25
+ parser_failed: bool,
26
+ /// The parser-local index of the currently open tool call, if any.
27
+ ///
28
+ /// Supported parsers currently emit at most one active tool call at a time.
29
+ /// Change this to an indexed map if a model needs interleaved calls later.
30
+ open_call_index: Option<usize>,
31
+}
32
+
33
+impl UnifiedParserState {
34
+ /// Create one fresh unified parsing state for a new streamed response.
35
+ fn new(parser: Box<dyn UnifiedParser>) -> Self {
36
+ Self {
37
+ parser,
38
+ parser_failed: false,
39
+ open_call_index: None,
40
+ }
41
+ }
42
+
43
+ /// Initialize parser state once prompt token IDs are available.
44
+ fn initialize(&mut self, prompt_token_ids: &[u32]) {
45
+ if self.parser_failed {
46
+ return;
47
+ }
48
+
49
+ match self.parser.initialize(prompt_token_ids) {
50
+ Ok(()) => {}
51
+ Err(error) => {
52
+ warn!(
53
+ error = %error.as_report(),
54
+ "failed to initialize unified parser; falling back to plain text deltas"
55
+ );
56
+ self.parser_failed = true;
57
+ self.open_call_index = None;
58
+ }
59
+ }
60
+ }
61
+
62
+ /// Convert one decoded text delta into zero or more parsed assistant events.
63
+ fn process_delta(&mut self, delta: String) -> Result<Vec<AssistantEvent>> {
64
+ if self.parser_failed {
65
+ self.open_call_index = None;
66
+ return Ok(text_event(AssistantBlockKind::Text, delta).into_iter().collect());
67
+ }
68
+
69
+ let mut output = UnifiedParserOutput::default();
70
+ match self.parser.parse_into(&delta, &mut output) {
71
+ Ok(()) => {
72
+ let mut events = Vec::new();
73
+ self.process_parser_output(output, &mut events)?;
74
+ Ok(events)
75
+ }
76
+ Err(error) => {
77
+ warn!(
78
+ error = %error.as_report(),
79
+ "unified parser failed; falling back to plain text deltas"
80
+ );
81
+ self.parser_failed = true;
82
+
83
+ let mut events = Vec::new();
84
+ self.process_parser_output(output, &mut events)?;
85
+ self.open_call_index = None;
86
+
87
+ let recovered = self.parser.reset();
88
+ if recovered.is_empty() && events.is_empty() {
89
+ push_text_delta(&mut events, AssistantBlockKind::Text, delta);
90
+ } else {
91
+ push_text_delta(&mut events, AssistantBlockKind::Text, recovered);
92
+ }
93
+ Ok(events)
94
+ }
95
+ }
96
+ }
97
+
98
+ /// Flush parser state at end-of-stream and close any remaining open calls.
99
+ fn finish(&mut self) -> Result<Vec<AssistantEvent>> {
100
+ let mut events = Vec::new();
101
+
102
+ if self.parser_failed {
103
+ return Ok(events);
104
+ }
105
+
106
+ match self.parser.finish() {
107
+ Ok(output) => self.process_parser_output(output, &mut events)?,
108
+ Err(error) => {
109
+ warn!(
110
+ error = %error.as_report(),
111
+ "unified parser finish failed; closing open parser state"
112
+ );
113
+ self.parser_failed = true;
114
+ self.open_call_index = None;
115
+ // TODO: should we reset and emit the buffered text?
116
+ }
117
+ }
118
+
119
+ Ok(events)
120
+ }
121
+
122
+ /// Apply one parsed unified output to the current stream state.
123
+ fn process_parser_output(
124
+ &mut self,
125
+ output: UnifiedParserOutput,
126
+ events: &mut Vec<AssistantEvent>,
127
+ ) -> Result<()> {
128
+ for event in output.events {
129
+ match event {
130
+ UnifiedParserEvent::Text(delta) => {
131
+ self.open_call_index = None;
132
+ push_text_delta(events, AssistantBlockKind::Text, delta);
133
+ }
134
+ UnifiedParserEvent::Reasoning(delta) => {
135
+ self.open_call_index = None;
136
+ push_text_delta(events, AssistantBlockKind::Reasoning, delta);
137
+ }
138
+ UnifiedParserEvent::ToolCall(item) => {
139
+ self.process_tool_item(item, events)?;
140
+ }
141
+ }
142
+ }
143
+
144
+ Ok(())
145
+ }
146
+
147
+ /// Apply one parsed tool-call delta emitted by the parser.
148
+ fn process_tool_item(
149
+ &mut self,
150
+ item: vllm_parser::tool::ToolCallDelta,
151
+ events: &mut Vec<AssistantEvent>,
152
+ ) -> Result<()> {
153
+ if let Some(name) = item.name {
154
+ let is_new_tool = match self.open_call_index {
155
+ Some(open_call_index) => open_call_index != item.tool_index,
156
+ None => true,
157
+ };
158
+ if is_new_tool {
159
+ let id = self
160
+ .parser
161
+ .tool_call_id(item.tool_index)
162
+ .map(str::to_string)
163
+ .unwrap_or_else(generate_tool_call_id);
164
+ self.open_call_index = Some(item.tool_index);
165
+ events.push(AssistantEvent::ToolCallStart { id, name });
166
+ }
167
+ }
168
+
169
+ if item.arguments.is_empty() {
170
+ return Ok(());
171
+ }
172
+ let Some(open_call_index) = self.open_call_index else {
173
+ return Err(Error::ToolCallStreamInvariant {
174
+ message: format!(
175
+ "received arguments for tool index {} before any tool-call start",
176
+ item.tool_index
177
+ ),
178
+ });
179
+ };
180
+ if open_call_index != item.tool_index {
181
+ return Err(Error::ToolCallStreamInvariant {
182
+ message: format!(
183
+ "received arguments for tool index {} while tool index {} is open",
184
+ item.tool_index, open_call_index
185
+ ),
186
+ });
187
+ }
188
+
189
+ events.push(AssistantEvent::ToolCallArgumentsDelta {
190
+ delta: item.arguments,
191
+ });
192
+ Ok(())
193
+ }
194
+}
195
+
196
+/// Build one plain text event if `delta` is non-empty.
197
+fn text_event(kind: AssistantBlockKind, delta: String) -> Option<AssistantEvent> {
198
+ if delta.is_empty() {
199
+ return None;
200
+ }
201
+ Some(AssistantEvent::TextDelta { kind, delta })
202
+}
203
+
204
+/// Push one plain text delta if it is non-empty.
205
+fn push_text_delta(events: &mut Vec<AssistantEvent>, kind: AssistantBlockKind, delta: String) {
206
+ if let Some(event) = text_event(kind, delta) {
207
+ events.push(event);
208
+ }
209
+}
210
+
211
+/// Wrap one decoded-text stream into the internal unified assistant stream.
212
+#[try_stream]
213
+pub(crate) async fn unified_event_stream(
214
+ decoded_stream: impl DecodedTextEventStream,
215
+ parser: Box<dyn UnifiedParser>,
216
+ mut y: TryYielder<AssistantEvent, Error>,
217
+) -> Result<()> {
218
+ pin_mut!(decoded_stream);
219
+
220
+ let mut state = UnifiedParserState::new(parser);
221
+
222
+ while let Some(event) = decoded_stream.next().await.transpose()? {
223
+ match event {
224
+ DecodedTextEvent::Start {
225
+ prompt_token_ids,
226
+ prompt_logprobs,
227
+ } => {
228
+ state.initialize(&prompt_token_ids);
229
+ y.yield_ok(AssistantEvent::Start {
230
+ prompt_token_ids,
231
+ prompt_logprobs,
232
+ })
233
+ .await;
234
+ }
235
+ DecodedTextEvent::TextDelta {
236
+ delta,
237
+ token_ids,
238
+ logprobs,
239
+ finished,
240
+ } => {
241
+ for next in state.process_delta(delta)? {
242
+ y.yield_ok(next).await;
243
+ }
244
+ if logprobs.is_some() || !token_ids.is_empty() {
245
+ y.yield_ok(AssistantEvent::LogprobsDelta {
246
+ logprobs,
247
+ token_ids,
248
+ })
249
+ .await;
250
+ }
251
+ if let Some(finished) = finished {
252
+ for next in state.finish()? {
253
+ y.yield_ok(next).await;
254
+ }
255
+ y.yield_ok(AssistantEvent::Done {
256
+ usage: finished.usage,
257
+ finish_reason: finished.finish_reason,
258
+ kv_transfer_params: finished.kv_transfer_params,
259
+ })
260
+ .await;
261
+ }
262
+ }
263
+ }
264
+ }
265
+ Ok(())
266
+}
267
+
268
+#[cfg(test)]
269
+mod tests {
270
+ use std::collections::VecDeque;
271
+
272
+ use futures::{StreamExt as _, stream};
273
+ use vllm_parser::reasoning::ReasoningError;
274
+ use vllm_parser::tool::ToolCallDelta;
275
+ use vllm_parser::unified::{UnifiedParserError, UnifiedParserOutput};
276
+
277
+ use super::unified_event_stream;
278
+ use crate::event::AssistantBlockKind;
279
+ use crate::output::AssistantEvent;
280
+
281
+ enum ScriptedStep {
282
+ Output(UnifiedParserOutput),
283
+ Error {
284
+ committed: UnifiedParserOutput,
285
+ reset_text: String,
286
+ },
287
+ }
288
+
289
+ struct ScriptedParser {
290
+ steps: VecDeque<ScriptedStep>,
291
+ reset_text: String,
292
+ tool_call_id: Option<String>,
293
+ finish_error_reset_text: Option<String>,
294
+ }
295
+
296
+ impl ScriptedParser {
297
+ fn new(steps: impl IntoIterator<Item = ScriptedStep>) -> Self {
298
+ Self {
299
+ steps: steps.into_iter().collect(),
300
+ reset_text: String::new(),
301
+ tool_call_id: Some("call_test".to_string()),
302
+ finish_error_reset_text: None,
303
+ }
304
+ }
305
+
306
+ fn with_finish_error(mut self, reset_text: &str) -> Self {
307
+ self.finish_error_reset_text = Some(reset_text.to_string());
308
+ self
309
+ }
310
+ }
311
+
312
+ impl vllm_parser::unified::UnifiedParser for ScriptedParser {
313
+ fn create(
314
+ _tools: &[vllm_parser::tool::Tool],
315
+ _tokenizer: vllm_tokenizer::DynTokenizer,
316
+ ) -> vllm_parser::unified::Result<Box<dyn vllm_parser::unified::UnifiedParser>>
317
+ where
318
+ Self: Sized + 'static,
319
+ {
320
+ unreachable!("ScriptedParser is constructed directly in tests")
321
+ }
322
+
323
+ fn parse_into(
324
+ &mut self,
325
+ _delta: &str,
326
+ output: &mut UnifiedParserOutput,
327
+ ) -> vllm_parser::unified::Result<()> {
328
+ match self.steps.pop_front().expect("unexpected parser call") {
329
+ ScriptedStep::Output(next) => {
330
+ output.append(next);
331
+ Ok(())
332
+ }
333
+ ScriptedStep::Error {
334
+ committed,
335
+ reset_text,
336
+ } => {
337
+ output.append(committed);
338
+ self.reset_text = reset_text;
339
+ Err(UnifiedParserError::Reasoning(
340
+ ReasoningError::MissingToken {
341
+ token: "<think>".to_string(),
342
+ },
343
+ ))
344
+ }
345
+ }
346
+ }
347
+
348
+ fn tool_call_id(&self, _tool_index: usize) -> Option<&str> {
349
+ self.tool_call_id.as_deref()
350
+ }
351
+
352
+ fn finish(&mut self) -> vllm_parser::unified::Result<UnifiedParserOutput> {
353
+ if let Some(reset_text) = self.finish_error_reset_text.take() {
354
+ self.reset_text = reset_text;
355
+ return Err(UnifiedParserError::Reasoning(
356
+ ReasoningError::MissingToken {
357
+ token: "</tool_call>".to_string(),
358
+ },
359
+ ));
360
+ }
361
+ Ok(UnifiedParserOutput::default())
362
+ }
363
+
364
+ fn reset(&mut self) -> String {
365
+ std::mem::take(&mut self.reset_text)
366
+ }
367
+ }
368
+
369
+ fn decoded_delta(delta: &str) -> vllm_text::output::DecodedTextEvent {
370
+ vllm_text::output::DecodedTextEvent::TextDelta {
371
+ delta: delta.to_string(),
372
+ token_ids: Vec::new(),
373
+ logprobs: None,
374
+ finished: None,
375
+ }
376
+ }
377
+
378
+ fn finished_delta(delta: &str) -> vllm_text::output::DecodedTextEvent {
379
+ vllm_text::output::DecodedTextEvent::TextDelta {
380
+ delta: delta.to_string(),
381
+ token_ids: Vec::new(),
382
+ logprobs: None,
383
+ finished: Some(vllm_text::output::Finished {
384
+ usage: vllm_llm::TokenUsage::default(),
385
+ finish_reason: crate::FinishReason::Stop(None),
386
+ kv_transfer_params: None,
387
+ }),
388
+ }
389
+ }
390
+
391
+ async fn collect(
392
+ parser: ScriptedParser,
393
+ events: Vec<vllm_text::output::DecodedTextEvent>,
394
+ ) -> Vec<AssistantEvent> {
395
+ let stream = stream::iter(events.into_iter().map(Ok));
396
+ unified_event_stream(stream, Box::new(parser))
397
+ .collect::<Vec<_>>()
398
+ .await
399
+ .into_iter()
400
+ .collect::<crate::Result<Vec<_>>>()
401
+ .unwrap()
402
+ }
403
+
404
+ fn text(delta: &str) -> UnifiedParserOutput {
405
+ let mut output = UnifiedParserOutput::default();
406
+ output.push_text(delta.to_string());
407
+ output
408
+ }
409
+
410
+ fn reasoning(delta: &str) -> UnifiedParserOutput {
411
+ let mut output = UnifiedParserOutput::default();
412
+ output.push_reasoning(delta.to_string());
413
+ output
414
+ }
415
+
416
+ fn tool_call(name: &str, arguments: &str) -> UnifiedParserOutput {
417
+ UnifiedParserOutput {
418
+ events: vec![vllm_parser::unified::UnifiedParserEvent::ToolCall(
419
+ ToolCallDelta {
420
+ tool_index: 0,
421
+ name: Some(name.to_string()),
422
+ arguments: arguments.to_string(),
423
+ },
424
+ )],
425
+ }
426
+ }
427
+
428
+ fn combined(first: UnifiedParserOutput, second: UnifiedParserOutput) -> UnifiedParserOutput {
429
+ let mut output = first;
430
+ output.append(second);
431
+ output
432
+ }
433
+
434
+ #[tokio::test]
435
+ async fn unified_stream_emits_reasoning_only_deltas() {
436
+ let events = collect(
437
+ ScriptedParser::new([ScriptedStep::Output(reasoning("thinking"))]),
438
+ vec![decoded_delta("raw")],
439
+ )
440
+ .await;
441
+
442
+ assert_eq!(
443
+ events,
444
+ vec![AssistantEvent::TextDelta {
445
+ kind: AssistantBlockKind::Reasoning,
446
+ delta: "thinking".to_string(),
447
+ }]
448
+ );
449
+ }
450
+
451
+ #[tokio::test]
452
+ async fn unified_stream_emits_tool_only_deltas() {
453
+ let events = collect(
454
+ ScriptedParser::new([ScriptedStep::Output(tool_call(
455
+ "get_weather",
456
+ r#"{"location":"Paris"}"#,
457
+ ))]),
458
+ vec![decoded_delta("raw")],
459
+ )
460
+ .await;
461
+
462
+ assert_eq!(
463
+ events,
464
+ vec![
465
+ AssistantEvent::ToolCallStart {
466
+ id: "call_test".to_string(),
467
+ name: "get_weather".to_string(),
468
+ },
469
+ AssistantEvent::ToolCallArgumentsDelta {
470
+ delta: r#"{"location":"Paris"}"#.to_string(),
471
+ },
472
+ ]
473
+ );
474
+ }
475
+
476
+ #[tokio::test]
477
+ async fn unified_stream_emits_reasoning_followed_by_tool_call() {
478
+ let events = collect(
479
+ ScriptedParser::new([ScriptedStep::Output(combined(
480
+ reasoning("thinking"),
481
+ tool_call("get_weather", r#"{"location":"Paris"}"#),
482
+ ))]),
483
+ vec![decoded_delta("raw")],
484
+ )
485
+ .await;
486
+
487
+ assert_eq!(
488
+ events,
489
+ vec![
490
+ AssistantEvent::TextDelta {
491
+ kind: AssistantBlockKind::Reasoning,
492
+ delta: "thinking".to_string(),
493
+ },
494
+ AssistantEvent::ToolCallStart {
495
+ id: "call_test".to_string(),
496
+ name: "get_weather".to_string(),
497
+ },
498
+ AssistantEvent::ToolCallArgumentsDelta {
499
+ delta: r#"{"location":"Paris"}"#.to_string(),
500
+ },
501
+ ]
502
+ );
503
+ }
504
+
505
+ #[tokio::test]
506
+ async fn unified_stream_emits_visible_text_followed_by_tool_call() {
507
+ let events = collect(
508
+ ScriptedParser::new([ScriptedStep::Output(combined(
509
+ text("visible "),
510
+ tool_call("get_weather", r#"{"location":"Paris"}"#),
511
+ ))]),
512
+ vec![decoded_delta("raw")],
513
+ )
514
+ .await;
515
+
516
+ assert_eq!(
517
+ events,
518
+ vec![
519
+ AssistantEvent::TextDelta {
520
+ kind: AssistantBlockKind::Text,
521
+ delta: "visible ".to_string(),
522
+ },
523
+ AssistantEvent::ToolCallStart {
524
+ id: "call_test".to_string(),
525
+ name: "get_weather".to_string(),
526
+ },
527
+ AssistantEvent::ToolCallArgumentsDelta {
528
+ delta: r#"{"location":"Paris"}"#.to_string(),
529
+ },
530
+ ]
531
+ );
532
+ }
533
+
534
+ #[tokio::test]
535
+ async fn unified_stream_fallback_keeps_committed_output_and_disables_later_parsing() {
536
+ let events = collect(
537
+ ScriptedParser::new([ScriptedStep::Error {
538
+ committed: text("committed"),
539
+ reset_text: "buffered".to_string(),
540
+ }]),
541
+ vec![decoded_delta("bad"), decoded_delta("later")],
542
+ )
543
+ .await;
544
+
545
+ assert_eq!(
546
+ events,
547
+ vec![
548
+ AssistantEvent::TextDelta {
549
+ kind: AssistantBlockKind::Text,
550
+ delta: "committed".to_string(),
551
+ },
552
+ AssistantEvent::TextDelta {
553
+ kind: AssistantBlockKind::Text,
554
+ delta: "buffered".to_string(),
555
+ },
556
+ AssistantEvent::TextDelta {
557
+ kind: AssistantBlockKind::Text,
558
+ delta: "later".to_string(),
559
+ },
560
+ ]
561
+ );
562
+ }
563
+
564
+ #[tokio::test]
565
+ async fn unified_stream_finish_error_closes_parser_without_reset_text() {
566
+ let events = collect(
567
+ ScriptedParser::new([ScriptedStep::Output(UnifiedParserOutput::default())])
568
+ .with_finish_error("buffered"),
569
+ vec![finished_delta("")],
570
+ )
571
+ .await;
572
+
573
+ assert_eq!(
574
+ events,
575
+ vec![AssistantEvent::Done {
576
+ usage: vllm_llm::TokenUsage::default(),
577
+ finish_reason: crate::FinishReason::Stop(None),
578
+ kv_transfer_params: None,
579
+ }]
580
+ );
581
+ }
582
+}
rust/src/chat/src/output/mod.rs
+2
-52
@@ -2,7 +2,6 @@ use std::pin::Pin;
2
use std::sync::Arc;
3
4
use futures::Stream;
5
-use subenum::subenum;
5
use trait_set::trait_set;
6
use uuid::Uuid;
7
use vllm_llm::TokenUsage;
@@ -22,23 +21,19 @@ pub(crate) use harmony::validate_harmony_parser_overrides;
21
22
/// Internal assistant event before final assembly.
23
///
25
-/// - [`ContentEvent`]: subenum after reasoning parsing, carries only text content.
26
-/// - [`AssistantEvent`]: full event after tool parsing, adds tool-call variants.
27
-#[subenum(ContentEvent)]
24
+/// Unified parsing produces these events, and structured assembly consumes
25
+/// them to build public chat events.
26
#[derive(Debug, Clone, PartialEq)]
27
pub(crate) enum AssistantEvent {
30
- #[subenum(ContentEvent)]
28
Start {
29
prompt_token_ids: Arc<[u32]>,
30
prompt_logprobs: Option<DecodedPromptLogprobs>,
31
},
35
- #[subenum(ContentEvent)]
32
TextDelta {
33
kind: AssistantBlockKind,
34
delta: String,
35
},
36
/// Per-decoded-update sample metadata: logprobs and/or output token IDs.
41
- #[subenum(ContentEvent)]
37
LogprobsDelta {
38
logprobs: Option<DecodedLogprobs>,
39
token_ids: Vec<u32>,
@@ -48,7 +43,6 @@ pub(crate) enum AssistantEvent {
43
/// A delta for the arguments of the currently open tool call. Must follow a
44
/// `ToolCallStart`.
45
ToolCallArgumentsDelta { delta: String },
51
- #[subenum(ContentEvent)]
46
Done {
47
usage: TokenUsage,
48
finish_reason: FinishReason,
@@ -57,50 +51,6 @@ pub(crate) enum AssistantEvent {
51
},
52
}
53
60
-impl ContentEvent {
61
- /// Convert a [`DecodedTextEvent`] into one or more [`ContentEvent`] values
62
- /// by treating all text as plain (non-reasoning) content.
63
- fn from_decoded_plain_text(event: DecodedTextEvent) -> Vec<Self> {
64
- match event {
65
- DecodedTextEvent::Start {
66
- prompt_token_ids,
67
- prompt_logprobs,
68
- } => vec![Self::Start {
69
- prompt_token_ids,
70
- prompt_logprobs,
71
- }],
72
- DecodedTextEvent::TextDelta {
73
- delta,
74
- token_ids,
75
- logprobs,
76
- finished,
77
- } => {
78
- let mut events = Vec::new();
79
- if !delta.is_empty() {
80
- events.push(Self::TextDelta {
81
- kind: AssistantBlockKind::Text,
82
- delta,
83
- });
84
- }
85
- if logprobs.is_some() || !token_ids.is_empty() {
86
- events.push(Self::LogprobsDelta {
87
- logprobs,
88
- token_ids,
89
- });
90
- }
91
- if let Some(finished) = finished {
92
- events.push(Self::Done {
93
- usage: finished.usage,
94
- finish_reason: finished.finish_reason,
95
- kv_transfer_params: finished.kv_transfer_params,
96
- });
97
- }
98
- events
99
- }
100
- }
101
- }
102
-}
103
-
54
/// Boxed stream of decoded text events coming from [`vllm_text`].
55
pub type DynDecodedTextEventStream = Pin<Box<dyn Stream<Item = Result<DecodedTextEvent>> + Send>>;
56
/// Boxed stream of structured chat events exposed by [`crate::ChatLlm`].
rust/src/chat/src/parser/reasoning/mod.rs
+2
-2
@@ -2,7 +2,7 @@
2
3
use std::sync::LazyLock;
4
5
-pub use vllm_reasoning_parser::{
5
+pub use vllm_parser::reasoning::{
6
CohereCmdReasoningParser, DeepSeekR1ReasoningParser, DeepSeekV3ReasoningParser,
7
DeepSeekV4ReasoningParser, Gemma4ReasoningParser, Glm45ReasoningParser, KimiK2ReasoningParser,
8
KimiReasoningParser, MiniMaxM2ReasoningParser, MiniMaxM3ReasoningParser,
@@ -34,7 +34,7 @@ pub mod names {
34
35
/// Constructor signature for one registered reasoning parser implementation.
36
type ReasoningParserCreator =
37
- fn(DynTokenizer) -> vllm_reasoning_parser::Result<Box<dyn ReasoningParser>>;
37
+ fn(DynTokenizer) -> vllm_parser::reasoning::Result<Box<dyn ReasoningParser>>;
38
39
/// Registry and model matcher for reasoning parsers.
40
pub type ReasoningParserFactory = ParserFactory<ReasoningParserCreator>;
rust/src/chat/src/parser/tool/mod.rs
+3
-4
@@ -2,13 +2,12 @@
2
3
use std::sync::LazyLock;
4
5
-pub use vllm_tool_parser::{
5
+pub use vllm_parser::tool::{
6
DeepSeekV3ToolParser, DeepSeekV4ToolParser, DeepSeekV31ToolParser, DeepSeekV32ToolParser,
7
Gemma4ToolParser, Glm45MoeToolParser, Glm47MoeToolParser, Granite4ToolParser, HermesToolParser,
8
HyV3ToolParser, Internlm2ToolParser, KimiK2ToolParser, Llama3JsonToolParser,
9
MinimaxM2ToolParser, MinimaxM3ToolParser, MistralToolParser, Phi4MiniJsonToolParser,
10
- Qwen3CoderToolParser, Qwen3XmlToolParser, ToolCallDelta, ToolParser, ToolParserError,
11
- ToolParserOutput,
10
+ Qwen3CoderToolParser, Qwen3XmlToolParser, ToolParser, ToolParserError,
11
};
12
13
use crate::parser::ParserFactory;
@@ -41,7 +40,7 @@ pub mod names {
40
}
41
42
/// Constructor signature for one registered tool parser implementation.
44
-type ToolParserCreator = fn(&[ChatTool]) -> vllm_tool_parser::Result<Box<dyn ToolParser>>;
43
+type ToolParserCreator = fn(&[ChatTool]) -> vllm_parser::tool::Result<Box<dyn ToolParser>>;
44
45
/// Registry and model matcher for tool parsers.
46
pub type ToolParserFactory = ParserFactory<ToolParserCreator>;
rust/src/chat/src/parser/tool/tests.rs
+2
-2
@@ -1,6 +1,6 @@
1
-use vllm_tool_parser::Result;
1
+use vllm_parser::tool::{Result, ToolParserOutput};
2
3
-use super::{ToolParser, ToolParserFactory, ToolParserOutput, names};
3
+use super::{ToolParser, ToolParserFactory, names};
4
use crate::Error;
5
use crate::request::ChatTool;
6
rust/src/chat/src/request.rs
+1
-1
@@ -4,9 +4,9 @@ use llm_multimodal::ImageDetail;
4
use serde::{Deserialize, Serialize};
5
use serde_json::Value;
6
use vllm_engine_core_client::protocol::lora::LoraRequest;
7
+pub use vllm_parser::tool::Tool as ChatTool;
8
pub use vllm_text::SamplingParams;
9
use vllm_text::TextDecodeOptions;
9
-pub use vllm_tool_parser::Tool as ChatTool;
10
11
use crate::AssistantMessageExt;
12
use crate::error::{Error, Result};
rust/src/chat/tests/chat.rs
+1
-1
@@ -1180,7 +1180,7 @@ async fn chat_stream_parses_tool_calls_automatically() {
1180
),
1181
request_output(
1182
"chat-tool",
1183
- bytes_to_token_ids(
1183
+ bytes_with_special_stop_token(
1184
b"\"arguments\":{\"city\":\"Paris\"}}\n</tool_call>",
1185
),
1186
Some(EngineCoreFinishReason::Stop),
rust/src/parser/Cargo.toml
renamed
+2
-1
@@ -1,5 +1,5 @@
1
[package]
2
-name = "vllm-tool-parser"
2
+name = "vllm-parser"
3
version.workspace = true
4
edition.workspace = true
5
license.workspace = true
@@ -13,6 +13,7 @@ serde.workspace = true
13
serde_json.workspace = true
14
thiserror.workspace = true
15
thiserror-ext.workspace = true
16
+vllm-tokenizer.workspace = true
17
winnow.workspace = true
18
xgrammar-structural-tag.workspace = true
19
rust/src/parser/benches/deepseek_v3.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::DeepSeekParser as ExternalDeepSeekParser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{DeepSeekV3ToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{DeepSeekV3ToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/deepseek_v31.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::DeepSeek31Parser as ExternalDeepSeek31Parser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{DeepSeekV31ToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{DeepSeekV31ToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/deepseek_v32.rs
renamed
+2
-2
@@ -1,8 +1,8 @@
1
use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
5
-use vllm_tool_parser::{DeepSeekV32ToolParser, Tool, ToolParser};
4
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
5
+use vllm_parser::tool::{DeepSeekV32ToolParser, Tool, ToolParser};
6
7
mod utils;
8
use utils::feed_parser;
rust/src/parser/benches/gemma4.rs
renamed
+2
-2
@@ -1,8 +1,8 @@
1
use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
5
-use vllm_tool_parser::{Gemma4ToolParser, Tool, ToolParser};
4
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
5
+use vllm_parser::tool::{Gemma4ToolParser, Tool, ToolParser};
6
7
mod utils;
8
use utils::feed_parser;
rust/src/parser/benches/glm45_moe.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::Glm4MoeParser as ExternalGlm4MoeParser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{Glm45MoeToolParser, Glm47MoeToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{Glm45MoeToolParser, Glm47MoeToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/kimi_k2.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::KimiK2Parser as ExternalKimiK2Parser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{KimiK2ToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{KimiK2ToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/llama3_json.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::LlamaParser as ExternalLlamaParser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{Llama3JsonToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{Llama3JsonToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/minimax_m2.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::MinimaxM2Parser as ExternalMinimaxM2Parser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{MinimaxM2ToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{MinimaxM2ToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/qwen3_coder.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::QwenCoderParser as ExternalQwenCoderParser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{Qwen3CoderToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{Qwen3CoderToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/qwen3_xml.rs
renamed
+2
-2
@@ -2,8 +2,8 @@ use std::time::Duration;
2
3
use criterion::{BatchSize, Criterion, Throughput, black_box, criterion_group, criterion_main};
4
use tool_parser::parsers::QwenParser as ExternalQwenParser;
5
-use vllm_tool_parser::test_utils::{split_by_chars, test_tools};
6
-use vllm_tool_parser::{Qwen3XmlToolParser, Tool, ToolParser};
5
+use vllm_parser::tool::test_utils::{split_by_chars, test_tools};
6
+use vllm_parser::tool::{Qwen3XmlToolParser, Tool, ToolParser};
7
8
mod utils;
9
use utils::{feed_external_parser, feed_parser, openai_tools};
rust/src/parser/benches/utils/mod.rs
renamed
+2
-2
@@ -3,8 +3,8 @@
3
use futures::FutureExt as _;
4
use openai_protocol::common::{Function as OpenAiFunction, Tool as OpenAiTool};
5
use tool_parser::traits::ToolParser as ExternalToolParser;
6
-use vllm_tool_parser::test_utils::collect_stream;
7
-use vllm_tool_parser::{Tool, ToolParser};
6
+use vllm_parser::tool::test_utils::collect_stream;
7
+use vllm_parser::tool::{Tool, ToolParser};
8
9
pub(super) fn openai_tools(tools: &[Tool]) -> Vec<OpenAiTool> {
10
tools
rust/src/parser/python/Cargo.toml
renamed
+1
-1
@@ -13,7 +13,7 @@ pyo3.workspace = true
13
pythonize = { workspace = true, features = ["serde_json"] }
14
serde_json.workspace = true
15
thiserror-ext.workspace = true
16
-vllm-tool-parser.workspace = true
16
+vllm-parser.workspace = true
17
18
[lints]
19
workspace = true
rust/src/parser/python/src/lib.rs
renamed
+3
-3
@@ -1,4 +1,4 @@
1
-//! Thin PyO3 bindings for `vllm_tool_parser`.
1
+//! Thin PyO3 bindings for `vllm_parser::tool`.
2
//!
3
//! This crate exposes the Rust tool parser trait and data shapes to Python
4
//! while keeping parser state, grammar, and schema-aware argument conversion in
@@ -11,7 +11,7 @@ use pyo3::types::{PyAny, PyModule};
11
use pythonize::{depythonize, pythonize};
12
use serde_json::Value;
13
use thiserror_ext::AsReport as _;
14
-use vllm_tool_parser::{Tool, ToolCallDelta, ToolParser, ToolParserOutput};
14
+use vllm_parser::tool::{Tool, ToolCallDelta, ToolParser, ToolParserOutput};
15
16
macro_rules! tool_parser_factory {
17
($($parser:ident),+ $(,)?) => {
@@ -22,7 +22,7 @@ macro_rules! tool_parser_factory {
22
match name {
23
$(
24
stringify!($parser) => {
25
- <vllm_tool_parser::$parser as ToolParser>::create(tools)
25
+ <vllm_parser::tool::$parser as ToolParser>::create(tools)
26
}
27
)+
28
_ => {
rust/src/parser/src/lib.rs
new
+5
@@ -0,0 +1,5 @@
1
+//! Streaming parsers for chat completions.
2
+
3
+pub mod reasoning;
4
+pub mod tool;
5
+pub mod unified;
rust/src/parser/src/reasoning/cohere_cmd.rs
renamed
rust/src/parser/src/reasoning/deepseek_r1.rs
renamed
rust/src/parser/src/reasoning/delimited.rs
renamed
rust/src/parser/src/reasoning/gemma4.rs
renamed
+1
-1
@@ -119,7 +119,7 @@ mod tests {
119
use vllm_tokenizer::Tokenizer;
120
121
use super::Gemma4ReasoningParser;
122
- use crate::ReasoningParser;
122
+ use crate::reasoning::ReasoningParser;
123
124
struct FakeTokenizer;
125
rust/src/parser/src/reasoning/kimi.rs
renamed
rust/src/parser/src/reasoning/minimax_m3.rs
renamed
rust/src/parser/src/reasoning/mod.rs
renamed
rust/src/parser/src/reasoning/qwen3.rs
renamed
rust/src/parser/src/reasoning/seed_oss.rs
renamed
+1
-1
@@ -49,7 +49,7 @@ mod tests {
49
use std::sync::Arc;
50
51
use super::SeedOssReasoningParser;
52
- use crate::{ReasoningParser, tests::FakeTokenizer};
52
+ use crate::reasoning::{ReasoningParser, tests::FakeTokenizer};
53
54
#[test]
55
fn without_prompt_markers_expects_start_token() {
rust/src/parser/src/reasoning/step3p5.rs
renamed
+1
-1
@@ -127,7 +127,7 @@ mod tests {
127
use std::sync::Arc;
128
129
use super::Step3p5ReasoningParser;
130
- use crate::{ReasoningParser, tests::FakeTokenizer};
130
+ use crate::reasoning::{ReasoningParser, tests::FakeTokenizer};
131
132
#[test]
133
fn picks_up_prompt_start_boundary() {
rust/src/parser/src/reasoning/tests.rs
renamed
rust/src/parser/src/tool/deepseek_dsml/deepseek_v32.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{DeepSeekDsmlToolParser, DsmlTokens};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
/// Tool parser for DeepSeek V3.2 models.
5
///
@@ -67,8 +67,8 @@ mod tests {
67
use thiserror_ext::AsReport;
68
69
use super::DeepSeekV32ToolParser;
70
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
71
- use crate::{ToolParser, ToolParserTestExt as _};
70
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
71
+ use crate::tool::{ToolParser, ToolParserTestExt as _};
72
73
fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
74
let params = params
rust/src/parser/src/tool/deepseek_dsml/deepseek_v4.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{DeepSeekDsmlToolParser, DsmlTokens};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
/// Tool parser for DeepSeek V4 models.
5
///
@@ -69,8 +69,8 @@ mod tests {
69
use serde_json::{Value, json};
70
71
use super::DeepSeekV4ToolParser;
72
- use crate::test_utils::{collect_stream, test_tools};
73
- use crate::{StructuralTagModel, ToolParser, ToolParserTestExt as _};
72
+ use crate::tool::test_utils::{collect_stream, test_tools};
73
+ use crate::tool::{StructuralTagModel, ToolParser, ToolParserTestExt as _};
74
75
fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
76
let params = params
rust/src/parser/src/tool/deepseek_dsml/mod.rs
renamed
+1
-1
@@ -7,7 +7,7 @@ use winnow::token::{literal, rest, take_until};
7
use super::parameters::ToolSchemas;
8
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
9
use super::{Result, ToolCallDelta, ToolParserOutput};
10
-use crate::Tool;
10
+use crate::tool::Tool;
11
12
mod deepseek_v32;
13
mod deepseek_v4;
rust/src/parser/src/tool/deepseek_json/deepseek_v3.rs
renamed
+4
-4
@@ -1,5 +1,5 @@
1
use super::{DeepSeekJsonFormat, DeepSeekJsonToolParser};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
/// Tool parser for DeepSeek V3 JSON-fenced tool calls.
5
///
@@ -55,12 +55,12 @@ mod tests {
55
use thiserror_ext::AsReport;
56
57
use super::DeepSeekV3ToolParser;
58
- use crate::deepseek_json::{
58
+ use crate::tool::deepseek_json::{
59
TOOL_CALL_SEPARATOR, TOOL_CALL_START, TOOL_CALLS_END, TOOL_CALLS_START, V3_ARGUMENT_END,
60
V3_JSON_START,
61
};
62
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
63
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
62
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
63
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
64
65
fn v3_tool_call(function_name: &str, arguments: &str) -> String {
66
format!(
rust/src/parser/src/tool/deepseek_json/deepseek_v31.rs
renamed
+4
-4
@@ -1,5 +1,5 @@
1
use super::{DeepSeekJsonFormat, DeepSeekJsonToolParser};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
/// Tool parser for DeepSeek V3.1 raw JSON tool calls.
5
///
@@ -51,11 +51,11 @@ mod tests {
51
use thiserror_ext::AsReport;
52
53
use super::DeepSeekV31ToolParser;
54
- use crate::deepseek_json::{
54
+ use crate::tool::deepseek_json::{
55
TOOL_CALL_END, TOOL_CALL_SEPARATOR, TOOL_CALL_START, TOOL_CALLS_END, TOOL_CALLS_START,
56
};
57
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
58
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
57
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
58
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
59
60
fn v31_tool_call(function_name: &str, arguments: &str) -> String {
61
format!("{TOOL_CALL_START}{function_name}{TOOL_CALL_SEPARATOR}{arguments}{TOOL_CALL_END}")
rust/src/parser/src/tool/deepseek_json/mod.rs
renamed
rust/src/parser/src/tool/error.rs
renamed
+1
-1
@@ -6,7 +6,7 @@ pub type Result<T> = std::result::Result<T, ToolParserError>;
6
7
/// Errors produced while creating or running tool parsers.
8
#[derive(Debug, Error, Macro)]
9
-#[thiserror_ext(macro(path = "crate::error"))]
9
+#[thiserror_ext(macro(path = "crate::tool::error"))]
10
pub enum ToolParserError {
11
#[error("tool parser parsing failed: {message}")]
12
ParsingFailed { message: String },
rust/src/parser/src/tool/gemma4.rs
renamed
+2
-2
@@ -8,7 +8,7 @@ use winnow::token::{literal, take_till, take_until};
8
9
use super::utils::{incomplete, parse_buffered_event, partial_prefix_len, safe_text_len};
10
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
11
-use crate::Tool;
11
+use crate::tool::Tool;
12
13
const TOOL_CALL_START: &str = "<|tool_call>";
14
const TOOL_CALL_END: &str = "<tool_call|>";
@@ -428,7 +428,7 @@ mod tests {
428
Gemma4ToolParser, ToolCallDelta, ToolParser, ToolParserOutput, gemma4_array_content,
429
parse_gemma4_args,
430
};
431
- use crate::{Tool, ToolParserTestExt as _};
431
+ use crate::tool::{Tool, ToolParserTestExt as _};
432
433
fn parse_gemma4_array(array: &str) -> super::Result<Vec<Value>> {
434
let mut input = array;
rust/src/parser/src/tool/glm_xml/glm45_moe.rs
renamed
+1
-1
@@ -1,5 +1,5 @@
1
use super::{GlmXmlToolParser, Separator};
2
-use crate::{Result, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, Tool, ToolParser, ToolParserOutput};
3
4
/// Tool parser for GLM-4.5/4.6 MoE XML-style tool calls.
5
///
rust/src/parser/src/tool/glm_xml/glm47_moe.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{GlmXmlToolParser, Separator};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
/// Tool parser for GLM-4.7 MoE XML-style tool calls.
5
///
@@ -44,8 +44,8 @@ mod tests {
44
use serde_json::{Value, json};
45
46
use super::Glm47MoeToolParser;
47
- use crate::ToolParserTestExt as _;
48
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
47
+ use crate::tool::ToolParserTestExt as _;
48
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
49
50
fn glm47_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
51
let params = params
rust/src/parser/src/tool/glm_xml/mod.rs
renamed
+3
-3
@@ -7,7 +7,7 @@ use winnow::token::{literal, rest, take_until, take_while};
7
use super::parameters::ToolSchemas;
8
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
9
use super::{Result, ToolCallDelta, ToolParserOutput};
10
-use crate::Tool;
10
+use crate::tool::Tool;
11
12
mod glm45_moe;
13
mod glm47_moe;
@@ -264,8 +264,8 @@ mod tests {
264
use thiserror_ext::AsReport;
265
266
use super::Glm45MoeToolParser;
267
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
268
- use crate::{ToolParser, ToolParserTestExt as _};
267
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
268
+ use crate::tool::{ToolParser, ToolParserTestExt as _};
269
270
fn glm45_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
271
let params = params
rust/src/parser/src/tool/hy_v3.rs
renamed
+3
-3
@@ -7,7 +7,7 @@ use winnow::token::{literal, rest, take_until};
7
use super::parameters::ToolSchemas;
8
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
9
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
10
-use crate::{StructuralTagModel, Tool};
10
+use crate::tool::{StructuralTagModel, Tool};
11
12
const TOOL_CALLS_START: &str = "<tool_calls>";
13
const TOOL_CALLS_END: &str = "</tool_calls>";
@@ -249,8 +249,8 @@ mod tests {
249
use thiserror_ext::AsReport;
250
251
use super::{HyV3ToolParser, ToolParser};
252
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
253
- use crate::{ToolParserOutput, ToolParserTestExt as _};
252
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
253
+ use crate::tool::{ToolParserOutput, ToolParserTestExt as _};
254
255
fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
256
let params = params
rust/src/parser/src/tool/json/granite4.rs
renamed
+4
-4
@@ -8,10 +8,10 @@ use super::{
8
JsonToolCallConfig, JsonToolCallEvent, JsonToolCallWhitespace, JsonToolInput,
9
tool_call_header_event,
10
};
11
-use crate::utils::{
11
+use crate::tool::utils::{
12
JsonObjectScanState, json_str, parse_buffered_event, safe_text_len, take_json_object,
13
};
14
-use crate::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
14
+use crate::tool::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
15
16
const TOOL_CALL_START: &str = "<tool_call>";
17
const TOOL_CALL_END: &str = "</tool_call>";
@@ -279,8 +279,8 @@ mod tests {
279
use thiserror_ext::AsReport;
280
281
use super::Granite4ToolParser;
282
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
283
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
282
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
283
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
284
285
#[test]
286
fn granite4_parse_complete_without_tool_call_keeps_text() {
rust/src/parser/src/tool/json/hermes.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
const HERMES_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5
parser_name: "Hermes",
@@ -68,8 +68,8 @@ mod tests {
68
use thiserror_ext::AsReport;
69
70
use super::HermesToolParser;
71
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
72
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
71
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
72
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
73
74
fn build_tool_call(function_name: &str, arguments: &str) -> String {
75
format!(r#"<tool_call>{{"name":"{function_name}","arguments":{arguments}}}</tool_call>"#)
rust/src/parser/src/tool/json/internlm2.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2
-use crate::{Result, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, Tool, ToolParser, ToolParserOutput};
3
4
const INTERNLM2_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5
parser_name: "InternLM2",
@@ -123,8 +123,8 @@ mod tests {
123
use thiserror_ext::AsReport;
124
125
use super::Internlm2ToolParser;
126
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
127
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
126
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
127
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
128
129
const ACTION_START: &str = "<|action_start|><|plugin|>";
130
const ACTION_END: &str = "<|action_end|>";
rust/src/parser/src/tool/json/llama.rs
renamed
+4
-4
@@ -8,8 +8,8 @@ use super::{
8
JsonToolCallConfig, JsonToolCallEvent, JsonToolCallWhitespace, JsonToolInput,
9
argument_delta_event, tool_call_header_event,
10
};
11
-use crate::utils::{JsonObjectScanState, parse_buffered_event};
12
-use crate::{Result, StructuralTagModel, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
11
+use crate::tool::utils::{JsonObjectScanState, parse_buffered_event};
12
+use crate::tool::{Result, StructuralTagModel, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
13
14
#[derive(Debug, Clone, PartialEq, Eq)]
15
enum LlamaJsonMode {
@@ -256,8 +256,8 @@ mod tests {
256
use thiserror_ext::AsReport;
257
258
use super::Llama3JsonToolParser;
259
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
260
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
259
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
260
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
261
262
fn build_tool_call(function_name: &str, parameters: &str) -> String {
263
format!(r#"{{"name":"{function_name}","parameters":{parameters}}}"#)
rust/src/parser/src/tool/json/mistral.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2
-use crate::{Result, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, Tool, ToolParser, ToolParserOutput};
3
4
const MISTRAL_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5
parser_name: "Mistral",
@@ -61,8 +61,8 @@ mod tests {
61
use thiserror_ext::AsReport;
62
63
use super::MistralToolParser;
64
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
65
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
64
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
65
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
66
67
fn build_tool_call(function_name: &str, arguments: &str) -> String {
68
format!(r#"{{"name":"{function_name}","arguments":{arguments}}}"#)
rust/src/parser/src/tool/json/mod.rs
renamed
+1
-1
@@ -374,7 +374,7 @@ mod tests {
374
use expect_test::expect;
375
376
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
377
- use crate::ToolParserOutput;
377
+ use crate::tool::ToolParserOutput;
378
379
const DELIMITED_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
380
parser_name: "Delimited JSON",
rust/src/parser/src/tool/json/phi4mini.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2
-use crate::{Result, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, Tool, ToolParser, ToolParserOutput};
3
4
const PHI4MINI_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5
parser_name: "Phi4Mini",
@@ -69,8 +69,8 @@ mod tests {
69
use thiserror_ext::AsReport;
70
71
use super::Phi4MiniJsonToolParser;
72
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
73
- use crate::{ToolParser, ToolParserTestExt as _};
72
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
73
+ use crate::tool::{ToolParser, ToolParserTestExt as _};
74
75
/// Build one phi-4-mini tool-call object: `{"name":..,"<args_key>":<args>}`.
76
fn build_call(function_name: &str, args_key: &str, arguments: &str) -> String {
rust/src/parser/src/tool/json/qwen.rs
renamed
+3
-3
@@ -1,5 +1,5 @@
1
use super::{JsonToolCallConfig, JsonToolCallParser, JsonToolCallWhitespace};
2
-use crate::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
2
+use crate::tool::{Result, StructuralTagModel, Tool, ToolParser, ToolParserOutput};
3
4
const QWEN_XML_CONFIG: JsonToolCallConfig = JsonToolCallConfig {
5
parser_name: "Qwen XML",
@@ -70,8 +70,8 @@ mod tests {
70
use thiserror_ext::AsReport;
71
72
use super::Qwen3XmlToolParser;
73
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
74
- use crate::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
73
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
74
+ use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
75
76
fn build_tool_call(function_name: &str, arguments: &str) -> String {
77
format!(
rust/src/parser/src/tool/kimi_k2.rs
renamed
+3
-3
@@ -8,7 +8,7 @@ use winnow::token::{literal, rest, take_until, take_while};
8
9
use super::utils::{JsonObjectScanState, parse_buffered_event, safe_text_len, take_json_object};
10
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
11
-use crate::{StructuralTagModel, Tool};
11
+use crate::tool::{StructuralTagModel, Tool};
12
13
const TOOL_CALLS_START: &str = "<|tool_calls_section_begin|>";
14
const TOOL_CALLS_END: &str = "<|tool_calls_section_end|>";
@@ -339,8 +339,8 @@ mod tests {
339
KimiK2ToolParser, TOOL_CALL_ARGUMENT_START, TOOL_CALL_END, TOOL_CALL_START, TOOL_CALLS_END,
340
TOOL_CALLS_START, ToolParser, tool_header,
341
};
342
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
343
- use crate::{ToolParserOutput, ToolParserTestExt as _};
342
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
343
+ use crate::tool::{ToolParserOutput, ToolParserTestExt as _};
344
345
fn build_tool_call(function_name: &str, index: usize, arguments: &str) -> String {
346
format!(
rust/src/parser/src/tool/minimax_m2.rs
renamed
+3
-3
@@ -7,7 +7,7 @@ use winnow::token::{literal, rest, take_until};
7
use super::parameters::ToolSchemas;
8
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
9
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
10
-use crate::{StructuralTagModel, Tool};
10
+use crate::tool::{StructuralTagModel, Tool};
11
12
const TOOL_CALL_START: &str = "<minimax:tool_call>";
13
const TOOL_CALL_END: &str = "</minimax:tool_call>";
@@ -272,8 +272,8 @@ mod tests {
272
use thiserror_ext::AsReport;
273
274
use super::{MinimaxM2ToolParser, TOOL_CALL_END, TOOL_CALL_START, ToolParser};
275
- use crate::ToolParserTestExt as _;
276
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
275
+ use crate::tool::ToolParserTestExt as _;
276
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
277
278
fn build_tool_block(invokes: &[(&str, Vec<(&str, &str)>)]) -> String {
279
let invokes = invokes
rust/src/parser/src/tool/minimax_m3.rs
renamed
+3
-3
@@ -8,7 +8,7 @@ use winnow::token::{literal, rest, take_until};
8
use super::parameters::{ParamElement, ParamInput, ToolSchemas};
9
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
10
use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
11
-use crate::Tool;
11
+use crate::tool::Tool;
12
13
const NAMESPACE: &str = "]<]minimax[>[";
14
const TOOL_CALL_START: &str = "]<]minimax[>[<tool_call>";
@@ -388,8 +388,8 @@ mod tests {
388
ELEMENT_END_START, ELEMENT_START, INVOKE_END, INVOKE_START, MinimaxM3ToolParser,
389
TOOL_CALL_END, TOOL_CALL_START, ToolParser,
390
};
391
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
392
- use crate::{Tool, ToolParserTestExt as _};
391
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
392
+ use crate::tool::{Tool, ToolParserTestExt as _};
393
394
fn element(name: &str, body: &str) -> String {
395
format!("{ELEMENT_START}{name}>{body}{ELEMENT_END_START}{name}>")
rust/src/parser/src/tool/mod.rs
renamed
+3
-3
@@ -1,9 +1,9 @@
1
//! Streaming tool parsers for chat completions.
2
3
#[macro_use]
4
-mod error;
4
+pub(crate) mod error;
5
mod deepseek_dsml;
6
-mod deepseek_json;
6
+pub(crate) mod deepseek_json;
7
mod gemma4;
8
mod glm_xml;
9
mod hy_v3;
@@ -15,7 +15,7 @@ mod parameters;
15
mod qwen_coder;
16
#[cfg(any(test, feature = "test-util"))]
17
pub mod test_utils;
18
-mod utils;
18
+pub(crate) mod utils;
19
20
use std::collections::{BTreeMap, btree_map};
21
rust/src/parser/src/tool/parameters.rs
renamed
+2
-2
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
2
3
use serde_json::{Map, Number, Value};
4
5
-use crate::Tool;
5
+use crate::tool::Tool;
6
7
/// Normalized parameter schemas for all tools in one request.
8
#[derive(Debug, Clone, Default, PartialEq, Eq)]
@@ -416,7 +416,7 @@ mod tests {
416
use serde_json::{Value, json};
417
418
use super::{ParamElement, ParamInput, ToolSchema, ToolSchemas};
419
- use crate::Tool;
419
+ use crate::tool::Tool;
420
421
fn test_tool(name: &str, parameters: serde_json::Value) -> Tool {
422
Tool {
rust/src/parser/src/tool/qwen_coder.rs
renamed
+3
-3
@@ -7,7 +7,7 @@ use winnow::token::{literal, take_until};
7
use super::parameters::ToolSchemas;
8
use super::utils::{MarkerScanState, parse_buffered_event, safe_text_len, take_until_marker};
9
use super::{Result, StructuralTagModel, ToolCallDelta, ToolParser, ToolParserOutput};
10
-use crate::Tool;
10
+use crate::tool::Tool;
11
12
const TOOL_CALL_START: &str = "<tool_call>";
13
const TOOL_CALL_END: &str = "</tool_call>";
@@ -241,8 +241,8 @@ mod tests {
241
use thiserror_ext::AsReport;
242
243
use super::{Qwen3CoderToolParser, StructuralTagModel, ToolParser};
244
- use crate::test_utils::{collect_stream, split_by_chars, test_tools};
245
- use crate::{ToolParserOutput, ToolParserTestExt as _};
244
+ use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
245
+ use crate::tool::{ToolParserOutput, ToolParserTestExt as _};
246
247
fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
248
let params = params
rust/src/parser/src/tool/test_utils.rs
renamed
+1
-1
@@ -1,7 +1,7 @@
1
use serde_json::json;
2
3
use super::{ToolParser, ToolParserOutput};
4
-use crate::{Tool, ToolParserTestExt as _};
4
+use crate::tool::{Tool, ToolParserTestExt as _};
5
6
/// Build a reusable set of function tools for parser unit tests.
7
pub fn test_tools() -> Vec<Tool> {
rust/src/parser/src/tool/tests.rs
renamed
+1
-1
@@ -1,5 +1,5 @@
1
use super::{Result, Tool, ToolCallDelta, ToolParser, ToolParserOutput};
2
-use crate::ToolParserTestExt as _;
2
+use crate::tool::ToolParserTestExt as _;
3
4
struct DefaultParser;
5
rust/src/parser/src/tool/utils.rs
renamed
rust/src/parser/src/unified/combined.rs
new
+346
@@ -0,0 +1,346 @@
1
+//! Adapter that combines reasoning and tool parsers.
2
+
3
+use vllm_tokenizer::DynTokenizer;
4
+
5
+use crate::reasoning::ReasoningParser;
6
+use crate::tool::{StructuralTagModel, Tool, ToolParser, ToolParserOutput};
7
+
8
+use super::{Result, UnifiedParser, UnifiedParserError, UnifiedParserOutput};
9
+
10
+/// Unified parser that composes existing reasoning and tool parsers.
11
+pub struct CombinedParser {
12
+ reasoning: Option<Box<dyn ReasoningParser>>,
13
+ tool: Option<Box<dyn ToolParser>>,
14
+}
15
+
16
+impl CombinedParser {
17
+ /// Create a combined parser from optional reasoning and tool parsers.
18
+ pub fn new(
19
+ reasoning: Option<Box<dyn ReasoningParser>>,
20
+ tool: Option<Box<dyn ToolParser>>,
21
+ ) -> Self {
22
+ Self { reasoning, tool }
23
+ }
24
+
25
+ /// Create a text-only combined parser.
26
+ pub fn plain_text_only() -> Self {
27
+ Self {
28
+ reasoning: None,
29
+ tool: None,
30
+ }
31
+ }
32
+
33
+ fn parse_tool(&mut self, content: &str, output: &mut UnifiedParserOutput) -> Result<()> {
34
+ let Some(tool) = self.tool.as_mut() else {
35
+ output.push_text(content.to_string());
36
+ return Ok(());
37
+ };
38
+
39
+ // Preserve any tool output that was already produced before the error.
40
+ let mut tool_output = ToolParserOutput::default();
41
+ let result = tool.parse_into(content, &mut tool_output);
42
+ output.append_tool_output(tool_output);
43
+ result?;
44
+
45
+ Ok(())
46
+ }
47
+
48
+ fn flush_tool(&mut self) -> Result<UnifiedParserOutput> {
49
+ let Some(tool) = self.tool.as_mut() else {
50
+ return Ok(UnifiedParserOutput::default());
51
+ };
52
+
53
+ let output = tool.finish()?;
54
+ let mut unified = UnifiedParserOutput::default();
55
+ unified.append_tool_output(output);
56
+ Ok(unified)
57
+ }
58
+}
59
+
60
+impl UnifiedParser for CombinedParser {
61
+ fn create(_tools: &[Tool], _tokenizer: DynTokenizer) -> Result<Box<dyn UnifiedParser>>
62
+ where
63
+ Self: Sized + 'static,
64
+ {
65
+ Err(UnifiedParserError::CombinedParserConstructor)
66
+ }
67
+
68
+ fn initialize(&mut self, prompt_token_ids: &[u32]) -> Result<()> {
69
+ if let Some(reasoning) = self.reasoning.as_mut() {
70
+ reasoning.initialize(prompt_token_ids)?;
71
+ }
72
+ Ok(())
73
+ }
74
+
75
+ fn preserve_special_tokens(&self) -> bool {
76
+ self.reasoning.as_ref().is_some_and(|parser| parser.preserve_special_tokens())
77
+ || self.tool.as_ref().is_some_and(|parser| parser.preserve_special_tokens())
78
+ }
79
+
80
+ fn structural_tag_model(&self) -> Option<StructuralTagModel> {
81
+ self.tool.as_ref().and_then(|parser| parser.structural_tag_model())
82
+ }
83
+
84
+ fn tool_call_id(&self, tool_index: usize) -> Option<&str> {
85
+ self.tool.as_ref().and_then(|parser| parser.tool_call_id(tool_index))
86
+ }
87
+
88
+ fn parse_into(&mut self, delta: &str, output: &mut UnifiedParserOutput) -> Result<()> {
89
+ let Some(reasoning) = self.reasoning.as_mut() else {
90
+ return self.parse_tool(delta, output);
91
+ };
92
+
93
+ let reasoning_delta = reasoning.push(delta)?;
94
+ if let Some(reasoning) = reasoning_delta.reasoning {
95
+ output.push_reasoning(reasoning);
96
+ }
97
+ if let Some(content) = reasoning_delta.content {
98
+ self.parse_tool(&content, output)?;
99
+ }
100
+ Ok(())
101
+ }
102
+
103
+ fn finish(&mut self) -> Result<UnifiedParserOutput> {
104
+ let mut output = UnifiedParserOutput::default();
105
+ if let Some(reasoning) = self.reasoning.as_mut() {
106
+ let reasoning_delta = reasoning.finish()?;
107
+ if let Some(reasoning) = reasoning_delta.reasoning {
108
+ output.push_reasoning(reasoning);
109
+ }
110
+ if let Some(content) = reasoning_delta.content {
111
+ self.parse_tool(&content, &mut output)?;
112
+ }
113
+ }
114
+ output.append(self.flush_tool()?);
115
+ Ok(output)
116
+ }
117
+
118
+ fn reset(&mut self) -> String {
119
+ self.tool.as_mut().map_or_else(String::new, |parser| parser.reset())
120
+ }
121
+}
122
+
123
+#[cfg(test)]
124
+mod tests {
125
+ use std::sync::Arc;
126
+
127
+ use vllm_tokenizer::Tokenizer;
128
+
129
+ use super::CombinedParser;
130
+ use crate::reasoning::{Qwen3ReasoningParser, ReasoningDelta, ReasoningParser};
131
+ use crate::tool::{Qwen3XmlToolParser, Tool, ToolParser};
132
+ use crate::unified::{UnifiedParser, UnifiedParserEvent, UnifiedParserOutput};
133
+
134
+ struct FakeTokenizer;
135
+
136
+ impl Tokenizer for FakeTokenizer {
137
+ fn encode(
138
+ &self,
139
+ text: &str,
140
+ _add_special_tokens: bool,
141
+ ) -> vllm_tokenizer::Result<Vec<u32>> {
142
+ Ok(text.chars().map(u32::from).collect())
143
+ }
144
+
145
+ fn decode(
146
+ &self,
147
+ token_ids: &[u32],
148
+ _skip_special_tokens: bool,
149
+ ) -> vllm_tokenizer::Result<String> {
150
+ Ok(token_ids
151
+ .iter()
152
+ .map(|token_id| char::from_u32(*token_id).unwrap_or('\u{FFFD}'))
153
+ .collect())
154
+ }
155
+
156
+ fn token_to_id(&self, token: &str) -> Option<u32> {
157
+ match token {
158
+ "<think>" => Some(1),
159
+ "</think>" => Some(2),
160
+ _ => None,
161
+ }
162
+ }
163
+ }
164
+
165
+ fn test_tools() -> Vec<Tool> {
166
+ vec![Tool {
167
+ name: "get_weather".to_string(),
168
+ description: None,
169
+ parameters: serde_json::json!({
170
+ "type": "object",
171
+ "properties": {
172
+ "location": { "type": "string" }
173
+ },
174
+ }),
175
+ strict: None,
176
+ }]
177
+ }
178
+
179
+ fn collect(parser: &mut dyn UnifiedParser, chunks: &[&str]) -> UnifiedParserOutput {
180
+ let mut output = UnifiedParserOutput::default();
181
+ for chunk in chunks {
182
+ parser.parse_into(chunk, &mut output).unwrap();
183
+ }
184
+ output.append(parser.finish().unwrap());
185
+ output
186
+ }
187
+
188
+ struct PreserveReasoningParser;
189
+
190
+ impl ReasoningParser for PreserveReasoningParser {
191
+ fn create(
192
+ _tokenizer: vllm_tokenizer::DynTokenizer,
193
+ ) -> crate::reasoning::Result<Box<dyn ReasoningParser>>
194
+ where
195
+ Self: Sized + 'static,
196
+ {
197
+ Ok(Box::new(Self))
198
+ }
199
+
200
+ fn preserve_special_tokens(&self) -> bool {
201
+ true
202
+ }
203
+
204
+ fn push(&mut self, delta: &str) -> crate::reasoning::Result<ReasoningDelta> {
205
+ Ok(ReasoningDelta {
206
+ reasoning: None,
207
+ content: Some(delta.to_string()),
208
+ })
209
+ }
210
+ }
211
+
212
+ struct PreserveToolParser;
213
+
214
+ impl ToolParser for PreserveToolParser {
215
+ fn create(_tools: &[Tool]) -> crate::tool::Result<Box<dyn ToolParser>>
216
+ where
217
+ Self: Sized + 'static,
218
+ {
219
+ Ok(Box::new(Self))
220
+ }
221
+
222
+ fn preserve_special_tokens(&self) -> bool {
223
+ true
224
+ }
225
+
226
+ fn parse_into(
227
+ &mut self,
228
+ chunk: &str,
229
+ output: &mut crate::tool::ToolParserOutput,
230
+ ) -> crate::tool::Result<()> {
231
+ output.normal_text.push_str(chunk);
232
+ Ok(())
233
+ }
234
+
235
+ fn finish(&mut self) -> crate::tool::Result<crate::tool::ToolParserOutput> {
236
+ Ok(crate::tool::ToolParserOutput::default())
237
+ }
238
+
239
+ fn reset(&mut self) -> String {
240
+ String::new()
241
+ }
242
+ }
243
+
244
+ struct PartialThenErrorToolParser;
245
+
246
+ impl ToolParser for PartialThenErrorToolParser {
247
+ fn create(_tools: &[Tool]) -> crate::tool::Result<Box<dyn ToolParser>>
248
+ where
249
+ Self: Sized + 'static,
250
+ {
251
+ Ok(Box::new(Self))
252
+ }
253
+
254
+ fn parse_into(
255
+ &mut self,
256
+ _chunk: &str,
257
+ output: &mut crate::tool::ToolParserOutput,
258
+ ) -> crate::tool::Result<()> {
259
+ output.normal_text.push_str("committed");
260
+ Err(crate::tool::ToolParserError::ParsingFailed {
261
+ message: "synthetic failure".to_string(),
262
+ })
263
+ }
264
+
265
+ fn finish(&mut self) -> crate::tool::Result<crate::tool::ToolParserOutput> {
266
+ Ok(crate::tool::ToolParserOutput::default())
267
+ }
268
+
269
+ fn reset(&mut self) -> String {
270
+ String::new()
271
+ }
272
+ }
273
+
274
+ #[test]
275
+ fn combined_parser_emits_reasoning_and_text() {
276
+ let tokenizer = Arc::new(FakeTokenizer);
277
+ let reasoning = Qwen3ReasoningParser::create(tokenizer).unwrap();
278
+ let mut parser = CombinedParser::new(Some(reasoning), None);
279
+
280
+ let output = collect(&mut parser, &["<think>work</think>answer"]);
281
+
282
+ assert_eq!(
283
+ output.events,
284
+ vec![
285
+ UnifiedParserEvent::Reasoning("work".to_string()),
286
+ UnifiedParserEvent::Text("answer".to_string()),
287
+ ]
288
+ );
289
+ }
290
+
291
+ #[test]
292
+ fn combined_parser_emits_tool_calls_from_visible_content() {
293
+ let tool = Qwen3XmlToolParser::create(&test_tools()).unwrap();
294
+ let mut parser = CombinedParser::new(None, Some(tool));
295
+ assert!(matches!(
296
+ parser.structural_tag_model(),
297
+ Some(crate::tool::StructuralTagModel::Qwen3)
298
+ ));
299
+
300
+ let output = collect(
301
+ &mut parser,
302
+ &[r#"<tool_call>
303
+{"name":"get_weather","arguments":{"location":"Paris"}}
304
+</tool_call>"#],
305
+ );
306
+
307
+ assert_eq!(
308
+ output.events,
309
+ vec![
310
+ UnifiedParserEvent::ToolCall(crate::tool::ToolCallDelta {
311
+ tool_index: 0,
312
+ name: Some("get_weather".to_string()),
313
+ arguments: String::new(),
314
+ }),
315
+ UnifiedParserEvent::ToolCall(crate::tool::ToolCallDelta {
316
+ tool_index: 0,
317
+ name: None,
318
+ arguments: r#"{"location":"Paris"}"#.to_string(),
319
+ }),
320
+ ]
321
+ );
322
+ }
323
+
324
+ #[test]
325
+ fn combined_parser_preserves_tool_output_on_parse_error() {
326
+ let mut parser = CombinedParser::new(None, Some(Box::new(PartialThenErrorToolParser)));
327
+ let mut output = UnifiedParserOutput::default();
328
+
329
+ let error = parser.parse_into("bad", &mut output).unwrap_err();
330
+
331
+ assert!(matches!(error, crate::unified::UnifiedParserError::Tool(_)));
332
+ assert_eq!(
333
+ output.events,
334
+ vec![UnifiedParserEvent::Text("committed".to_string())]
335
+ );
336
+ }
337
+
338
+ #[test]
339
+ fn combined_parser_preserves_special_tokens_when_either_inner_parser_needs_it() {
340
+ let mut parser = CombinedParser::new(Some(Box::new(PreserveReasoningParser)), None);
341
+ assert!(parser.preserve_special_tokens());
342
+
343
+ parser = CombinedParser::new(None, Some(Box::new(PreserveToolParser)));
344
+ assert!(parser.preserve_special_tokens());
345
+ }
346
+}
rust/src/parser/src/unified/mod.rs
new
+114
@@ -0,0 +1,114 @@
1
+//! Unified parser interface for reasoning and tool-call deltas.
2
+
3
+mod combined;
4
+
5
+use thiserror::Error;
6
+use vllm_tokenizer::DynTokenizer;
7
+
8
+pub use combined::CombinedParser;
9
+
10
+use crate::reasoning::ReasoningError;
11
+use crate::tool::{StructuralTagModel, Tool, ToolCallDelta, ToolParserError, ToolParserOutput};
12
+
13
+/// Result alias for unified parser operations.
14
+pub type Result<T> = std::result::Result<T, UnifiedParserError>;
15
+
16
+/// One parsed event emitted by a unified parser.
17
+#[derive(Debug, Clone, PartialEq, Eq)]
18
+pub enum UnifiedParserEvent {
19
+ /// Normal assistant-visible text.
20
+ Text(String),
21
+ /// Reasoning text hidden from the normal content stream.
22
+ Reasoning(String),
23
+ /// A tool-call update extracted from visible assistant text.
24
+ ToolCall(ToolCallDelta),
25
+}
26
+
27
+/// Result of advancing unified parsing with one assistant-text input.
28
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
29
+pub struct UnifiedParserOutput {
30
+ /// Ordered parser events committed by this input.
31
+ pub events: Vec<UnifiedParserEvent>,
32
+}
33
+
34
+impl UnifiedParserOutput {
35
+ /// Append one visible text event if `delta` is non-empty.
36
+ pub fn push_text(&mut self, delta: String) {
37
+ if delta.is_empty() {
38
+ return;
39
+ }
40
+ self.events.push(UnifiedParserEvent::Text(delta));
41
+ }
42
+
43
+ /// Append one reasoning text event if `delta` is non-empty.
44
+ pub fn push_reasoning(&mut self, delta: String) {
45
+ if delta.is_empty() {
46
+ return;
47
+ }
48
+ self.events.push(UnifiedParserEvent::Reasoning(delta));
49
+ }
50
+
51
+ /// Append parsed tool parser output as unified events.
52
+ pub fn append_tool_output(&mut self, output: ToolParserOutput) {
53
+ // TODO: make ToolParserOutput carry ordered events and remove this text-first flattening.
54
+ self.push_text(output.normal_text);
55
+ self.events.extend(output.calls.into_iter().map(UnifiedParserEvent::ToolCall));
56
+ }
57
+
58
+ /// Append another parser output onto this one.
59
+ pub fn append(&mut self, mut other: Self) {
60
+ self.events.append(&mut other.events);
61
+ }
62
+}
63
+
64
+/// Incremental parser that extracts reasoning and tool-call events from assistant output.
65
+pub trait UnifiedParser: Send {
66
+ /// Construct a boxed parser instance for one request stream.
67
+ fn create(tools: &[Tool], tokenizer: DynTokenizer) -> Result<Box<dyn UnifiedParser>>
68
+ where
69
+ Self: Sized + 'static;
70
+
71
+ /// Initialize parser state from prompt token IDs before output deltas arrive.
72
+ fn initialize(&mut self, _prompt_token_ids: &[u32]) -> Result<()> {
73
+ Ok(())
74
+ }
75
+
76
+ /// Return whether decoded output must preserve tokenizer special tokens.
77
+ fn preserve_special_tokens(&self) -> bool {
78
+ false
79
+ }
80
+
81
+ /// Return the xgrammar structural-tag model used for strict tool calling.
82
+ fn structural_tag_model(&self) -> Option<StructuralTagModel> {
83
+ None
84
+ }
85
+
86
+ /// Return the parser-provided ID for a tool call by index, if the model emitted one.
87
+ fn tool_call_id(&self, _tool_index: usize) -> Option<&str> {
88
+ None
89
+ }
90
+
91
+ /// Feed one decoded text delta into the parser, appending committed output into `output`.
92
+ fn parse_into(&mut self, delta: &str, output: &mut UnifiedParserOutput) -> Result<()>;
93
+
94
+ /// Flush any buffered parser state at end of stream.
95
+ fn finish(&mut self) -> Result<UnifiedParserOutput> {
96
+ Ok(UnifiedParserOutput::default())
97
+ }
98
+
99
+ /// Clear parser state and return currently uncommitted buffered text.
100
+ fn reset(&mut self) -> String {
101
+ String::new()
102
+ }
103
+}
104
+
105
+/// Errors produced while creating or running unified parsers.
106
+#[derive(Debug, Error)]
107
+pub enum UnifiedParserError {
108
+ #[error("combined parser is constructed from split parser instances")]
109
+ CombinedParserConstructor,
110
+ #[error(transparent)]
111
+ Reasoning(#[from] ReasoningError),
112
+ #[error(transparent)]
113
+ Tool(#[from] ToolParserError),
114
+}
rust/src/reasoning-parser/Cargo.toml
deleted
-12
@@ -1,12 +0,0 @@
1
-[package]
2
-name = "vllm-reasoning-parser"
3
-version.workspace = true
4
-edition.workspace = true
5
-license.workspace = true
6
-
7
-[dependencies]
8
-thiserror.workspace = true
9
-vllm-tokenizer.workspace = true
10
-
11
-[lints]
12
-workspace = true
tools/build_rust.py
+1
-1
@@ -27,7 +27,7 @@ def rust_extensions(*, optional: bool = False) -> list[RustExtension]:
27
),
28
RustExtension(
29
target="vllm._rust_tool_parser",
30
- path="rust/src/tool-parser/python/Cargo.toml",
30
+ path="rust/src/parser/python/Cargo.toml",
31
features=["pyo3/abi3-py38"],
32
binding=Binding.PyO3,
33
optional=optional,