From f18e29834b2114428aa5e261bfde09cb87965b4b Mon Sep 17 00:00:00 2001 From: Taneem Ibrahim Date: Thu, 27 Aug 2026 11:26:10 -0400 Subject: [PATCH] [Bugfix] Preserve parallel HY-V3 calls delivered in one streaming delta (#53965) Signed-off-by: Taneem Ibrahim --- vllm/tool_parsers/hy_v3_tool_parser.py | 57 ++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/vllm/tool_parsers/hy_v3_tool_parser.py b/vllm/tool_parsers/hy_v3_tool_parser.py index 0ffaf92d86..4af3bbe534 100644 --- a/vllm/tool_parsers/hy_v3_tool_parser.py +++ b/vllm/tool_parsers/hy_v3_tool_parser.py @@ -419,12 +419,11 @@ class HYV3ToolParser(ToolParser): return DeltaMessage(content=delta_text) # Encountered tool_calls start tag; extract preceding content and buffer + content_delta: str | None = None if self.tool_calls_start_token in delta_text: text_parts = delta_text.split(self.tool_calls_start_token) self._buffer += text_parts[-1] - if text_parts[0]: - return DeltaMessage(content=text_parts[0]) - # Don't return None; continue processing buffer for complete content + content_delta = text_parts[0] or None else: self._buffer += delta_text @@ -441,6 +440,8 @@ class HYV3ToolParser(ToolParser): # Haven't encountered tool_call start tag yet; keep buffering start_idx = cur_text.find(self.tool_call_start_token) if start_idx == -1 and self._streaming_tool_name is None: + if content_delta is not None: + return DeltaMessage(content=content_delta) self._buffer = "" return None @@ -451,7 +452,9 @@ class HYV3ToolParser(ToolParser): if sep_idx == -1: # tool_sep not yet seen; keep buffering from tool_call_start self._buffer = cur_text[start_idx:] - return None + if content_delta is None: + return None + return DeltaMessage(content=content_delta) # Extract tool name: between tool_call_start_token and tool_sep_token name_start = start_idx + len(self.tool_call_start_token) @@ -476,6 +479,7 @@ class HYV3ToolParser(ToolParser): ) ] ) + name_delta.content = content_delta # Check if buffer already has complete arguments (all-in-one-delta) if self.tool_call_end_token not in self._buffer: @@ -496,6 +500,34 @@ class HYV3ToolParser(ToolParser): ] ) + def _drain_calls(self, request: ChatCompletionRequest) -> list[DeltaToolCall]: + deltas: list[DeltaToolCall] = [] + while (start_idx := self._buffer.find(self.tool_call_start_token)) != -1: + end_idx = self._buffer.find(self.tool_call_end_token, start_idx) + if end_idx == -1: + break + end_idx += len(self.tool_call_end_token) + complete_call = self._buffer[start_idx:end_idx] + self._buffer = self._buffer[end_idx:] + for call in self._extract_tool_calls(complete_call, request): + self.current_tool_id += 1 + arguments = call.function.arguments + self.prev_tool_call_arr.append( + {"name": call.function.name, "arguments": json.loads(arguments)} + ) + self.streamed_args_for_tool.append(arguments) + deltas.append( + DeltaToolCall( + index=self.current_tool_id, + id=make_tool_call_id(), + type="function", + function=DeltaFunctionCall( + name=call.function.name, arguments=arguments + ), + ) + ) + return deltas + def _extract_streaming_incremental( self, name_delta: DeltaMessage | None, @@ -632,9 +664,10 @@ class HYV3ToolParser(ToolParser): self._streamed_json_len = end # --- construct return DeltaMessage --- + delta: DeltaMessage | None if name_delta is not None and argument_diff: nd_func = name_delta.tool_calls[0].function - return DeltaMessage( + delta = DeltaMessage( tool_calls=[ DeltaToolCall( index=self.current_tool_id, @@ -647,9 +680,17 @@ class HYV3ToolParser(ToolParser): ) ] ) + delta.content = name_delta.content elif name_delta is not None: - return name_delta + delta = name_delta elif argument_diff: - return self._make_args_delta(argument_diff) + delta = self._make_args_delta(argument_diff) else: - return None + delta = None + + extra_tool_calls = self._drain_calls(request) if is_complete else [] + if extra_tool_calls: + if delta is None: + return DeltaMessage(tool_calls=extra_tool_calls) + delta.tool_calls.extend(extra_tool_calls) + return delta