@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 5d18bf8b32
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 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