[Bugfix] Preserve parallel HY-V3 calls delivered in one streaming delta (#53965)

Signed-off-by: Taneem Ibrahim <taneem.ibrahim@gmail.com>

Taneem Ibrahim committed Aug 27, 2026 at 11:26 UTC f18e29834b2114428aa5e261bfde09cb87965b4b
1 file changed +49 -8
vllm/tool_parsers/hy_v3_tool_parser.py
+49 -8
@@ -419,12 +419,11 @@ class HYV3ToolParser(ToolParser):
419 return DeltaMessage(content=delta_text)
420
421 # Encountered tool_calls start tag; extract preceding content and buffer
422 + content_delta: str | None = None
423 if self.tool_calls_start_token in delta_text:
424 text_parts = delta_text.split(self.tool_calls_start_token)
425 self._buffer += text_parts[-1]
425 - if text_parts[0]:
426 - return DeltaMessage(content=text_parts[0])
427 - # Don't return None; continue processing buffer for complete content
426 + content_delta = text_parts[0] or None
427 else:
428 self._buffer += delta_text
429
@@ -441,6 +440,8 @@ class HYV3ToolParser(ToolParser):
440 # Haven't encountered tool_call start tag yet; keep buffering
441 start_idx = cur_text.find(self.tool_call_start_token)
442 if start_idx == -1 and self._streaming_tool_name is None:
443 + if content_delta is not None:
444 + return DeltaMessage(content=content_delta)
445 self._buffer = ""
446 return None
447
@@ -451,7 +452,9 @@ class HYV3ToolParser(ToolParser):
452 if sep_idx == -1:
453 # tool_sep not yet seen; keep buffering from tool_call_start
454 self._buffer = cur_text[start_idx:]
454 - return None
455 + if content_delta is None:
456 + return None
457 + return DeltaMessage(content=content_delta)
458
459 # Extract tool name: between tool_call_start_token and tool_sep_token
460 name_start = start_idx + len(self.tool_call_start_token)
@@ -476,6 +479,7 @@ class HYV3ToolParser(ToolParser):
479 )
480 ]
481 )
482 + name_delta.content = content_delta
483
484 # Check if buffer already has complete arguments (all-in-one-delta)
485 if self.tool_call_end_token not in self._buffer:
@@ -496,6 +500,34 @@ class HYV3ToolParser(ToolParser):
500 ]
501 )
502
503 + def _drain_calls(self, request: ChatCompletionRequest) -> list[DeltaToolCall]:
504 + deltas: list[DeltaToolCall] = []
505 + while (start_idx := self._buffer.find(self.tool_call_start_token)) != -1:
506 + end_idx = self._buffer.find(self.tool_call_end_token, start_idx)
507 + if end_idx == -1:
508 + break
509 + end_idx += len(self.tool_call_end_token)
510 + complete_call = self._buffer[start_idx:end_idx]
511 + self._buffer = self._buffer[end_idx:]
512 + for call in self._extract_tool_calls(complete_call, request):
513 + self.current_tool_id += 1
514 + arguments = call.function.arguments
515 + self.prev_tool_call_arr.append(
516 + {"name": call.function.name, "arguments": json.loads(arguments)}
517 + )
518 + self.streamed_args_for_tool.append(arguments)
519 + deltas.append(
520 + DeltaToolCall(
521 + index=self.current_tool_id,
522 + id=make_tool_call_id(),
523 + type="function",
524 + function=DeltaFunctionCall(
525 + name=call.function.name, arguments=arguments
526 + ),
527 + )
528 + )
529 + return deltas
530 +
531 def _extract_streaming_incremental(
532 self,
533 name_delta: DeltaMessage | None,
@@ -632,9 +664,10 @@ class HYV3ToolParser(ToolParser):
664 self._streamed_json_len = end
665
666 # --- construct return DeltaMessage ---
667 + delta: DeltaMessage | None
668 if name_delta is not None and argument_diff:
669 nd_func = name_delta.tool_calls[0].function
637 - return DeltaMessage(
670 + delta = DeltaMessage(
671 tool_calls=[
672 DeltaToolCall(
673 index=self.current_tool_id,
@@ -647,9 +680,17 @@ class HYV3ToolParser(ToolParser):
680 )
681 ]
682 )
683 + delta.content = name_delta.content
684 elif name_delta is not None:
651 - return name_delta
685 + delta = name_delta
686 elif argument_diff:
653 - return self._make_args_delta(argument_diff)
687 + delta = self._make_args_delta(argument_diff)
688 else:
655 - return None
689 + delta = None
690 +
691 + extra_tool_calls = self._drain_calls(request) if is_complete else []
692 + if extra_tool_calls:
693 + if delta is None:
694 + return DeltaMessage(tool_calls=extra_tool_calls)
695 + delta.tool_calls.extend(extra_tool_calls)
696 + return delta