[Refactor] Chat Completions Streaming Harmony Refactor and Bugfixes (#45104)

Signed-off-by: Yifan Zong <yzong@redhat.com>
This commit is contained in:
yzong-rh
2026-06-12 01:09:47 +00:00
committed by GitHub
parent 6f573f486b
commit e0871ad225
5 changed files with 394 additions and 733 deletions
@@ -1,471 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Unit tests for harmony streaming delta extraction.
"""
from dataclasses import dataclass, field
from unittest.mock import patch
import pytest
from vllm.entrypoints.openai.chat_completion.stream_harmony import (
TokenState,
extract_harmony_streaming_delta,
)
@dataclass
class MockMessage:
"""Mock message object for testing."""
channel: str | None = None
recipient: str | None = None
@dataclass
class MockStreamableParser:
"""Mock StreamableParser for testing without openai_harmony dependency."""
messages: list[MockMessage] = field(default_factory=list)
class TestExtractHarmonyStreamingDelta:
"""Tests for extract_harmony_streaming_delta function."""
@pytest.mark.parametrize(
"delta_text,expected_content",
[
("Hello, world!", "Hello, world!"),
("", ""),
],
)
def test_final_channel_returns_content_delta(self, delta_text, expected_content):
"""Test that final channel returns a DeltaMessage with content."""
parser = MockStreamableParser()
# Updated to use TokenState list
token_states = [TokenState(channel="final", recipient=None, text=delta_text)]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=False,
)
assert delta_message is not None
assert delta_message.content == expected_content
assert tools_streamed is False
@pytest.mark.parametrize(
"include_reasoning,expected_has_message",
[
(True, True),
(False, False),
],
)
def test_analysis_channel_reasoning(self, include_reasoning, expected_has_message):
"""Test analysis channel respects include_reasoning flag."""
parser = MockStreamableParser()
text = "Let me think..."
token_states = [TokenState(channel="analysis", recipient=None, text=text)]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=include_reasoning,
)
if expected_has_message:
assert delta_message is not None
assert delta_message.reasoning == text
else:
assert delta_message is None
assert tools_streamed is False
@pytest.mark.parametrize("channel", ["commentary", "analysis"])
@patch("vllm.entrypoints.openai.chat_completion.stream_harmony.make_tool_call_id")
def test_new_tool_call(self, mock_make_tool_call_id, channel):
"""Test new tool call creation when recipient changes."""
mock_make_tool_call_id.return_value = "call_test123"
parser = MockStreamableParser()
token_states = [
TokenState(channel=channel, recipient="functions.get_weather", text="")
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=False,
)
assert delta_message is not None
assert len(delta_message.tool_calls) == 1
tool_call = delta_message.tool_calls[0]
assert tool_call.id == "call_test123"
assert tool_call.type == "function"
assert tool_call.function.name == "get_weather"
assert tool_call.function.arguments == ""
assert tool_call.index == 0
assert tools_streamed is True
@pytest.mark.parametrize("channel", ["commentary", "analysis"])
def test_tool_call_argument_streaming(self, channel):
"""Test streaming tool call arguments (same recipient)."""
parser = MockStreamableParser()
args_text = '{"location": "Paris"}'
token_states = [
TokenState(
channel=channel,
recipient="functions.get_weather",
text=args_text,
)
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient="functions.get_weather",
include_reasoning=False,
)
assert delta_message is not None
tool_call = delta_message.tool_calls[0]
assert tool_call.id is None
assert tool_call.function.arguments == args_text
assert tool_call.index == 0
assert tools_streamed is True
@pytest.mark.parametrize("channel", ["commentary", "analysis"])
def test_tool_call_empty_arguments_returns_none(self, channel):
"""Test empty delta_text with same recipient returns None."""
parser = MockStreamableParser()
token_states = [
TokenState(channel=channel, recipient="functions.get_weather", text="")
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient="functions.get_weather",
include_reasoning=False,
)
assert delta_message is None
assert tools_streamed is False
def test_tool_call_index_from_previous_messages(self):
"""Test tool call index accounts for previous function messages."""
messages = [
MockMessage(channel="analysis", recipient=None), # Not counted
MockMessage(channel="commentary", recipient="functions.tool1"), # Counted
MockMessage(channel="final", recipient=None), # Not counted
]
parser = MockStreamableParser(messages=messages)
token_states = [
TokenState(
channel="commentary",
recipient="functions.tool2",
text="args",
)
]
delta_message, _ = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient="functions.tool2",
include_reasoning=False,
)
assert delta_message.tool_calls[0].index == 1
def test_returns_preambles_as_content(self):
"""Test that commentary with no recipient (preamble) is user content."""
parser = MockStreamableParser()
delta_text = "some text"
token_states = [
TokenState(channel="commentary", recipient=None, text=delta_text)
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=True,
)
assert delta_message.content == delta_text
assert tools_streamed is False
@pytest.mark.parametrize("channel", ["commentary", "analysis"])
@patch("vllm.entrypoints.openai.chat_completion.stream_harmony.make_tool_call_id")
def test_new_tool_call_dotted_function_name(self, mock_make_tool_call_id, channel):
mock_make_tool_call_id.return_value = "call_dotted123"
parser = MockStreamableParser()
token_states = [TokenState(channel=channel, recipient="math.sum", text="")]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=False,
)
assert delta_message is not None
assert len(delta_message.tool_calls) == 1
tool_call = delta_message.tool_calls[0]
assert tool_call.id == "call_dotted123"
assert tool_call.type == "function"
assert tool_call.function.name == "math.sum"
assert tool_call.function.arguments == ""
assert tool_call.index == 0
assert tools_streamed is True
@pytest.mark.parametrize(
"channel,recipient",
[
(None, None),
("unknown_channel", None),
("commentary", "browser.search"),
("commentary", "assistant"),
],
)
def test_returns_none_for_invalid_inputs(self, channel, recipient):
"""Test that invalid channel/recipient combinations return None."""
parser = MockStreamableParser()
token_states = [
TokenState(channel=channel, recipient=recipient, text="some text")
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=True,
)
assert delta_message is None
assert tools_streamed is False
def test_consecutive_token_grouping(self):
"""
Test that consecutive tokens with the same channel/recipient
are merged into a single processing group.
"""
parser = MockStreamableParser()
token_states = [
TokenState("final", None, "H"),
TokenState("final", None, "el"),
TokenState("final", None, "lo"),
TokenState("final", None, ","),
TokenState("final", None, " World"),
]
delta_message, _ = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=False,
)
assert delta_message is not None
assert delta_message.content == "Hello, World"
@patch("vllm.entrypoints.openai.chat_completion.stream_harmony.make_tool_call_id")
def test_complex_batch_permutation(self, mock_make_id):
"""
Test a complex permutation: Reasoning -> Tool Call -> Content.
This verifies that multiple distinct actions in one batch
are all captured in the single DeltaMessage.
"""
mock_make_id.return_value = "call_batch_test"
parser = MockStreamableParser()
token_states = [
# 1. Reasoning
TokenState("analysis", None, "Reasoning about query..."),
# 2. Tool Calling
TokenState("commentary", "functions.search", '{"query":'),
TokenState("commentary", "functions.search", ' "vllm"}'),
# 3. Final Content
TokenState("final", None, "."),
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=True,
)
assert delta_message is not None
assert delta_message.reasoning == "Reasoning about query..."
# We expect 2 objects for 1 logical tool call:
# 1. The definition (id, name, type)
# 2. The arguments payload
assert len(delta_message.tool_calls) == 2
header = delta_message.tool_calls[0]
payload = delta_message.tool_calls[1]
assert header.function.name == "search"
assert header.id == "call_batch_test"
assert header.index == 0
assert payload.index == 0
assert payload.function.arguments == '{"query": "vllm"}'
assert delta_message.content == "."
assert tools_streamed is True
@patch("vllm.entrypoints.openai.chat_completion.stream_harmony.make_tool_call_id")
def test_tool_call_index_consistency_with_ongoing_call(self, mock_make_id):
"""
Test that an ongoing tool call continuation and subsequent new calls
maintain correct indexing when interleaved with content.
"""
mock_make_id.side_effect = ["id_b", "id_c"]
messages = [
MockMessage(channel="commentary", recipient="functions.previous_tool")
]
parser = MockStreamableParser(messages=messages)
token_states = [
TokenState("commentary", "functions.tool_a", '{"key_a": "val_a"}'),
TokenState("final", None, "Thinking..."),
TokenState("commentary", "functions.tool_b", '{"key_b": "val_b"}'),
TokenState("final", None, " Thinking again..."),
TokenState("commentary", "functions.tool_c", '{"key_c": "val_c"}'),
]
delta_message, _ = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient="functions.tool_a",
include_reasoning=False,
)
assert delta_message is not None
tool_a_deltas = [t for t in delta_message.tool_calls if t.index == 1]
assert len(tool_a_deltas) > 0
assert tool_a_deltas[0].id is None
assert tool_a_deltas[0].function.arguments == '{"key_a": "val_a"}'
tool_b_header = next(t for t in delta_message.tool_calls if t.id == "id_b")
assert tool_b_header.index == 2
tool_b_args = next(
t for t in delta_message.tool_calls if t.index == 2 and t.id is None
)
assert tool_b_args.function.arguments == '{"key_b": "val_b"}'
tool_c_start = next(t for t in delta_message.tool_calls if t.id == "id_c")
assert tool_c_start.index == 3
tool_c_args = next(
t for t in delta_message.tool_calls if t.index == 3 and t.id is None
)
assert tool_c_args.function.arguments == '{"key_c": "val_c"}'
assert delta_message.content == "Thinking... Thinking again..."
class TestToolCallsOnNonStandardChannels:
"""Tool calls are detected by recipient, not channel.
Models sometimes emit tool calls on unexpected channels (e.g. ``comment``
instead of ``commentary``). These tests verify that the streaming delta
extraction is channel-agnostic for tool call detection.
"""
@patch("vllm.entrypoints.openai.chat_completion.stream_harmony.make_tool_call_id")
def test_prefixed_tool_call_on_comment_channel(self, mock_make_tool_call_id):
mock_make_tool_call_id.return_value = "call_comment_chan"
parser = MockStreamableParser()
token_states = [
TokenState(channel="comment", recipient="functions.get_weather", text="")
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=False,
)
assert delta_message is not None
assert len(delta_message.tool_calls) == 1
assert delta_message.tool_calls[0].function.name == "get_weather"
assert tools_streamed is True
@patch("vllm.entrypoints.openai.chat_completion.stream_harmony.make_tool_call_id")
def test_bare_tool_call_on_comment_channel(self, mock_make_tool_call_id):
mock_make_tool_call_id.return_value = "call_bare_comment"
parser = MockStreamableParser()
token_states = [TokenState(channel="comment", recipient="get_weather", text="")]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient=None,
include_reasoning=False,
)
assert delta_message is not None
assert len(delta_message.tool_calls) == 1
assert delta_message.tool_calls[0].function.name == "get_weather"
assert tools_streamed is True
def test_tool_call_arguments_on_comment_channel(self):
parser = MockStreamableParser()
args_text = '{"location": "Paris"}'
token_states = [
TokenState(
channel="comment", recipient="functions.get_weather", text=args_text
)
]
delta_message, tools_streamed = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient="functions.get_weather",
include_reasoning=False,
)
assert delta_message is not None
assert delta_message.tool_calls[0].function.arguments == args_text
assert tools_streamed is True
def test_base_index_counts_tool_calls_on_comment_channel(self):
messages = [
MockMessage(channel="comment", recipient="functions.tool1"),
]
parser = MockStreamableParser(messages=messages)
token_states = [
TokenState(channel="commentary", recipient="functions.tool2", text="args")
]
delta_message, _ = extract_harmony_streaming_delta(
harmony_parser=parser,
token_states=token_states,
prev_recipient="functions.tool2",
include_reasoning=False,
)
assert delta_message.tool_calls[0].index == 1
+309 -25
View File
@@ -94,16 +94,36 @@ def get_text(msg: Message) -> str:
return msg.content[0].text if msg.content else ""
def visible_segments(result) -> list[tuple[str | None, str | None, str]]:
def tool_call_tuples(tool_calls: list[FunctionCall] | None) -> list[tuple[str, str]]:
return [] if tool_calls is None else [(tc.name, tc.arguments) for tc in tool_calls]
def tool_call_headers(delta_message) -> list:
if delta_message is None or not delta_message.tool_calls:
return []
return [
(segment.channel, segment.recipient, segment.delta)
for segment in result.segments
if not segment.is_boundary and segment.delta
tool_call
for tool_call in delta_message.tool_calls
if tool_call.function and tool_call.function.name
]
def tool_call_tuples(tool_calls: list[FunctionCall] | None) -> list[tuple[str, str]]:
return [] if tool_calls is None else [(tc.name, tc.arguments) for tc in tool_calls]
def tool_call_payloads(delta_message) -> list:
if delta_message is None or not delta_message.tool_calls:
return []
return [
tool_call
for tool_call in delta_message.tool_calls
if tool_call.function and tool_call.function.arguments
]
def combined_tool_arguments(delta_message) -> dict[int, str]:
combined: dict[int, str] = {}
for tool_call in tool_call_payloads(delta_message):
combined.setdefault(tool_call.index, "")
combined[tool_call.index] += tool_call.function.arguments
return combined
class TestParse:
@@ -394,6 +414,276 @@ class TestParse:
]
class TestParseDelta:
def test_basic(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
first_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output("<|channel|>analysis<|message|>Thinking"),
request=chat_request,
finished=False,
)
second_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|end|><|start|>assistant<|channel|>final<|message|>Answer"
),
request=chat_request,
finished=False,
)
assert first_delta is not None
assert first_delta.reasoning == "Thinking"
assert first_delta.content is None
assert second_delta is not None
assert second_delta.content == "Answer"
assert second_delta.reasoning is None
def test_multi_token(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output("<|channel|>final<|message|>Hello, world!"),
request=chat_request,
finished=False,
)
assert delta is not None
assert delta.content == "Hello, world!"
assert delta.reasoning is None
assert not delta.tool_calls
@pytest.mark.parametrize("tool_channel", ["commentary", "analysis"])
def test_tool_call_split_across_deltas(
self, gpt_oss_tokenizer, chat_request, tool_channel
):
parser = HarmonyParser(gpt_oss_tokenizer)
first_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Thinking<|end|>"
f"<|start|>assistant to=functions.get_weather<|channel|>{tool_channel}"
'<|constrain|>json<|message|>{"location": '
),
request=chat_request,
finished=False,
)
second_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output('"Paris"}<|call|>'),
request=chat_request,
finished=False,
)
assert first_delta is not None
assert first_delta.reasoning == "Thinking"
assert first_delta.content is None
assert [tool.function.name for tool in tool_call_headers(first_delta)] == [
"get_weather"
]
assert combined_tool_arguments(first_delta) == {0: '{"location": '}
assert {tool.index for tool in first_delta.tool_calls} == {0}
assert second_delta is not None
assert second_delta.reasoning is None
assert second_delta.content is None
assert not tool_call_headers(second_delta)
assert combined_tool_arguments(second_delta) == {0: '"Paris"}'}
assert {tool.index for tool in second_delta.tool_calls} == {0}
def test_commentary_preamble_streaming(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>commentary<|message|>I'll search for that"
),
request=chat_request,
finished=False,
)
assert delta is not None
assert delta.content == "I'll search for that"
assert delta.reasoning is None
assert not delta.tool_calls
def test_multiple_choices(self, gpt_oss_tokenizer, chat_request):
parser_a = HarmonyParser(gpt_oss_tokenizer)
parser_b = HarmonyParser(gpt_oss_tokenizer)
delta_a = parser_a.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Check weather<|end|>"
"<|start|>assistant to=functions.get_weather<|channel|>commentary"
'<|constrain|>json<|message|>{"location": "Paris"}'
),
request=chat_request,
finished=False,
)
delta_b = parser_b.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Check time<|end|>"
"<|start|>assistant to=functions.get_time<|channel|>commentary"
'<|constrain|>json<|message|>{"timezone": "UTC"}'
),
request=chat_request,
finished=False,
)
assert [tool.function.name for tool in tool_call_headers(delta_a)] == [
"get_weather"
]
assert [tool.function.name for tool in tool_call_headers(delta_b)] == [
"get_time"
]
assert {tool.index for tool in delta_a.tool_calls} == {0}
assert {tool.index for tool in delta_b.tool_calls} == {0}
def test_dotted_function_name(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Compute this<|end|>"
"<|start|>assistant to=math.sum<|channel|>commentary"
'<|constrain|>json<|message|>{"a": 2, "b": 3}'
),
request=chat_request,
finished=False,
)
assert delta is not None
assert [tool.function.name for tool in tool_call_headers(delta)] == ["math.sum"]
assert {tool.index for tool in delta.tool_calls} == {0}
@pytest.mark.parametrize("recipient", ["assistant", "browser"])
def test_builtin_recipient_skipped(
self,
gpt_oss_tokenizer,
chat_request,
recipient,
):
parser = HarmonyParser(gpt_oss_tokenizer)
prompt = [Message.from_role_and_content(Role.USER, "Hello")]
response = [tool_call(recipient, "Ignore this", content_type=None)]
delta = parser.parse_delta(
delta_text="",
delta_token_ids=get_model_output_tokens(prompt, response),
request=chat_request,
finished=False,
)
assert delta is None
def test_cross_channel_with_tool(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Reasoning about query...<|end|>"
"<|start|>assistant to=functions.search<|channel|>commentary"
'<|constrain|>json<|message|>{"query": "vllm"}<|call|>'
"<|start|>assistant<|channel|>final<|message|>Done"
),
request=chat_request,
finished=False,
)
assert delta is not None
assert delta.reasoning == "Reasoning about query..."
assert delta.content == "Done"
assert [tool.function.name for tool in tool_call_headers(delta)] == ["search"]
assert combined_tool_arguments(delta) == {0: '{"query": "vllm"}'}
def test_tool_index_across_calls(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
first_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Thinking<|end|>"
"<|start|>assistant to=functions.get_weather<|channel|>commentary"
'<|constrain|>json<|message|>{"location": "Paris"}<|call|>'
),
request=chat_request,
finished=False,
)
second_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|start|>assistant to=functions.get_time<|channel|>commentary"
'<|constrain|>json<|message|>{"timezone": "UTC"}<|call|>'
),
request=chat_request,
finished=False,
)
assert [tool.index for tool in tool_call_headers(first_delta)] == [0]
assert [tool.index for tool in tool_call_headers(second_delta)] == [1]
assert [tool.function.name for tool in tool_call_headers(second_delta)] == [
"get_time"
]
def test_multi_tool_interleaved(self, gpt_oss_tokenizer, chat_request):
parser = HarmonyParser(gpt_oss_tokenizer)
first_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"<|channel|>analysis<|message|>Plan<|end|>"
"<|start|>assistant to=functions.tool_a<|channel|>commentary"
'<|constrain|>json<|message|>{"a": 1}<|call|>'
"<|start|>assistant to=functions.tool_b<|channel|>commentary"
'<|constrain|>json<|message|>{"b": '
),
request=chat_request,
finished=False,
)
second_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output("2"),
request=chat_request,
finished=False,
)
third_delta = parser.parse_delta(
delta_text="",
delta_token_ids=encode_output(
"}<|call|><|start|>assistant<|channel|>final<|message|>Done<|end|>"
"<|start|>assistant to=functions.tool_c<|channel|>commentary"
'<|constrain|>json<|message|>{"c": 3}'
),
request=chat_request,
finished=False,
)
assert [tool.index for tool in tool_call_headers(first_delta)] == [0, 1]
assert combined_tool_arguments(first_delta) == {
0: '{"a": 1}',
1: '{"b": ',
}
assert second_delta is not None
assert [tool.index for tool in tool_call_payloads(second_delta)] == [1]
assert combined_tool_arguments(second_delta) == {1: "2"}
assert third_delta is not None
assert third_delta.content == "Done"
assert combined_tool_arguments(third_delta) == {
1: "}",
2: '{"c": 3}',
}
assert [tool.index for tool in tool_call_headers(third_delta)] == [2]
class TestProcessChunk:
def test_empty(self, harmony_parser):
result = harmony_parser.process_chunk([])
@@ -405,7 +695,9 @@ class TestProcessChunk:
encode_output("<|channel|>final<|message|>Hello")
)
assert visible_segments(result) == [("final", None, "Hello")]
assert [
(s.channel, s.recipient, s.delta) for s in result.segments if s.delta
] == [("final", None, "Hello")]
def test_cross_channel(self, harmony_parser):
result = harmony_parser.process_chunk(
@@ -415,24 +707,13 @@ class TestProcessChunk:
)
)
assert visible_segments(result) == [
assert [
(s.channel, s.recipient, s.delta) for s in result.segments if s.delta
] == [
("analysis", None, "Think"),
("final", None, "Answer"),
]
def test_boundary_detection(self, harmony_parser):
result = harmony_parser.process_chunk(
encode_output("<|channel|>final<|message|>Done<|end|>")
)
boundary_segments = [
segment for segment in result.segments if segment.is_boundary
]
assert len(boundary_segments) == 1
assert boundary_segments[0].completed_message is not None
assert boundary_segments[0].completed_message.channel == "final"
assert get_text(boundary_segments[0].completed_message) == "Done"
def test_multi_boundary(self, harmony_parser):
result = harmony_parser.process_chunk(
encode_output(
@@ -442,11 +723,14 @@ class TestProcessChunk:
)
boundary_segments = [
segment for segment in result.segments if segment.is_boundary
segment
for segment in result.segments
if segment.completed_message is not None
]
assert [
get_text(segment.completed_message) for segment in boundary_segments
(segment.completed_message.channel, get_text(segment.completed_message))
for segment in boundary_segments
] == [
"One",
"Two",
("analysis", "One"),
("final", "Two"),
]
@@ -33,10 +33,6 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionStreamResponse,
ChatMessage,
)
from vllm.entrypoints.openai.chat_completion.stream_harmony import (
TokenState,
extract_harmony_streaming_delta,
)
from vllm.entrypoints.openai.engine.protocol import (
DeltaMessage,
ErrorResponse,
@@ -52,9 +48,6 @@ from vllm.entrypoints.openai.engine.serving import (
clamp_prompt_logprobs,
)
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.entrypoints.openai.parser.harmony_utils import (
get_streamable_parser_for_assistant,
)
from vllm.entrypoints.serve.utils.api_utils import get_max_tokens, should_include_usage
from vllm.entrypoints.serve.utils.request_logger import RequestLogger
from vllm.entrypoints.serve.utils.tool_calls_utils import (
@@ -155,7 +148,6 @@ class OpenAIServingChat(OpenAIServing):
if mc.generation_config not in ("auto", "vllm")
else getattr(mc, "override_generation_config", {}).get("max_new_tokens")
)
self.use_harmony = self.model_config.hf_config.model_type == "gpt_oss"
self.tool_call_id_type = get_tool_call_id_type(self.model_config)
# NOTE(woosuk): While OpenAI's chat completion API supports browsing
@@ -408,11 +400,6 @@ class OpenAIServingChat(OpenAIServing):
finish_reason_sent = [False] * num_choices
num_prompt_tokens = 0
num_cached_tokens = None
if self.use_harmony:
harmony_parsers = [
get_streamable_parser_for_assistant() for _ in range(num_choices)
]
harmony_tools_streamed = [False] * num_choices
tools_streamed = [False] * num_choices
if isinstance(request.tool_choice, ChatCompletionNamedToolChoiceParam):
@@ -443,6 +430,7 @@ class OpenAIServingChat(OpenAIServing):
]
for p in parsers:
if p is not None:
# NOTE: HarmonyParser ignores _stream_state (uses its own FSM).
p._stream_state.tool_call_id_type = self.tool_call_id_type
p._stream_state.history_tool_call_cnt = history_tool_call_cnt
else:
@@ -572,32 +560,7 @@ class OpenAIServingChat(OpenAIServing):
else:
logprobs = None
if self.use_harmony:
harmony_parser = harmony_parsers[i]
prev_recipient = harmony_parser.current_recipient
# Track accumulated content per token with their state
token_states: list[TokenState] = []
for token_id in output.token_ids:
harmony_parser.process(token_id)
token_delta = harmony_parser.last_content_delta or ""
token_states.append(
TokenState(
harmony_parser.current_channel,
harmony_parser.current_recipient,
token_delta,
)
)
delta_text = "".join(delta for _, _, delta in token_states)
cur_channel = harmony_parser.current_channel
# handle the case where several tokens where generated at once
# including the final token, leading to a delta in the text
# but the current channel to be empty (start state)
if not cur_channel and delta_text:
cur_channel = "final"
else:
delta_text = output.text
delta_text = output.text
if (
not delta_text
@@ -609,17 +572,7 @@ class OpenAIServingChat(OpenAIServing):
delta_message: DeltaMessage | None
if self.use_harmony:
delta_message, tools_streamed_flag = (
extract_harmony_streaming_delta(
harmony_parser=harmony_parser,
token_states=token_states,
prev_recipient=prev_recipient,
include_reasoning=request.include_reasoning,
)
)
harmony_tools_streamed[i] |= tools_streamed_flag
elif parser is not None:
if parser is not None:
delta_message = parser.parse_delta(
delta_text=delta_text,
delta_token_ids=as_list(output.token_ids),
@@ -627,8 +580,20 @@ class OpenAIServingChat(OpenAIServing):
prompt_token_ids=res.prompt_token_ids,
finished=output.finish_reason is not None,
)
if delta_message and delta_message.tool_calls:
tools_streamed[i] = True
if delta_message is not None:
if delta_message.tool_calls:
tools_streamed[i] = True
if (
delta_message.reasoning
and not request.include_reasoning
):
delta_message.reasoning = None
if not (
delta_message.content or delta_message.tool_calls
):
delta_message = None
# handle streaming just a content delta (no parsers)
else:
delta_message = DeltaMessage(content=delta_text)
@@ -706,9 +671,7 @@ class OpenAIServingChat(OpenAIServing):
# finish_reason is:
# "tool_calls" for "auto" or "required" tool calls,
# and "stop" for named tool calls.
if (tools_streamed[i] and not tool_choice_function_name) or (
self.use_harmony and harmony_tools_streamed[i]
):
if tools_streamed[i] and not tool_choice_function_name:
finish_reason_ = "tool_calls"
else:
finish_reason_ = (
@@ -1,167 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Harmony-specific streaming delta extraction for chat completions.
This module handles the extraction of DeltaMessage objects from
harmony parser state during streaming chat completions.
"""
from typing import NamedTuple
from openai_harmony import StreamableParser
from vllm.entrypoints.chat_utils import make_tool_call_id
from vllm.entrypoints.openai.engine.protocol import (
DeltaFunctionCall,
DeltaMessage,
DeltaToolCall,
)
from vllm.entrypoints.openai.parser.harmony_utils import (
extract_function_from_recipient,
is_function_recipient,
)
class TokenState(NamedTuple):
channel: str | None
recipient: str | None
text: str
def extract_harmony_streaming_delta(
harmony_parser: StreamableParser,
token_states: list[TokenState],
prev_recipient: str | None,
include_reasoning: bool,
) -> tuple[DeltaMessage | None, bool]:
"""
Extract a DeltaMessage from harmony parser state during streaming.
Args:
harmony_parser: The StreamableParser instance tracking parse state
token_states: List of TokenState tuples for each token
prev_recipient: Previous recipient for detecting tool call transitions
include_reasoning: Whether to include reasoning content
Returns:
A tuple of (DeltaMessage or None, tools_streamed_flag)
"""
if not token_states:
return None, False
tools_streamed = False
# Group consecutive tokens with same channel/recipient
groups: list[TokenState] = []
current_channel = token_states[0].channel
current_recipient = token_states[0].recipient
current_text = token_states[0].text
for i in range(1, len(token_states)):
state = token_states[i]
if state.channel == current_channel and state.recipient == current_recipient:
current_text += state.text
else:
groups.append(TokenState(current_channel, current_recipient, current_text))
current_channel = state.channel
current_recipient = state.recipient
current_text = state.text
groups.append(TokenState(current_channel, current_recipient, current_text))
# Process each group and create delta messages
delta_message = None
combined_content = ""
combined_reasoning = ""
tool_messages = []
content_encountered = False
# Calculate base_index once before the loop
# This counts completed tool calls in messages
base_index = 0
for msg in harmony_parser.messages:
if msg.recipient and is_function_recipient(msg.recipient):
base_index += 1
# If there's an ongoing tool call from previous chunk,
# the next new tool call starts at base_index + 1
if prev_recipient and is_function_recipient(prev_recipient):
next_tool_index = base_index + 1
# Ongoing call is at base_index
ongoing_tool_index = base_index
else:
# No ongoing call, next new call is at base_index
next_tool_index = base_index
ongoing_tool_index = None
for group in groups:
if group.channel == "final":
combined_content += group.text
content_encountered = True
elif group.recipient and is_function_recipient(group.recipient):
opened_new_call = False
if prev_recipient != group.recipient:
# New tool call - emit the opening message
tool_name = extract_function_from_recipient(group.recipient)
tool_messages.append(
DeltaToolCall(
id=make_tool_call_id(),
type="function",
function=DeltaFunctionCall(
name=tool_name,
arguments="",
),
index=next_tool_index,
)
)
opened_new_call = True
prev_recipient = group.recipient
# Increment for subsequent new tool calls
next_tool_index += 1
if group.text:
# Stream arguments for the ongoing tool call
if opened_new_call:
# Just opened in this group
tool_call_index = next_tool_index - 1
else:
# Continuing from previous chunk
# If ongoing_tool_index is None here, it means
# we're continuing a call but prev_recipient
# wasn't a function. Use base_index.
tool_call_index = (
ongoing_tool_index
if ongoing_tool_index is not None
else base_index
)
tool_messages.append(
DeltaToolCall(
index=tool_call_index,
function=DeltaFunctionCall(arguments=group.text),
)
)
elif group.channel == "commentary" and group.recipient is None:
# Tool call preambles meant to be shown to the user
combined_content += group.text
content_encountered = True
elif group.channel == "analysis" and include_reasoning:
combined_reasoning += group.text
# Combine all non-empty fields into a single message
if content_encountered or combined_reasoning or tool_messages:
delta_kwargs: dict[str, str | list[DeltaToolCall]] = {}
if content_encountered:
delta_kwargs["content"] = combined_content
if combined_reasoning:
delta_kwargs["reasoning"] = combined_reasoning
if tool_messages:
delta_kwargs["tool_calls"] = tool_messages
tools_streamed = True
delta_message = DeltaMessage(**delta_kwargs)
else:
delta_message = None
return delta_message, tools_streamed
+67 -15
View File
@@ -9,9 +9,12 @@ from dataclasses import dataclass
from enum import Enum, auto
from typing import TYPE_CHECKING, NamedTuple
from vllm.entrypoints.chat_utils import make_tool_call_id
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.engine.protocol import (
DeltaFunctionCall,
DeltaMessage,
DeltaToolCall,
FunctionCall,
)
from vllm.entrypoints.openai.parser.harmony_utils import (
@@ -52,7 +55,6 @@ class Segment(NamedTuple):
channel: str | None
recipient: str | None
delta: str
is_boundary: bool = False
completed_message: Message | None = None
@@ -81,10 +83,8 @@ class HarmonyParser(DelegatingParser):
)
self._harmony_parser = get_streamable_parser_for_assistant()
@property
def messages(self) -> list[Message]:
return self._harmony_parser.messages
self._next_tool_call_index = 0
self._num_processed_messages = 0
@property
def state(self) -> HarmonyStreamState:
@@ -194,17 +194,69 @@ class HarmonyParser(DelegatingParser):
*,
finished: bool,
) -> DeltaMessage | None:
raise NotImplementedError(
"HarmonyParser streaming parsing is deferred. "
"Use the existing harmony streaming path."
)
prev_recipient = self.current_recipient
result = self.process_chunk(delta_token_ids)
combined_content = ""
combined_reasoning = ""
tool_messages: list[DeltaToolCall] = []
for segment in result.segments:
if segment.completed_message is not None:
prev_recipient = None
continue
segment_type = _SegmentType.from_channel_and_recipient(
segment.channel, segment.recipient
)
match segment_type:
case _SegmentType.REASONING:
combined_reasoning += segment.delta
case _SegmentType.CONTENT:
combined_content += segment.delta
case _SegmentType.TOOL:
assert segment.recipient is not None
if prev_recipient != segment.recipient:
tool_name = extract_function_from_recipient(segment.recipient)
tool_messages.append(
DeltaToolCall(
# HarmonyParser does not use _stream_state;
# "random" tool_call_id_type is always used
id=make_tool_call_id(),
type="function",
function=DeltaFunctionCall(
name=tool_name,
arguments=segment.delta,
),
index=self._next_tool_call_index,
)
)
self._next_tool_call_index += 1
prev_recipient = segment.recipient
elif segment.delta:
tool_call_index = self._next_tool_call_index - 1
tool_messages.append(
DeltaToolCall(
index=tool_call_index,
function=DeltaFunctionCall(arguments=segment.delta),
)
)
if not combined_content and not combined_reasoning and not tool_messages:
return None
delta_message = DeltaMessage()
if combined_content:
delta_message.content = combined_content
if combined_reasoning:
delta_message.reasoning = combined_reasoning
if tool_messages:
delta_message.tool_calls = tool_messages
return delta_message
def process_chunk(self, token_ids: Sequence[int]) -> ChunkResult:
if not token_ids:
return ChunkResult(segments=[], reasoning_token_count=0)
from openai_harmony import StreamState
segments: list[Segment] = []
reasoning_token_count = 0
for token_id in token_ids:
@@ -213,9 +265,10 @@ class HarmonyParser(DelegatingParser):
recipient = self.current_recipient
delta = self._harmony_parser.last_content_delta or ""
completed_message = None
is_boundary = self.state == StreamState.EXPECT_START
if is_boundary and self.messages:
completed_message = self.messages[-1]
_messages = self._harmony_parser.messages
if len(_messages) > self._num_processed_messages:
completed_message = _messages[self._num_processed_messages]
self._num_processed_messages += 1
if channel == "analysis" or (
channel == "commentary" and recipient is not None
@@ -227,7 +280,6 @@ class HarmonyParser(DelegatingParser):
channel=channel,
recipient=recipient,
delta=delta,
is_boundary=is_boundary,
completed_message=completed_message,
)
)