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 7, 19:00 UTC
[Bugfix] Fix Harmony preamble visibility in Responses API (#32114)
Signed-off-by: Pushkar Patel <git@thepushkarp.com> Signed-off-by: pupa <pupa@users.noreply.github.com>
pushkar committed
Feb 25, 2026 at 21:38 UTC
5d18bf8b32837275d7656e2ae8b5c684274234d2
10 files changed
+341
-63
tests/entrypoints/openai/parser/test_harmony_utils.py
+109
-14
@@ -2,7 +2,11 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import pytest
5
-from openai.types.responses import ResponseFunctionToolCall, ResponseReasoningItem
5
+from openai.types.responses import (
6
+ ResponseFunctionToolCall,
7
+ ResponseOutputMessage,
8
+ ResponseReasoningItem,
9
+)
10
from openai.types.responses.response_output_item import McpCall
11
from openai_harmony import Author, Message, Role, TextContent
12
@@ -10,6 +14,7 @@ from tests.entrypoints.openai.utils import verify_harmony_messages
14
from vllm.entrypoints.openai.parser.harmony_utils import (
15
auto_drop_analysis_messages,
16
get_encoding,
17
+ get_system_message,
18
has_custom_tools,
19
parse_chat_input_to_harmony_message,
20
parse_chat_output,
@@ -840,15 +845,58 @@ class TestParseChatOutput:
845
assert reasoning == "I've thought hard about this."
846
assert final_content == "The answer is 4."
847
848
+ def test_parse_chat_output_commentary_with_recipient_excluded(self) -> None:
849
+ """Commentary with a recipient (tool call) should not appear in
850
+ final_content — those are handled separately by the tool parser.
851
+
852
+ The first message is a preamble (visible), the second is a tool
853
+ call (excluded). Only the preamble should appear in final_content.
854
+ """
855
+ harmony_str = (
856
+ "<|channel|>commentary"
857
+ "<|message|>Let me check the weather.<|end|>"
858
+ "<|start|>assistant to=functions.get_weather"
859
+ "<|channel|>commentary"
860
+ '<|message|>{"location": "SF"}<|end|>'
861
+ )
862
+ token_ids = get_encoding().encode(harmony_str, allowed_special="all")
863
+ reasoning, final_content, _ = parse_chat_output(token_ids)
864
+ assert reasoning is None
865
+ assert final_content == "Let me check the weather."
866
+
867
+ def test_parse_chat_output_interrupted_preamble(self) -> None:
868
+ """Partial/interrupted preamble (commentary without recipient) should
869
+ appear in final_content, not reasoning."""
870
+ harmony_str = "<|channel|>commentary<|message|>I'll search for that"
871
+ token_ids = get_encoding().encode(harmony_str, allowed_special="all")
872
+ reasoning, final_content, _ = parse_chat_output(token_ids)
873
+ assert reasoning is None
874
+ assert final_content == "I'll search for that"
875
+
876
+ def test_parse_chat_output_preamble_then_final(self) -> None:
877
+ """Preamble followed by a final message should both appear in
878
+ final_content, joined by newline."""
879
+ harmony_str = (
880
+ "<|channel|>commentary"
881
+ "<|message|>Let me look that up.<|end|>"
882
+ "<|start|>assistant<|channel|>final"
883
+ "<|message|>The answer is 42.<|end|>"
884
+ )
885
+ token_ids = get_encoding().encode(harmony_str, allowed_special="all")
886
+ reasoning, final_content, _ = parse_chat_output(token_ids)
887
+ assert reasoning is None
888
+ assert final_content == "Let me look that up.\nThe answer is 42."
889
+
890
891
class TestParseOutputMessage:
892
"""Tests for parse_output_message function."""
893
847
- def test_commentary_with_no_recipient_creates_reasoning(self):
848
- """Test that commentary with recipient=None (preambles) creates reasoning items.
894
+ def test_commentary_with_no_recipient_creates_message(self):
895
+ """Test that commentary with recipient=None (preambles) creates message items.
896
850
- Per Harmony format, commentary channel can contain preambles to calling
851
- multiple functions - explanatory text with no recipient.
897
+ Per Harmony format, preambles are intended to be shown to end-users,
898
+ unlike analysis channel content which is hidden reasoning.
899
+ See: https://cookbook.openai.com/articles/openai-harmony
900
"""
901
message = Message.from_role_and_content(
902
Role.ASSISTANT, "I will now search for the weather information."
@@ -859,13 +907,16 @@ class TestParseOutputMessage:
907
output_items = parse_output_message(message)
908
909
assert len(output_items) == 1
862
- assert isinstance(output_items[0], ResponseReasoningItem)
863
- assert output_items[0].type == "reasoning"
910
+ assert isinstance(output_items[0], ResponseOutputMessage)
911
+ assert output_items[0].type == "message"
912
+ assert output_items[0].role == "assistant"
913
+ assert output_items[0].status == "completed"
914
+ assert len(output_items[0].content) == 1
915
+ assert output_items[0].content[0].type == "output_text"
916
assert (
917
output_items[0].content[0].text
918
== "I will now search for the weather information."
919
)
868
- assert output_items[0].content[0].type == "reasoning_text"
920
921
def test_commentary_with_function_recipient_creates_function_call(self):
922
"""Test commentary with recipient='functions.X' creates function calls."""
@@ -944,7 +995,7 @@ class TestParseOutputMessage:
995
output_items = parse_output_message(message)
996
997
assert len(output_items) == 1
947
- assert isinstance(output_items[0], ResponseReasoningItem)
998
+ assert isinstance(output_items[0], ResponseOutputMessage)
999
assert output_items[0].content[0].text == ""
1000
1001
def test_commentary_with_multiple_contents_and_no_recipient(self):
@@ -958,10 +1009,13 @@ class TestParseOutputMessage:
1009
1010
output_items = parse_output_message(message)
1011
961
- assert len(output_items) == 2
962
- assert all(isinstance(item, ResponseReasoningItem) for item in output_items)
1012
+ # _parse_final_message returns single ResponseOutputMessage with
1013
+ # multiple contents
1014
+ assert len(output_items) == 1
1015
+ assert isinstance(output_items[0], ResponseOutputMessage)
1016
+ assert len(output_items[0].content) == 2
1017
assert output_items[0].content[0].text == "Step 1: Analyze the request"
964
- assert output_items[1].content[0].text == "Step 2: Prepare to call functions"
1018
+ assert output_items[0].content[1].text == "Step 2: Prepare to call functions"
1019
1020
def test_commentary_with_multiple_function_calls(self):
1021
"""Test multiple function calls in commentary channel."""
@@ -1133,7 +1187,7 @@ def test_parse_remaining_state_commentary_channel() -> None:
1187
assert mcp_items[0].status == "in_progress"
1188
1189
# Test 3: Built-in tool (python)
1136
- # should NOT return MCP call, falls through to reasoning
1190
+ # should NOT return MCP call, returns reasoning (internal tool interaction)
1191
parser_builtin = Mock()
1192
parser_builtin.current_content = "print('hello')"
1193
parser_builtin.current_role = Role.ASSISTANT
@@ -1142,11 +1196,26 @@ def test_parse_remaining_state_commentary_channel() -> None:
1196
1197
builtin_items = parse_remaining_state(parser_builtin)
1198
1145
- # Should fall through to reasoning logic
1199
+ # Built-in tools explicitly return reasoning
1200
assert len(builtin_items) == 1
1201
assert not isinstance(builtin_items[0], McpCall)
1202
assert builtin_items[0].type == "reasoning"
1203
1204
+ # Test 4: No recipient (preamble) → should return message, not reasoning
1205
+ parser_preamble = Mock()
1206
+ parser_preamble.current_content = "I'll search for that information now."
1207
+ parser_preamble.current_role = Role.ASSISTANT
1208
+ parser_preamble.current_channel = "commentary"
1209
+ parser_preamble.current_recipient = None
1210
+
1211
+ preamble_items = parse_remaining_state(parser_preamble)
1212
+
1213
+ assert len(preamble_items) == 1
1214
+ assert isinstance(preamble_items[0], ResponseOutputMessage)
1215
+ assert preamble_items[0].type == "message"
1216
+ assert preamble_items[0].content[0].text == "I'll search for that information now."
1217
+ assert preamble_items[0].status == "incomplete" # streaming
1218
+
1219
1220
def test_parse_remaining_state_analysis_channel() -> None:
1221
"""Test parse_remaining_state with analysis channel and various recipients."""
@@ -1199,3 +1268,29 @@ def test_parse_remaining_state_analysis_channel() -> None:
1268
assert len(builtin_items) == 1
1269
assert not isinstance(builtin_items[0], McpCall)
1270
assert builtin_items[0].type == "reasoning"
1271
+
1272
+
1273
+class TestGetSystemMessage:
1274
+ """Tests for get_system_message channel configuration."""
1275
+
1276
+ def test_commentary_channel_present_without_custom_tools(self) -> None:
1277
+ """Commentary channel must be valid even without custom tools."""
1278
+ sys_msg = get_system_message(with_custom_tools=False)
1279
+ valid_channels = sys_msg.content[0].channel_config.valid_channels
1280
+ assert "commentary" in valid_channels
1281
+
1282
+ def test_commentary_channel_present_with_custom_tools(self) -> None:
1283
+ """Commentary channel present when custom tools are enabled."""
1284
+ sys_msg = get_system_message(with_custom_tools=True)
1285
+ valid_channels = sys_msg.content[0].channel_config.valid_channels
1286
+ assert "commentary" in valid_channels
1287
+
1288
+ def test_all_standard_channels_present(self) -> None:
1289
+ """All three standard Harmony channels should always be valid."""
1290
+ for with_tools in (True, False):
1291
+ sys_msg = get_system_message(with_custom_tools=with_tools)
1292
+ valid_channels = sys_msg.content[0].channel_config.valid_channels
1293
+ for channel in ("analysis", "commentary", "final"):
1294
+ assert channel in valid_channels, (
1295
+ f"{channel} missing when with_custom_tools={with_tools}"
1296
+ )
tests/entrypoints/openai/responses/test_harmony.py
+8
-9
@@ -712,15 +712,14 @@ async def test_function_calling_required(client: OpenAI, model_name: str):
712
async def test_system_message_with_tools(client: OpenAI, model_name: str):
713
from vllm.entrypoints.openai.parser.harmony_utils import get_system_message
714
715
- # Test with custom tools enabled - commentary channel should be available
716
- sys_msg = get_system_message(with_custom_tools=True)
717
- valid_channels = sys_msg.content[0].channel_config.valid_channels
718
- assert "commentary" in valid_channels
719
-
720
- # Test with custom tools disabled - commentary channel should be removed
721
- sys_msg = get_system_message(with_custom_tools=False)
722
- valid_channels = sys_msg.content[0].channel_config.valid_channels
723
- assert "commentary" not in valid_channels
715
+ # Commentary channel should always be present (needed for preambles)
716
+ # regardless of whether custom tools are enabled
717
+ for with_tools in (True, False):
718
+ sys_msg = get_system_message(with_custom_tools=with_tools)
719
+ valid_channels = sys_msg.content[0].channel_config.valid_channels
720
+ assert "commentary" in valid_channels, (
721
+ f"commentary channel missing when with_custom_tools={with_tools}"
722
+ )
723
724
725
@pytest.mark.asyncio
tests/entrypoints/openai/responses/test_mcp_tools.py
+2
-2
@@ -172,13 +172,13 @@ class TestMCPEnabled:
172
recipient = message.get("recipient")
173
if recipient and recipient.startswith("python"):
174
tool_call_found = True
175
- assert message.get("channel") == "analysis"
175
+ assert message.get("channel") == "commentary"
176
author = message.get("author", {})
177
if author.get("role") == "tool" and (author.get("name") or "").startswith(
178
"python"
179
):
180
tool_response_found = True
181
- assert message.get("channel") == "analysis"
181
+ assert message.get("channel") == "commentary"
182
183
assert tool_call_found, (
184
f"No Python tool call found. "
tests/entrypoints/openai/test_serving_chat_stream_harmony.py
+4
-10
@@ -180,20 +180,13 @@ class TestExtractHarmonyStreamingDelta:
180
181
assert delta_message.tool_calls[0].index == 1
182
183
- @pytest.mark.parametrize(
184
- "channel,recipient",
185
- [
186
- ("commentary", None),
187
- ("commentary", "browser.search"),
188
- ],
189
- )
190
- def test_returns_tool_call_preambles(self, channel, recipient):
191
- """Test that invalid tool recipient on commentary is treated as content."""
183
+ def test_returns_preambles_as_content(self):
184
+ """Test that commentary with no recipient (preamble) is user content."""
185
parser = MockStreamableParser()
186
delta_text = "some text"
187
188
token_states = [
196
- TokenState(channel=channel, recipient=recipient, text=delta_text)
189
+ TokenState(channel="commentary", recipient=None, text=delta_text)
190
]
191
192
delta_message, tools_streamed = extract_harmony_streaming_delta(
@@ -211,6 +204,7 @@ class TestExtractHarmonyStreamingDelta:
204
[
205
(None, None),
206
("unknown_channel", None),
207
+ ("commentary", "browser.search"),
208
],
209
)
210
def test_returns_none_for_invalid_inputs(self, channel, recipient):
tests/entrypoints/openai/test_serving_responses.py
+115
@@ -26,6 +26,9 @@ from vllm.entrypoints.openai.responses.serving import (
26
_extract_allowed_tools_from_mcp_requests,
27
extract_tool_types,
28
)
29
+from vllm.entrypoints.openai.responses.streaming_events import (
30
+ StreamingState,
31
+)
32
from vllm.inputs.data import TokensPrompt
33
from vllm.outputs import CompletionOutput, RequestOutput
34
from vllm.sampling_params import SamplingParams
@@ -439,3 +442,115 @@ class TestExtractAllowedToolsFromMcpRequests:
442
"server1": ["tool1"],
443
"server2": ["tool2"],
444
}
445
+
446
+
447
+class TestHarmonyPreambleStreaming:
448
+ """Tests for preamble (commentary with no recipient) streaming events."""
449
+
450
+ @staticmethod
451
+ def _make_ctx(*, channel, recipient, delta="hello"):
452
+ """Build a lightweight mock StreamingHarmonyContext."""
453
+ ctx = MagicMock()
454
+ ctx.last_content_delta = delta
455
+ ctx.parser.current_channel = channel
456
+ ctx.parser.current_recipient = recipient
457
+ return ctx
458
+
459
+ @staticmethod
460
+ def _make_previous_item(*, channel, recipient, text="preamble text"):
461
+ """Build a lightweight mock previous_item (openai_harmony Message)."""
462
+ content_part = MagicMock()
463
+ content_part.text = text
464
+ item = MagicMock()
465
+ item.channel = channel
466
+ item.recipient = recipient
467
+ item.content = [content_part]
468
+ return item
469
+
470
+ def test_preamble_delta_emits_text_events(self) -> None:
471
+ """commentary + recipient=None should emit output_text.delta events."""
472
+ from vllm.entrypoints.openai.responses.streaming_events import (
473
+ emit_content_delta_events,
474
+ )
475
+
476
+ ctx = self._make_ctx(channel="commentary", recipient=None)
477
+ state = StreamingState()
478
+
479
+ events = emit_content_delta_events(ctx, state)
480
+
481
+ type_names = [e.type for e in events]
482
+ assert "response.output_text.delta" in type_names
483
+ assert "response.output_item.added" in type_names
484
+
485
+ def test_preamble_delta_second_token_no_added(self) -> None:
486
+ """Second preamble token should emit delta only, not added again."""
487
+ from vllm.entrypoints.openai.responses.streaming_events import (
488
+ emit_content_delta_events,
489
+ )
490
+
491
+ ctx = self._make_ctx(channel="commentary", recipient=None, delta="w")
492
+ state = StreamingState()
493
+ state.sent_output_item_added = True
494
+ state.current_item_id = "msg_test"
495
+ state.current_content_index = 0
496
+
497
+ events = emit_content_delta_events(ctx, state)
498
+
499
+ type_names = [e.type for e in events]
500
+ assert "response.output_text.delta" in type_names
501
+ assert "response.output_item.added" not in type_names
502
+
503
+ def test_commentary_with_function_recipient_not_preamble(self) -> None:
504
+ """commentary + recipient='functions.X' must NOT use preamble path."""
505
+ from vllm.entrypoints.openai.responses.streaming_events import (
506
+ emit_content_delta_events,
507
+ )
508
+
509
+ ctx = self._make_ctx(
510
+ channel="commentary",
511
+ recipient="functions.get_weather",
512
+ )
513
+ state = StreamingState()
514
+
515
+ events = emit_content_delta_events(ctx, state)
516
+
517
+ type_names = [e.type for e in events]
518
+ assert "response.output_text.delta" not in type_names
519
+
520
+ def test_preamble_done_emits_text_done_events(self) -> None:
521
+ """Completed preamble should emit text done + content_part done +
522
+ output_item done, same shape as final channel."""
523
+ from vllm.entrypoints.openai.responses.streaming_events import (
524
+ emit_previous_item_done_events,
525
+ )
526
+
527
+ previous = self._make_previous_item(channel="commentary", recipient=None)
528
+ state = StreamingState()
529
+ state.current_item_id = "msg_test"
530
+ state.current_output_index = 0
531
+ state.current_content_index = 0
532
+
533
+ events = emit_previous_item_done_events(previous, state)
534
+
535
+ type_names = [e.type for e in events]
536
+ assert "response.output_text.done" in type_names
537
+ assert "response.content_part.done" in type_names
538
+ assert "response.output_item.done" in type_names
539
+
540
+ def test_commentary_with_recipient_no_preamble_done(self) -> None:
541
+ """commentary + recipient='functions.X' should route to function call
542
+ done, not preamble done."""
543
+ from vllm.entrypoints.openai.responses.streaming_events import (
544
+ emit_previous_item_done_events,
545
+ )
546
+
547
+ previous = self._make_previous_item(
548
+ channel="commentary", recipient="functions.get_weather"
549
+ )
550
+ state = StreamingState()
551
+ state.current_item_id = "fc_test"
552
+
553
+ events = emit_previous_item_done_events(previous, state)
554
+
555
+ type_names = [e.type for e in events]
556
+ assert "response.output_text.done" not in type_names
tests/entrypoints/test_context.py
+38
@@ -236,6 +236,44 @@ def test_reasoning_tokens_counting(mock_parser):
236
assert context.num_output_tokens == 4
237
238
239
+def test_preamble_tokens_not_counted_as_reasoning(mock_parser):
240
+ """Preambles (commentary with no recipient) are visible user text,
241
+ not hidden reasoning. They must NOT inflate num_reasoning_tokens."""
242
+ context = HarmonyContext(messages=[], available_tools=[])
243
+
244
+ mock_parser.current_channel = "commentary"
245
+ mock_parser.current_recipient = None # preamble
246
+
247
+ mock_output = create_mock_request_output(
248
+ prompt_token_ids=[1, 2, 3],
249
+ output_token_ids=[4, 5, 6],
250
+ num_cached_tokens=0,
251
+ )
252
+ context.append_output(mock_output)
253
+
254
+ assert context.num_reasoning_tokens == 0
255
+ assert context.num_output_tokens == 3
256
+
257
+
258
+def test_commentary_with_recipient_counted_as_reasoning(mock_parser):
259
+ """Commentary directed at a tool (recipient != None) is hidden from
260
+ the user, so it should still count as reasoning tokens."""
261
+ context = HarmonyContext(messages=[], available_tools=[])
262
+
263
+ mock_parser.current_channel = "commentary"
264
+ mock_parser.current_recipient = "python"
265
+
266
+ mock_output = create_mock_request_output(
267
+ prompt_token_ids=[1, 2, 3],
268
+ output_token_ids=[4, 5, 6],
269
+ num_cached_tokens=0,
270
+ )
271
+ context.append_output(mock_output)
272
+
273
+ assert context.num_reasoning_tokens == 3
274
+ assert context.num_output_tokens == 3
275
+
276
+
277
def test_zero_tokens_edge_case():
278
"""Test behavior with all zero token counts."""
279
context = HarmonyContext(messages=[], available_tools=[])
vllm/entrypoints/openai/chat_completion/stream_harmony.py
+1
-1
@@ -147,7 +147,7 @@ def extract_harmony_streaming_delta(
147
function=DeltaFunctionCall(arguments=group.text),
148
)
149
)
150
- elif group.channel == "commentary":
150
+ elif group.channel == "commentary" and group.recipient is None:
151
# Tool call preambles meant to be shown to the user
152
combined_content += group.text
153
content_encountered = True
vllm/entrypoints/openai/parser/harmony_utils.py
+52
-23
@@ -26,7 +26,6 @@ from openai.types.responses.response_reasoning_item import (
26
from openai.types.responses.tool import Tool
27
from openai_harmony import (
28
Author,
29
- ChannelConfig,
29
Conversation,
30
DeveloperContent,
31
HarmonyEncodingName,
@@ -126,13 +125,6 @@ def get_system_message(
125
sys_msg_content = sys_msg_content.with_tools(python_description)
126
if container_description is not None:
127
sys_msg_content = sys_msg_content.with_tools(container_description)
129
- if not with_custom_tools:
130
- channel_config = sys_msg_content.channel_config
131
- invalid_channel = "commentary"
132
- new_config = ChannelConfig.require_channels(
133
- [c for c in channel_config.valid_channels if c != invalid_channel]
134
- )
135
- sys_msg_content = sys_msg_content.with_channel_config(new_config)
128
sys_msg = Message.from_role_and_content(Role.SYSTEM, sys_msg_content)
129
return sys_msg
130
@@ -686,6 +678,22 @@ def _parse_mcp_call(message: Message, recipient: str) -> list[ResponseOutputItem
678
return output_items
679
680
681
+def _parse_message_no_recipient(
682
+ message: Message,
683
+) -> list[ResponseOutputItem]:
684
+ """Parse a Harmony message with no recipient based on its channel."""
685
+ if message.channel == "analysis":
686
+ return _parse_reasoning(message)
687
+
688
+ if message.channel in ("commentary", "final"):
689
+ # Per Harmony format, preambles (commentary with no recipient) and
690
+ # final channel content are both intended to be shown to end-users.
691
+ # See: https://cookbook.openai.com/articles/openai-harmony
692
+ return [_parse_final_message(message)]
693
+
694
+ raise ValueError(f"Unknown channel: {message.channel}")
695
+
696
+
697
def parse_output_message(message: Message) -> list[ResponseOutputItem]:
698
"""
699
Parse a Harmony message into a list of output response items.
@@ -717,19 +725,8 @@ def parse_output_message(message: Message) -> list[ResponseOutputItem]:
725
output_items.extend(_parse_mcp_call(message, recipient))
726
727
# No recipient - handle based on channel for non-tool messages
720
- elif message.channel == "analysis":
721
- output_items.extend(_parse_reasoning(message))
722
-
723
- elif message.channel == "commentary":
724
- # Per Harmony format, commentary channel can contain preambles to calling
725
- # multiple functions - explanatory text with no recipient
726
- output_items.extend(_parse_reasoning(message))
727
-
728
- elif message.channel == "final":
729
- output_items.append(_parse_final_message(message))
730
-
728
else:
732
- raise ValueError(f"Unknown channel: {message.channel}")
729
+ output_items.extend(_parse_message_no_recipient(message))
730
731
return output_items
732
@@ -786,7 +783,26 @@ def parse_remaining_state(parser: StreamableParser) -> list[ResponseOutputItem]:
783
)
784
]
785
789
- if parser.current_channel in ("commentary", "analysis"):
786
+ if parser.current_channel == "commentary":
787
+ # Per Harmony format, preambles (commentary with no recipient) are
788
+ # intended to be shown to end-users, unlike analysis channel content.
789
+ output_text = ResponseOutputText(
790
+ text=parser.current_content,
791
+ annotations=[],
792
+ type="output_text",
793
+ logprobs=None,
794
+ )
795
+ return [
796
+ ResponseOutputMessage(
797
+ id=f"msg_{random_uuid()}",
798
+ content=[output_text],
799
+ role="assistant",
800
+ status="incomplete",
801
+ type="message",
802
+ )
803
+ ]
804
+
805
+ if parser.current_channel == "analysis":
806
return [
807
ResponseReasoningItem(
808
id=f"rs_{random_uuid()}",
@@ -855,17 +871,30 @@ def parse_chat_output(
871
is_tool_call = False # TODO: update this when tool call is supported
872
873
# Get completed messages from the parser
874
+ # - analysis channel: hidden reasoning
875
+ # - commentary channel without recipient (preambles): visible to user
876
+ # - final channel: visible to user
877
+ # - commentary with recipient (tool calls): handled separately by tool parser
878
reasoning_texts = [
879
msg.content[0].text for msg in output_msgs if msg.channel == "analysis"
880
]
881
final_texts = [
862
- msg.content[0].text for msg in output_msgs if msg.channel != "analysis"
882
+ msg.content[0].text
883
+ for msg in output_msgs
884
+ if msg.channel == "final" or (msg.channel == "commentary" and not msg.recipient)
885
]
886
887
# Extract partial messages from the parser
888
if parser.current_channel == "analysis" and parser.current_content:
889
reasoning_texts.append(parser.current_content)
868
- elif parser.current_channel != "analysis" and parser.current_content:
890
+ elif parser.current_channel == "final" and parser.current_content:
891
+ final_texts.append(parser.current_content)
892
+ elif (
893
+ parser.current_channel == "commentary"
894
+ and not parser.current_recipient
895
+ and parser.current_content
896
+ ):
897
+ # Preambles (commentary without recipient) are visible to user
898
final_texts.append(parser.current_content)
899
900
# Flatten multiple messages into a single string
vllm/entrypoints/openai/responses/context.py
+6
-2
@@ -540,8 +540,12 @@ class HarmonyContext(ConversationContext):
540
self.first_tok_of_message = True # For streaming support
541
542
def _update_num_reasoning_tokens(self):
543
- # Count all analysis and commentary channels as reasoning tokens
544
- if self.parser.current_channel in {"analysis", "commentary"}:
543
+ channel = self.parser.current_channel
544
+ if channel == "analysis":
545
+ self.num_reasoning_tokens += 1
546
+ elif channel == "commentary" and self.parser.current_recipient is not None:
547
+ # Tool interactions (python/browser/container) are hidden.
548
+ # Preambles (recipient=None) are visible user text.
549
self.num_reasoning_tokens += 1
550
551
def append_output(self, output: RequestOutput) -> None:
vllm/entrypoints/openai/responses/streaming_events.py
+6
-2
@@ -563,7 +563,9 @@ def emit_content_delta_events(
563
channel = ctx.parser.current_channel
564
recipient = ctx.parser.current_recipient
565
566
- if channel == "final" and recipient is None:
566
+ if channel in ("final", "commentary") and recipient is None:
567
+ # Preambles (commentary with no recipient) and final messages
568
+ # are both user-visible text.
569
return emit_text_delta_events(delta, state)
570
elif channel == "analysis" and recipient is None:
571
return emit_reasoning_delta_events(delta, state)
@@ -607,7 +609,9 @@ def emit_previous_item_done_events(
609
return emit_mcp_completion_events(previous_item.recipient, text, state)
610
elif previous_item.channel == "analysis":
611
return emit_reasoning_done_events(text, state)
610
- elif previous_item.channel == "final":
612
+ elif previous_item.channel in ("commentary", "final"):
613
+ # Preambles (commentary with no recipient) and final messages
614
+ # are both user-visible text.
615
return emit_text_output_done_events(text, state)
616
return []
617