@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 3a282230ee
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 2, 13:30 UTC

[Rust Frontend] Add `hy_v3` tool parser (#43872)

Signed-off-by: Bugen Zhao <i@bugenzhao.com>

Bugen Zhao committed May 28, 2026 at 22:42 UTC 3a282230ee5b58e7331910a81bedacdd0e2ac7f1
6 files changed +583 -7
rust/src/chat/src/lib.rs
+1 -1
@@ -233,7 +233,7 @@ mod tests {
233 )
234 .unwrap_err();
235
236 - expect_test::expect!["tool parser `definitely_missing_tool_parser` is not registered (choose from: deepseek_v3, deepseek_v31, deepseek_v32, deepseek_v4, gemma4, glm45, glm47, hermes, kimi_k2, llama3_json, llama4_json, minimax_m2, mistral, qwen3_coder, qwen3_xml)"].assert_eq(&error.to_report_string());
236 + expect_test::expect!["tool parser `definitely_missing_tool_parser` is not registered (choose from: deepseek_v3, deepseek_v31, deepseek_v32, deepseek_v4, gemma4, glm45, glm47, hermes, hy_v3, kimi_k2, llama3_json, llama4_json, minimax_m2, mistral, qwen3_coder, qwen3_xml)"].assert_eq(&error.to_report_string());
237 }
238
239 #[test]
rust/src/chat/src/parser/tool/mod.rs
+8 -3
@@ -4,9 +4,10 @@ use std::sync::LazyLock;
4
5 pub use vllm_tool_parser::{
6 DeepSeekV3ToolParser, DeepSeekV4ToolParser, DeepSeekV31ToolParser, DeepSeekV32ToolParser,
7 - Gemma4ToolParser, Glm45MoeToolParser, Glm47MoeToolParser, HermesToolParser, KimiK2ToolParser,
8 - Llama3JsonToolParser, MinimaxM2ToolParser, MistralToolParser, Qwen3CoderToolParser,
9 - Qwen3XmlToolParser, ToolCallDelta, ToolParser, ToolParserError, ToolParserOutput,
7 + Gemma4ToolParser, Glm45MoeToolParser, Glm47MoeToolParser, HermesToolParser, HyV3ToolParser,
8 + KimiK2ToolParser, Llama3JsonToolParser, MinimaxM2ToolParser, MistralToolParser,
9 + Qwen3CoderToolParser, Qwen3XmlToolParser, ToolCallDelta, ToolParser, ToolParserError,
10 + ToolParserOutput,
11 };
12
13 use crate::parser::ParserFactory;
@@ -22,6 +23,7 @@ pub mod names {
23 pub const GLM47: &str = "glm47";
24 pub const GEMMA4: &str = "gemma4";
25 pub const HERMES: &str = "hermes";
26 + pub const HY_V3: &str = "hy_v3";
27 pub const KIMI_K2: &str = "kimi_k2";
28 pub const LLAMA3_JSON: &str = "llama3_json";
29 pub const LLAMA4_JSON: &str = "llama4_json";
@@ -59,6 +61,7 @@ impl ToolParserFactory {
61 .register_parser::<Glm47MoeToolParser>(names::GLM47)
62 .register_parser::<Gemma4ToolParser>(names::GEMMA4)
63 .register_parser::<HermesToolParser>(names::HERMES)
64 + .register_parser::<HyV3ToolParser>(names::HY_V3)
65 .register_parser::<KimiK2ToolParser>(names::KIMI_K2)
66 .register_parser::<Llama3JsonToolParser>(names::LLAMA3_JSON)
67 .register_parser::<Llama3JsonToolParser>(names::LLAMA4_JSON)
@@ -75,6 +78,8 @@ impl ToolParserFactory {
78 .register_pattern("qwen3.5", names::QWEN3_CODER)
79 .register_pattern("qwen", names::QWEN3_XML)
80 .register_pattern("hermes", names::HERMES)
81 + .register_pattern("hy3", names::HY_V3)
82 + .register_pattern("hy_v3", names::HY_V3)
83 .register_pattern("llama-4", names::LLAMA4_JSON)
84 .register_pattern("llama-3.2", names::LLAMA3_JSON)
85 .register_pattern("llama-3.1", names::LLAMA3_JSON)
rust/src/chat/src/parser/tool/tests.rs
+4
@@ -149,6 +149,10 @@ fn factory_new_resolves_default_patterns() {
149 factory.resolve_name_for_model("NousResearch/Hermes-3-Llama-3.1-8B"),
150 Some(names::HERMES)
151 );
152 + assert_eq!(
153 + factory.resolve_name_for_model("tencent/Hy3-preview"),
154 + Some(names::HY_V3)
155 + );
156 assert_eq!(
157 factory.resolve_name_for_model("MiniMax/MiniMax-M2-01"),
158 Some(names::MINIMAX_M2)
rust/src/tool-parser/src/hy_v3.rs new
+559
@@ -0,0 +1,559 @@
1 +use winnow::ascii::multispace0 as ws0;
2 +use winnow::combinator::{alt, delimited, eof, repeat, seq, terminated};
3 +use winnow::prelude::*;
4 +use winnow::stream::Partial;
5 +use winnow::token::{literal, rest, take_until};
6 +
7 +use super::parameters::ToolSchemas;
8 +use super::utils::{parse_buffered_event, safe_text_len};
9 +use super::{Result, ToolCallDelta, ToolParser, ToolParserOutput};
10 +use crate::Tool;
11 +
12 +const TOOL_CALLS_START: &str = "<tool_calls>";
13 +const TOOL_CALLS_END: &str = "</tool_calls>";
14 +const TOOL_CALL_START: &str = "<tool_call>";
15 +const TOOL_CALL_END: &str = "</tool_call>";
16 +const TOOL_SEP: &str = "<tool_sep>";
17 +const ARG_KEY_START: &str = "<arg_key>";
18 +const ARG_KEY_END: &str = "</arg_key>";
19 +const ARG_VALUE_START: &str = "<arg_value>";
20 +const ARG_VALUE_END: &str = "</arg_value>";
21 +
22 +type HyV3Input<'i> = Partial<&'i str>;
23 +
24 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25 +enum HyV3Mode {
26 + Text,
27 + ToolBlock,
28 + Done,
29 +}
30 +
31 +#[derive(Debug, Clone, PartialEq, Eq)]
32 +enum HyV3Event {
33 + Text {
34 + len: usize,
35 + },
36 + ToolBlockStart,
37 + ToolCall {
38 + name: String,
39 + raw_params: Vec<(String, String)>,
40 + },
41 + ToolBlockEnd,
42 + IgnoredRest,
43 +}
44 +
45 +/// Tool parser for HY3 XML-style tool calls.
46 +///
47 +/// Example tool call content:
48 +///
49 +/// ```text
50 +/// <tool_calls>
51 +/// <tool_call>get_weather<tool_sep>
52 +/// <arg_key>city</arg_key><arg_value>Beijing</arg_value>
53 +/// </tool_call>
54 +/// </tool_calls>
55 +/// ```
56 +///
57 +/// Arguments are emitted only after a full `<tool_call>` block is parsed.
58 +/// HY3 marker tokens are added-vocabulary tokens rather than tokenizer special
59 +/// tokens, so the default `preserve_special_tokens() == false` is sufficient.
60 +pub struct HyV3ToolParser {
61 + buffer: String,
62 + mode: HyV3Mode,
63 + emitted_tool_count: usize,
64 + tool_parameters: ToolSchemas,
65 +}
66 +
67 +impl HyV3ToolParser {
68 + /// Create a HY3 tool parser.
69 + fn new(tools: &[Tool]) -> Self {
70 + Self {
71 + buffer: String::new(),
72 + mode: HyV3Mode::Text,
73 + emitted_tool_count: 0,
74 + tool_parameters: ToolSchemas::from_tools(tools),
75 + }
76 + }
77 +
78 + /// Apply one parsed HY3 event to parser state and output.
79 + fn apply_event(&mut self, event: HyV3Event, output: &mut ToolParserOutput) -> Result<()> {
80 + match event {
81 + HyV3Event::Text { len: consumed_len } => {
82 + output.normal_text.push_str(&self.buffer[..consumed_len]);
83 + }
84 + HyV3Event::ToolBlockStart => self.mode = HyV3Mode::ToolBlock,
85 + HyV3Event::ToolCall { name, raw_params } => {
86 + let arguments = self.tool_parameters.convert_params_with_schema(&name, raw_params);
87 + let arguments = serde_json::to_string(&arguments)
88 + .map_err(|error| parsing_failed!("failed to serialize arguments: {}", error))?;
89 +
90 + output.calls.push(ToolCallDelta {
91 + tool_index: self.emitted_tool_count,
92 + name: Some(name),
93 + arguments,
94 + });
95 + self.emitted_tool_count += 1;
96 + }
97 + HyV3Event::ToolBlockEnd => self.mode = HyV3Mode::Done,
98 + HyV3Event::IgnoredRest => {}
99 + }
100 + Ok(())
101 + }
102 +}
103 +
104 +impl ToolParser for HyV3ToolParser {
105 + fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
106 + where
107 + Self: Sized + 'static,
108 + {
109 + Ok(Box::new(Self::new(tools)))
110 + }
111 +
112 + fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
113 + self.buffer.push_str(chunk);
114 +
115 + while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
116 + parse_next_hy_v3_event(input, self.mode)
117 + })? {
118 + self.apply_event(event, output)?;
119 + self.buffer.drain(..consumed_len);
120 + }
121 +
122 + Ok(())
123 + }
124 +
125 + fn finish(&mut self) -> Result<ToolParserOutput> {
126 + let mut output = ToolParserOutput::default();
127 + match self.mode {
128 + HyV3Mode::Text => output.normal_text.push_str(&self.buffer),
129 + HyV3Mode::ToolBlock => return Err(parsing_failed!("incomplete HY3 tool call")),
130 + HyV3Mode::Done => {}
131 + }
132 + let _ = self.reset();
133 + Ok(output)
134 + }
135 +
136 + fn reset(&mut self) -> String {
137 + self.mode = HyV3Mode::Text;
138 + self.emitted_tool_count = 0;
139 + std::mem::take(&mut self.buffer)
140 + }
141 +}
142 +
143 +/// Parse a HY3 event for the current parser mode.
144 +fn parse_next_hy_v3_event(input: &mut HyV3Input<'_>, mode: HyV3Mode) -> ModalResult<HyV3Event> {
145 + match mode {
146 + HyV3Mode::Text => parse_text_event(input),
147 + HyV3Mode::ToolBlock => parse_tool_block_event(input),
148 + HyV3Mode::Done => ignored_rest_event(input),
149 + }
150 +}
151 +
152 +/// Parse a text-mode HY3 event.
153 +fn parse_text_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
154 + alt((tool_block_start_event, safe_text_event)).parse_next(input)
155 +}
156 +
157 +/// Parse a HY3 tool-block start marker.
158 +fn tool_block_start_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
159 + literal(TOOL_CALLS_START).value(HyV3Event::ToolBlockStart).parse_next(input)
160 +}
161 +
162 +/// Parse a safe text run before the next HY3 marker.
163 +fn safe_text_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
164 + safe_text_len(input, TOOL_CALLS_START).map(|len| HyV3Event::Text { len })
165 +}
166 +
167 +/// Parse one event inside a HY3 tool block.
168 +fn parse_tool_block_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
169 + alt((tool_block_end_event, tool_call_event)).parse_next(input)
170 +}
171 +
172 +/// Parse a HY3 tool-block end marker.
173 +fn tool_block_end_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
174 + (ws0, literal(TOOL_CALLS_END)).value(HyV3Event::ToolBlockEnd).parse_next(input)
175 +}
176 +
177 +/// Parse a complete HY3 tool-call block.
178 +fn tool_call_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
179 + let (name, body) = seq!(
180 + _: ws0,
181 + _: literal(TOOL_CALL_START),
182 + take_until(0.., TOOL_SEP),
183 + _: literal(TOOL_SEP),
184 + take_until(0.., TOOL_CALL_END),
185 + _: literal(TOOL_CALL_END),
186 + )
187 + .parse_next(input)?;
188 + let raw_params = parse_tool_call_params(body)?;
189 +
190 + Ok(HyV3Event::ToolCall {
191 + name: name.trim().to_string(),
192 + raw_params,
193 + })
194 +}
195 +
196 +/// Parse all parameter blocks inside a complete HY3 tool call.
197 +fn parse_tool_call_params(tool_call_body: &str) -> ModalResult<Vec<(String, String)>> {
198 + let mut input = tool_call_body;
199 + delimited(ws0, repeat(0.., terminated(parameter, ws0)), eof).parse_next(&mut input)
200 +}
201 +
202 +/// Parse a HY3 argument key/value block.
203 +fn parameter(input: &mut &str) -> ModalResult<(String, String)> {
204 + let (name, value) = seq!(
205 + _: literal(ARG_KEY_START),
206 + take_until(0.., ARG_KEY_END),
207 + _: literal(ARG_KEY_END),
208 + _: ws0,
209 + _: literal(ARG_VALUE_START),
210 + take_until(0.., ARG_VALUE_END),
211 + _: literal(ARG_VALUE_END),
212 + )
213 + .parse_next(input)?;
214 +
215 + Ok((name.trim().to_string(), value.to_string()))
216 +}
217 +
218 +/// Parse ignored rest after the HY3 tool block ends.
219 +fn ignored_rest_event(input: &mut HyV3Input<'_>) -> ModalResult<HyV3Event> {
220 + rest.value(HyV3Event::IgnoredRest).parse_next(input)
221 +}
222 +
223 +#[cfg(test)]
224 +mod tests {
225 + use expect_test::expect;
226 + use serde_json::{Value, json};
227 + use thiserror_ext::AsReport;
228 +
229 + use super::{HyV3ToolParser, ToolParser};
230 + use crate::test_utils::{collect_stream, split_by_chars, test_tools};
231 + use crate::{ToolParserOutput, ToolParserTestExt as _};
232 +
233 + fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
234 + let params = params
235 + .iter()
236 + .map(|(name, value)| format!("<arg_key>{name}</arg_key><arg_value>{value}</arg_value>"))
237 + .collect::<Vec<_>>()
238 + .join("\n");
239 + format!("<tool_call>{function_name}<tool_sep>{params}</tool_call>")
240 + }
241 +
242 + fn build_tool_calls(tool_calls: &[String]) -> String {
243 + format!("<tool_calls>\n{}\n</tool_calls>", tool_calls.join("\n"))
244 + }
245 +
246 + fn parsed_arguments(output: &ToolParserOutput, index: usize) -> Value {
247 + serde_json::from_str(&output.calls[index].arguments).unwrap()
248 + }
249 +
250 + #[test]
251 + fn hy_v3_does_not_preserve_special_tokens() {
252 + let parser = HyV3ToolParser::new(&test_tools());
253 +
254 + assert!(!parser.preserve_special_tokens());
255 + }
256 +
257 + #[test]
258 + fn hy_v3_parse_complete_without_tool_call_keeps_text() {
259 + let mut parser = HyV3ToolParser::new(&test_tools());
260 + let output = parser.parse_complete("This is a plain response.").unwrap();
261 +
262 + assert_eq!(output.normal_text, "This is a plain response.");
263 + assert!(output.calls.is_empty());
264 + }
265 +
266 + #[test]
267 + fn hy_v3_parse_complete_extracts_zero_arg_inline_tool_call() {
268 + let mut parser = HyV3ToolParser::new(&test_tools());
269 + let output = parser
270 + .parse_complete(
271 + "<tool_calls><tool_call>get_current_date<tool_sep></tool_call></tool_calls>",
272 + )
273 + .unwrap();
274 +
275 + assert_eq!(output.normal_text, "");
276 + assert_eq!(output.calls.len(), 1);
277 + assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
278 + assert_eq!(parsed_arguments(&output, 0), json!({}));
279 + }
280 +
281 + #[test]
282 + fn hy_v3_parse_complete_extracts_zero_arg_newline_tool_call() {
283 + let mut parser = HyV3ToolParser::new(&test_tools());
284 + let output = parser
285 + .parse_complete(
286 + "<tool_calls>\n<tool_call>get_current_date<tool_sep>\n</tool_call>\n</tool_calls>",
287 + )
288 + .unwrap();
289 +
290 + assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
291 + assert_eq!(parsed_arguments(&output, 0), json!({}));
292 + }
293 +
294 + #[test]
295 + fn hy_v3_parse_complete_extracts_arguments_on_same_line() {
296 + let mut parser = HyV3ToolParser::new(&test_tools());
297 + let output = parser
298 + .parse_complete(
299 + "<tool_calls><tool_call>get_weather<tool_sep><arg_key>city</arg_key><arg_value>Beijing</arg_value><arg_key>date</arg_key><arg_value>2026-03-30</arg_value></tool_call></tool_calls>",
300 + )
301 + .unwrap();
302 +
303 + assert_eq!(
304 + parsed_arguments(&output, 0),
305 + json!({ "city": "Beijing", "date": "2026-03-30" })
306 + );
307 + }
308 +
309 + #[test]
310 + fn hy_v3_parse_complete_extracts_arguments_with_newlines() {
311 + let mut parser = HyV3ToolParser::new(&test_tools());
312 + let output = parser
313 + .parse_complete(&build_tool_calls(&[build_tool_call(
314 + "get_weather",
315 + &[("city", "Beijing"), ("date", "2026-03-30")],
316 + )]))
317 + .unwrap();
318 +
319 + assert_eq!(
320 + parsed_arguments(&output, 0),
321 + json!({ "city": "Beijing", "date": "2026-03-30" })
322 + );
323 + }
324 +
325 + #[test]
326 + fn hy_v3_parse_complete_preserves_prefix_and_ignores_trailing_text() {
327 + let mut parser = HyV3ToolParser::new(&test_tools());
328 + let output = parser
329 + .parse_complete(&format!(
330 + "Checking.{} trailing text",
331 + build_tool_calls(&[build_tool_call("get_current_date", &[])])
332 + ))
333 + .unwrap();
334 +
335 + assert_eq!(output.normal_text, "Checking.");
336 + assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
337 + }
338 +
339 + #[test]
340 + fn hy_v3_parse_complete_extracts_multiple_tool_calls_in_one_block() {
341 + let mut parser = HyV3ToolParser::new(&test_tools());
342 + let output = parser
343 + .parse_complete(&build_tool_calls(&[
344 + build_tool_call(
345 + "get_weather",
346 + &[("city", "Beijing"), ("date", "2026-03-30")],
347 + ),
348 + build_tool_call(
349 + "get_weather",
350 + &[("city", "Hangzhou"), ("date", "2026-03-30")],
351 + ),
352 + ]))
353 + .unwrap();
354 +
355 + expect![[r#"
356 + ToolParserOutput {
357 + normal_text: "",
358 + calls: [
359 + ToolCallDelta {
360 + tool_index: 0,
361 + name: Some(
362 + "get_weather",
363 + ),
364 + arguments: "{\"city\":\"Beijing\",\"date\":\"2026-03-30\"}",
365 + },
366 + ToolCallDelta {
367 + tool_index: 1,
368 + name: Some(
369 + "get_weather",
370 + ),
371 + arguments: "{\"city\":\"Hangzhou\",\"date\":\"2026-03-30\"}",
372 + },
373 + ],
374 + }
375 + "#]]
376 + .assert_debug_eq(&output);
377 + }
378 +
379 + #[test]
380 + fn hy_v3_parse_complete_converts_schema_types() {
381 + let mut parser = HyV3ToolParser::new(&test_tools());
382 + let output = parser
383 + .parse_complete(&build_tool_calls(&[build_tool_call(
384 + "convert",
385 + &[
386 + ("whole", "5.3"),
387 + ("flag", "true"),
388 + ("payload", r#"{"k":1}"#),
389 + ("items", "[1,2]"),
390 + ],
391 + )]))
392 + .unwrap();
393 +
394 + assert_eq!(
395 + parsed_arguments(&output, 0),
396 + json!({
397 + "whole": 5.3,
398 + "flag": true,
399 + "payload": { "k": 1 },
400 + "items": [1, 2]
401 + })
402 + );
403 + }
404 +
405 + #[test]
406 + fn hy_v3_streaming_without_tool_call_emits_text_incrementally() {
407 + let mut parser = HyV3ToolParser::new(&test_tools());
408 + let mut output = ToolParserOutput::default();
409 +
410 + output.append(parser.parse_chunk("This is ").unwrap());
411 + output.append(parser.parse_chunk("a plain ").unwrap());
412 + output.append(parser.parse_chunk("response.").unwrap());
413 + output.append(parser.finish().unwrap());
414 +
415 + assert_eq!(output.normal_text, "This is a plain response.");
416 + assert!(output.calls.is_empty());
417 + }
418 +
419 + #[test]
420 + fn hy_v3_streaming_extracts_zero_arg_tool_call() {
421 + let mut parser = HyV3ToolParser::new(&test_tools());
422 + let chunks = [
423 + "<tool_calls>",
424 + "\n<tool_call>",
425 + "get_current_date",
426 + "<tool_sep>",
427 + "\n</tool_call>",
428 + "\n</tool_calls>",
429 + ];
430 +
431 + let output = collect_stream(&mut parser, &chunks);
432 +
433 + assert_eq!(output.calls.len(), 1);
434 + assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
435 + assert_eq!(parsed_arguments(&output, 0), json!({}));
436 + }
437 +
438 + #[test]
439 + fn hy_v3_streaming_extracts_arguments() {
440 + let mut parser = HyV3ToolParser::new(&test_tools());
441 + let chunks = [
442 + "<tool_calls>",
443 + "\n<tool_call>",
444 + "get_weather",
445 + "<tool_sep>",
446 + "\n<arg_key>city</arg_key>",
447 + "\n<arg_value>Beijing</arg_value>",
448 + "\n<arg_key>date</arg_key>",
449 + "\n<arg_value>2026-03-30</arg_value>",
450 + "\n</tool_call>",
451 + "\n</tool_calls>",
452 + ];
453 +
454 + let output = collect_stream(&mut parser, &chunks);
455 +
456 + assert_eq!(output.calls.len(), 1);
457 + assert_eq!(output.calls[0].name.as_deref(), Some("get_weather"));
458 + assert_eq!(
459 + parsed_arguments(&output, 0),
460 + json!({ "city": "Beijing", "date": "2026-03-30" })
461 + );
462 + }
463 +
464 + #[test]
465 + fn hy_v3_streaming_preserves_prefix_text() {
466 + let mut parser = HyV3ToolParser::new(&test_tools());
467 + let chunks = [
468 + "Checking.",
469 + "<tool_calls>",
470 + "\n<tool_call>",
471 + "get_current_date",
472 + "<tool_sep>",
473 + "\n</tool_call>",
474 + "\n</tool_calls>",
475 + ];
476 +
477 + let output = collect_stream(&mut parser, &chunks);
478 +
479 + assert_eq!(output.normal_text, "Checking.");
480 + assert_eq!(output.calls[0].name.as_deref(), Some("get_current_date"));
481 + }
482 +
483 + #[test]
484 + fn hy_v3_streaming_extracts_multiple_tool_calls_in_one_block() {
485 + let input = build_tool_calls(&[
486 + build_tool_call(
487 + "get_weather",
488 + &[("city", "Beijing"), ("date", "2026-03-30")],
489 + ),
490 + build_tool_call(
491 + "get_weather",
492 + &[("city", "Hangzhou"), ("date", "2026-03-30")],
493 + ),
494 + ]);
495 + let chunks = split_by_chars(&input, 9);
496 + let mut parser = HyV3ToolParser::new(&test_tools());
497 +
498 + let output = collect_stream(&mut parser, &chunks);
499 +
500 + assert_eq!(output.calls.len(), 2);
501 + assert_eq!(parsed_arguments(&output, 0)["city"], json!("Beijing"));
502 + assert_eq!(parsed_arguments(&output, 1)["city"], json!("Hangzhou"));
503 + }
504 +
505 + #[test]
506 + fn hy_v3_streaming_handles_start_marker_split_across_chunks() {
507 + let input = format!(
508 + "hello {}",
509 + build_tool_calls(&[build_tool_call("get_weather", &[("city", "Beijing")])])
510 + );
511 + let chunks = split_by_chars(&input, 5);
512 + let mut parser = HyV3ToolParser::new(&test_tools());
513 +
514 + let output = collect_stream(&mut parser, &chunks);
515 +
516 + assert_eq!(output.normal_text, "hello ");
517 + assert_eq!(output.calls.len(), 1);
518 + assert_eq!(parsed_arguments(&output, 0), json!({ "city": "Beijing" }));
519 + }
520 +
521 + #[test]
522 + fn hy_v3_streaming_does_not_emit_incomplete_tool_call() {
523 + let mut parser = HyV3ToolParser::new(&test_tools());
524 + let mut output = ToolParserOutput::default();
525 +
526 + parser
527 + .parse_into(
528 + "<tool_calls><tool_call>get_weather<tool_sep><arg_key>city</arg_key><arg_value>Bei",
529 + &mut output,
530 + )
531 + .unwrap();
532 +
533 + assert_eq!(output.normal_text, "");
534 + assert!(output.calls.is_empty());
535 + }
536 +
537 + #[test]
538 + fn hy_v3_finish_fails_incomplete_tool_call() {
539 + let mut parser = HyV3ToolParser::new(&test_tools());
540 + parser.parse_chunk("<tool_calls><tool_call>get_weather<tool_sep>").unwrap();
541 +
542 + let error = parser.finish().unwrap_err();
543 +
544 + expect!["tool parser parsing failed: incomplete HY3 tool call"]
545 + .assert_eq(&error.to_report_string());
546 + }
547 +
548 + #[test]
549 + fn hy_v3_malformed_tool_call_fails_fast() {
550 + let mut parser = HyV3ToolParser::new(&test_tools());
551 + let error = parser
552 + .parse_complete(
553 + "<tool_calls><tool_call>get_weather<tool_sep><arg_key>city</arg_key><arg_value>Beijing</tool_call></tool_calls>",
554 + )
555 + .unwrap_err();
556 +
557 + assert!(error.to_report_string().starts_with("tool parser parsing failed:"));
558 + }
559 +}
rust/src/tool-parser/src/lib.rs
+2
@@ -6,6 +6,7 @@ mod deepseek_dsml;
6 mod deepseek_json;
7 mod gemma4;
8 mod glm_xml;
9 +mod hy_v3;
10 mod json;
11 mod kimi_k2;
12 mod minimax_m2;
@@ -22,6 +23,7 @@ pub use deepseek_json::{DeepSeekV3ToolParser, DeepSeekV31ToolParser};
23 pub use error::{Result, ToolParserError};
24 pub use gemma4::Gemma4ToolParser;
25 pub use glm_xml::{Glm45MoeToolParser, Glm47MoeToolParser};
26 +pub use hy_v3::HyV3ToolParser;
27 pub use json::{HermesToolParser, Llama3JsonToolParser, MistralToolParser, Qwen3XmlToolParser};
28 pub use kimi_k2::KimiK2ToolParser;
29 pub use minimax_m2::MinimaxM2ToolParser;
rust/src/tool-parser/src/parameters.rs
+9 -3
@@ -177,10 +177,10 @@ impl JsonParamType {
177 match kind.as_str() {
178 "string" | "str" | "text" | "varchar" | "char" | "enum" => Some(Self::String),
179 "integer" | "int" => Some(Self::Integer),
180 - "number" | "float" => Some(Self::Number),
180 + "number" | "float" | "double" => Some(Self::Number),
181 "boolean" | "bool" | "binary" => Some(Self::Boolean),
182 - "object" => Some(Self::Object),
183 - "array" | "arr" | "sequence" => Some(Self::Array),
182 + "object" | "dict" | "map" => Some(Self::Object),
183 + "array" | "arr" | "list" | "sequence" => Some(Self::Array),
184 "null" => Some(Self::Null),
185 _ if kind.starts_with("int")
186 || kind.starts_with("uint")
@@ -288,9 +288,12 @@ mod tests {
288 "text": { "type": "string" },
289 "count": { "type": "integer" },
290 "size": { "type": "number" },
291 + "ratio": { "type": "double" },
292 "enabled": { "type": "boolean" },
293 "payload": { "type": "object" },
294 + "mapping": { "type": "map" },
295 "items": { "type": "array" },
296 + "names": { "type": "list" },
297 "nothing": { "type": "null" }
298 }
299 }));
@@ -298,9 +301,12 @@ mod tests {
301 assert_eq!(params.convert("text", "42"), json!("42"));
302 assert_eq!(params.convert("count", "42"), json!(42));
303 assert_eq!(params.convert("size", "5.0"), json!(5.0));
304 + assert_eq!(params.convert("ratio", "2.5"), json!(2.5));
305 assert_eq!(params.convert("enabled", "1"), json!(true));
306 assert_eq!(params.convert("payload", r#"{"k":1}"#), json!({ "k": 1 }));
307 + assert_eq!(params.convert("mapping", r#"{"k":1}"#), json!({ "k": 1 }));
308 assert_eq!(params.convert("items", "[1,2]"), json!([1, 2]));
309 + assert_eq!(params.convert("names", r#"["a","b"]"#), json!(["a", "b"]));
310 assert_eq!(params.convert("nothing", "null"), json!(null));
311 }
312