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, 17:45 UTC
[Misc][Harmony] Move Responses API only harmony utils to responses/harmony.py (#35339)
Signed-off-by: sfeng33 <4florafeng@gmail.com>
Flora Feng committed
Feb 26, 2026 at 01:35 UTC
186ea22efefd2c6f4f9b7fcb657bd00f50cb465a
6 files changed
+1040
-990
tests/entrypoints/openai/parser/test_harmony_utils.py
+7
-460
@@ -2,13 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import pytest
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
5
+from openai_harmony import Message, Role
6
7
from tests.entrypoints.openai.utils import verify_harmony_messages
8
from vllm.entrypoints.openai.parser.harmony_utils import (
@@ -18,20 +12,21 @@ from vllm.entrypoints.openai.parser.harmony_utils import (
12
has_custom_tools,
13
parse_chat_input_to_harmony_message,
14
parse_chat_output,
21
- parse_input_to_harmony_message,
22
- parse_output_message,
15
+)
16
+from vllm.entrypoints.openai.responses.harmony import (
17
+ response_previous_input_to_harmony,
18
)
19
20
21
class TestCommonParseInputToHarmonyMessage:
22
"""
23
Tests for scenarios that are common to both Chat Completion
29
- parse_chat_input_to_harmony_message and Responsees API
30
- parse_input_to_harmony_message functions.
24
+ parse_chat_input_to_harmony_message and Responses API
25
+ response_previous_input_to_harmony functions.
26
"""
27
28
@pytest.fixture(
34
- params=[parse_chat_input_to_harmony_message, parse_input_to_harmony_message]
29
+ params=[parse_chat_input_to_harmony_message, response_previous_input_to_harmony]
30
)
31
def parse_function(self, request):
32
return request.param
@@ -216,81 +211,6 @@ class TestCommonParseInputToHarmonyMessage:
211
assert messages[0].content[1].text == "actual text"
212
213
219
-class TestParseInputToHarmonyMessage:
220
- """
221
- Tests for scenarios that are specific to the Responses API
222
- parse_input_to_harmony_message function.
223
- """
224
-
225
- def test_message_with_empty_content(self):
226
- """Test parsing message with empty string content."""
227
- chat_msg = {
228
- "role": "user",
229
- "content": "",
230
- }
231
-
232
- messages = parse_input_to_harmony_message(chat_msg)
233
-
234
- assert len(messages) == 1
235
- assert messages[0].content[0].text == ""
236
-
237
- def test_tool_message_with_string_content(self):
238
- """Test parsing tool message with string content."""
239
- chat_msg = {
240
- "role": "tool",
241
- "name": "get_weather",
242
- "content": "The weather in San Francisco is sunny, 72°F",
243
- }
244
-
245
- messages = parse_input_to_harmony_message(chat_msg)
246
-
247
- assert len(messages) == 1
248
- assert messages[0].author.role == Role.TOOL
249
- assert messages[0].author.name == "functions.get_weather"
250
- assert (
251
- messages[0].content[0].text == "The weather in San Francisco is sunny, 72°F"
252
- )
253
- assert messages[0].channel == "commentary"
254
-
255
- def test_tool_message_with_array_content(self):
256
- """Test parsing tool message with array content."""
257
- chat_msg = {
258
- "role": "tool",
259
- "name": "search_results",
260
- "content": [
261
- {"type": "text", "text": "Result 1: "},
262
- {"type": "text", "text": "Result 2: "},
263
- {
264
- "type": "image",
265
- "url": "http://example.com/img.png",
266
- }, # Should be ignored
267
- {"type": "text", "text": "Result 3"},
268
- ],
269
- }
270
-
271
- messages = parse_input_to_harmony_message(chat_msg)
272
-
273
- assert len(messages) == 1
274
- assert messages[0].author.role == Role.TOOL
275
- assert messages[0].author.name == "functions.search_results"
276
- assert messages[0].content[0].text == "Result 1: Result 2: Result 3"
277
-
278
- def test_tool_message_with_empty_content(self):
279
- """Test parsing tool message with None content."""
280
- chat_msg = {
281
- "role": "tool",
282
- "name": "empty_tool",
283
- "content": None,
284
- }
285
-
286
- messages = parse_input_to_harmony_message(chat_msg)
287
-
288
- assert len(messages) == 1
289
- assert messages[0].author.role == Role.TOOL
290
- assert messages[0].author.name == "functions.empty_tool"
291
- assert messages[0].content[0].text == ""
292
-
293
-
214
class TestParseChatInputToHarmonyMessage:
215
"""
216
Tests for scenarios that are specific to the Chat Completion API
@@ -888,200 +808,6 @@ class TestParseChatOutput:
808
assert final_content == "Let me look that up.\nThe answer is 42."
809
810
891
-class TestParseOutputMessage:
892
- """Tests for parse_output_message function."""
893
-
894
- def test_commentary_with_no_recipient_creates_message(self):
895
- """Test that commentary with recipient=None (preambles) creates message items.
896
-
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."
903
- )
904
- message = message.with_channel("commentary")
905
- # recipient is None by default, representing a preamble
906
-
907
- output_items = parse_output_message(message)
908
-
909
- assert len(output_items) == 1
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
- )
920
-
921
- def test_commentary_with_function_recipient_creates_function_call(self):
922
- """Test commentary with recipient='functions.X' creates function calls."""
923
- message = Message.from_role_and_content(
924
- Role.ASSISTANT, '{"location": "San Francisco", "units": "celsius"}'
925
- )
926
- message = message.with_channel("commentary")
927
- message = message.with_recipient("functions.get_weather")
928
-
929
- output_items = parse_output_message(message)
930
-
931
- assert len(output_items) == 1
932
- assert isinstance(output_items[0], ResponseFunctionToolCall)
933
- assert output_items[0].type == "function_call"
934
- assert output_items[0].name == "get_weather"
935
- assert (
936
- output_items[0].arguments
937
- == '{"location": "San Francisco", "units": "celsius"}'
938
- )
939
- assert output_items[0].call_id.startswith("call_")
940
- assert output_items[0].id.startswith("fc_")
941
-
942
- def test_commentary_with_python_recipient_creates_reasoning(self):
943
- """Test that commentary with recipient='python' creates reasoning items."""
944
- message = Message.from_role_and_content(
945
- Role.ASSISTANT, "import numpy as np\nprint(np.array([1, 2, 3]))"
946
- )
947
- message = message.with_channel("commentary")
948
- message = message.with_recipient("python")
949
-
950
- output_items = parse_output_message(message)
951
-
952
- assert len(output_items) == 1
953
- assert isinstance(output_items[0], ResponseReasoningItem)
954
- assert output_items[0].type == "reasoning"
955
- assert (
956
- output_items[0].content[0].text
957
- == "import numpy as np\nprint(np.array([1, 2, 3]))"
958
- )
959
-
960
- def test_commentary_with_browser_recipient_creates_reasoning(self):
961
- """Test that commentary with recipient='browser' creates reasoning items."""
962
- message = Message.from_role_and_content(
963
- Role.ASSISTANT, "Navigating to the specified URL"
964
- )
965
- message = message.with_channel("commentary")
966
- message = message.with_recipient("browser")
967
-
968
- output_items = parse_output_message(message)
969
-
970
- assert len(output_items) == 1
971
- assert isinstance(output_items[0], ResponseReasoningItem)
972
- assert output_items[0].type == "reasoning"
973
- assert output_items[0].content[0].text == "Navigating to the specified URL"
974
-
975
- def test_commentary_with_container_recipient_creates_reasoning(self):
976
- """Test that commentary with recipient='container' creates reasoning items."""
977
- message = Message.from_role_and_content(
978
- Role.ASSISTANT, "Running command in container"
979
- )
980
- message = message.with_channel("commentary")
981
- message = message.with_recipient("container")
982
-
983
- output_items = parse_output_message(message)
984
-
985
- assert len(output_items) == 1
986
- assert isinstance(output_items[0], ResponseReasoningItem)
987
- assert output_items[0].type == "reasoning"
988
- assert output_items[0].content[0].text == "Running command in container"
989
-
990
- def test_commentary_with_empty_content_and_no_recipient(self):
991
- """Test edge case: empty commentary with recipient=None."""
992
- message = Message.from_role_and_content(Role.ASSISTANT, "")
993
- message = message.with_channel("commentary")
994
-
995
- output_items = parse_output_message(message)
996
-
997
- assert len(output_items) == 1
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):
1002
- """Test multiple content items in commentary with no recipient."""
1003
- contents = [
1004
- TextContent(text="Step 1: Analyze the request"),
1005
- TextContent(text="Step 2: Prepare to call functions"),
1006
- ]
1007
- message = Message.from_role_and_contents(Role.ASSISTANT, contents)
1008
- message = message.with_channel("commentary")
1009
-
1010
- output_items = parse_output_message(message)
1011
-
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"
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."""
1022
- contents = [
1023
- TextContent(text='{"location": "San Francisco"}'),
1024
- TextContent(text='{"location": "New York"}'),
1025
- ]
1026
- message = Message.from_role_and_contents(Role.ASSISTANT, contents)
1027
- message = message.with_channel("commentary")
1028
- message = message.with_recipient("functions.get_weather")
1029
-
1030
- output_items = parse_output_message(message)
1031
-
1032
- assert len(output_items) == 2
1033
- assert all(isinstance(item, ResponseFunctionToolCall) for item in output_items)
1034
- assert output_items[0].name == "get_weather"
1035
- assert output_items[1].name == "get_weather"
1036
- assert output_items[0].arguments == '{"location": "San Francisco"}'
1037
- assert output_items[1].arguments == '{"location": "New York"}'
1038
-
1039
- def test_commentary_with_unknown_recipient_creates_mcp_call(self):
1040
- """Test that commentary with unknown recipient creates MCP call."""
1041
- message = Message.from_role_and_content(Role.ASSISTANT, '{"arg": "value"}')
1042
- message = message.with_channel("commentary")
1043
- message = message.with_recipient("custom_tool")
1044
-
1045
- output_items = parse_output_message(message)
1046
-
1047
- assert len(output_items) == 1
1048
- assert isinstance(output_items[0], McpCall)
1049
- assert output_items[0].type == "mcp_call"
1050
- assert output_items[0].name == "custom_tool"
1051
- assert output_items[0].server_label == "custom_tool"
1052
-
1053
- def test_analysis_channel_creates_reasoning(self):
1054
- """Test that analysis channel creates reasoning items."""
1055
- message = Message.from_role_and_content(
1056
- Role.ASSISTANT, "Analyzing the problem step by step..."
1057
- )
1058
- message = message.with_channel("analysis")
1059
-
1060
- output_items = parse_output_message(message)
1061
-
1062
- assert len(output_items) == 1
1063
- assert isinstance(output_items[0], ResponseReasoningItem)
1064
- assert output_items[0].type == "reasoning"
1065
- assert (
1066
- output_items[0].content[0].text == "Analyzing the problem step by step..."
1067
- )
1068
-
1069
- def test_non_assistant_message_returns_empty(self):
1070
- """Test that non-assistant messages return empty list.
1071
-
1072
- Per the implementation, tool messages to assistant (e.g., search results)
1073
- are not included in final output to align with OpenAI behavior.
1074
- """
1075
- message = Message.from_author_and_content(
1076
- Author.new(Role.TOOL, "functions.get_weather"),
1077
- "The weather is sunny, 72°F",
1078
- )
1079
-
1080
- output_items = parse_output_message(message)
1081
-
1082
- assert len(output_items) == 0
1083
-
1084
-
811
def test_has_custom_tools() -> None:
812
assert not has_custom_tools(set())
813
assert not has_custom_tools({"web_search_preview", "code_interpreter", "container"})
@@ -1091,185 +817,6 @@ def test_has_custom_tools() -> None:
817
)
818
819
1094
-def test_parse_mcp_call_basic() -> None:
1095
- """Test that MCP calls are parsed with correct type and server_label."""
1096
- message = Message.from_role_and_content(Role.ASSISTANT, '{"path": "/tmp"}')
1097
- message = message.with_recipient("filesystem")
1098
- message = message.with_channel("commentary")
1099
-
1100
- output_items = parse_output_message(message)
1101
-
1102
- assert len(output_items) == 1
1103
- assert isinstance(output_items[0], McpCall)
1104
- assert output_items[0].type == "mcp_call"
1105
- assert output_items[0].name == "filesystem"
1106
- assert output_items[0].server_label == "filesystem"
1107
- assert output_items[0].arguments == '{"path": "/tmp"}'
1108
- assert output_items[0].status == "completed"
1109
-
1110
-
1111
-def test_parse_mcp_call_dotted_recipient() -> None:
1112
- """Test that dotted recipients extract the tool name correctly."""
1113
- message = Message.from_role_and_content(Role.ASSISTANT, '{"cmd": "ls"}')
1114
- message = message.with_recipient("repo_browser.list")
1115
- message = message.with_channel("commentary")
1116
-
1117
- output_items = parse_output_message(message)
1118
-
1119
- assert len(output_items) == 1
1120
- assert isinstance(output_items[0], McpCall)
1121
- assert output_items[0].name == "list"
1122
- assert output_items[0].server_label == "repo_browser"
1123
-
1124
-
1125
-def test_mcp_vs_function_call() -> None:
1126
- """Test that function calls are not parsed as MCP calls."""
1127
- func_message = Message.from_role_and_content(Role.ASSISTANT, '{"arg": "value"}')
1128
- func_message = func_message.with_recipient("functions.my_tool")
1129
- func_message = func_message.with_channel("commentary")
1130
-
1131
- func_items = parse_output_message(func_message)
1132
-
1133
- assert len(func_items) == 1
1134
- assert not isinstance(func_items[0], McpCall)
1135
- assert func_items[0].type == "function_call"
1136
-
1137
-
1138
-def test_mcp_vs_builtin_tools() -> None:
1139
- """Test that built-in tools (python, container) are not parsed as MCP calls."""
1140
- # Test python (built-in tool) - should be reasoning, not MCP
1141
- python_message = Message.from_role_and_content(Role.ASSISTANT, "print('hello')")
1142
- python_message = python_message.with_recipient("python")
1143
- python_message = python_message.with_channel("commentary")
1144
-
1145
- python_items = parse_output_message(python_message)
1146
-
1147
- assert len(python_items) == 1
1148
- assert not isinstance(python_items[0], McpCall)
1149
- assert python_items[0].type == "reasoning"
1150
-
1151
-
1152
-def test_parse_remaining_state_commentary_channel() -> None:
1153
- """Test parse_remaining_state with commentary channel and various recipients."""
1154
- from unittest.mock import Mock
1155
-
1156
- from vllm.entrypoints.openai.parser.harmony_utils import parse_remaining_state
1157
-
1158
- # Test 1: functions.* recipient → should return function tool call
1159
- parser_func = Mock()
1160
- parser_func.current_content = '{"arg": "value"}'
1161
- parser_func.current_role = Role.ASSISTANT
1162
- parser_func.current_channel = "commentary"
1163
- parser_func.current_recipient = "functions.my_tool"
1164
-
1165
- func_items = parse_remaining_state(parser_func)
1166
-
1167
- assert len(func_items) == 1
1168
- assert not isinstance(func_items[0], McpCall)
1169
- assert func_items[0].type == "function_call"
1170
- assert func_items[0].name == "my_tool"
1171
- assert func_items[0].status == "in_progress"
1172
-
1173
- # Test 2: MCP tool (not builtin) → should return MCP call
1174
- parser_mcp = Mock()
1175
- parser_mcp.current_content = '{"path": "/tmp"}'
1176
- parser_mcp.current_role = Role.ASSISTANT
1177
- parser_mcp.current_channel = "commentary"
1178
- parser_mcp.current_recipient = "filesystem"
1179
-
1180
- mcp_items = parse_remaining_state(parser_mcp)
1181
-
1182
- assert len(mcp_items) == 1
1183
- assert isinstance(mcp_items[0], McpCall)
1184
- assert mcp_items[0].type == "mcp_call"
1185
- assert mcp_items[0].name == "filesystem"
1186
- assert mcp_items[0].server_label == "filesystem"
1187
- assert mcp_items[0].status == "in_progress"
1188
-
1189
- # Test 3: Built-in tool (python)
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
1194
- parser_builtin.current_channel = "commentary"
1195
- parser_builtin.current_recipient = "python"
1196
-
1197
- builtin_items = parse_remaining_state(parser_builtin)
1198
-
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."""
1222
- from unittest.mock import Mock
1223
-
1224
- from vllm.entrypoints.openai.parser.harmony_utils import parse_remaining_state
1225
-
1226
- # Test 1: functions.* recipient → should return function tool call
1227
- parser_func = Mock()
1228
- parser_func.current_content = '{"arg": "value"}'
1229
- parser_func.current_role = Role.ASSISTANT
1230
- parser_func.current_channel = "analysis"
1231
- parser_func.current_recipient = "functions.my_tool"
1232
-
1233
- func_items = parse_remaining_state(parser_func)
1234
-
1235
- assert len(func_items) == 1
1236
- assert not isinstance(func_items[0], McpCall)
1237
- assert func_items[0].type == "function_call"
1238
- assert func_items[0].name == "my_tool"
1239
- assert func_items[0].status == "in_progress"
1240
-
1241
- # Test 2: MCP tool (not builtin) → should return MCP call
1242
- parser_mcp = Mock()
1243
- parser_mcp.current_content = '{"query": "test"}'
1244
- parser_mcp.current_role = Role.ASSISTANT
1245
- parser_mcp.current_channel = "analysis"
1246
- parser_mcp.current_recipient = "database"
1247
-
1248
- mcp_items = parse_remaining_state(parser_mcp)
1249
-
1250
- assert len(mcp_items) == 1
1251
- assert isinstance(mcp_items[0], McpCall)
1252
- assert mcp_items[0].type == "mcp_call"
1253
- assert mcp_items[0].name == "database"
1254
- assert mcp_items[0].server_label == "database"
1255
- assert mcp_items[0].status == "in_progress"
1256
-
1257
- # Test 3: Built-in tool (container)
1258
- # should NOT return MCP call, falls through to reasoning
1259
- parser_builtin = Mock()
1260
- parser_builtin.current_content = "docker run"
1261
- parser_builtin.current_role = Role.ASSISTANT
1262
- parser_builtin.current_channel = "analysis"
1263
- parser_builtin.current_recipient = "container"
1264
-
1265
- builtin_items = parse_remaining_state(parser_builtin)
1266
-
1267
- # Should fall through to reasoning logic
1268
- assert len(builtin_items) == 1
1269
- assert not isinstance(builtin_items[0], McpCall)
1270
- assert builtin_items[0].type == "reasoning"
1271
-
1272
-
820
class TestGetSystemMessage:
821
"""Tests for get_system_message channel configuration."""
822
tests/entrypoints/openai/responses/test_harmony_utils.py
new
+463
@@ -0,0 +1,463 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Unit tests for vllm.entrypoints.openai.responses.harmony."""
4
+
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
+
13
+from vllm.entrypoints.openai.responses.harmony import (
14
+ harmony_to_response_output,
15
+ parser_state_to_response_output,
16
+ response_previous_input_to_harmony,
17
+)
18
+
19
+
20
+class TestResponsePreviousInputToHarmony:
21
+ """
22
+ Tests for scenarios that are specific to the Responses API
23
+ response_previous_input_to_harmony function.
24
+ """
25
+
26
+ def test_message_with_empty_content(self):
27
+ """Test parsing message with empty string content."""
28
+ chat_msg = {
29
+ "role": "user",
30
+ "content": "",
31
+ }
32
+
33
+ messages = response_previous_input_to_harmony(chat_msg)
34
+
35
+ assert len(messages) == 1
36
+ assert messages[0].content[0].text == ""
37
+
38
+ def test_tool_message_with_string_content(self):
39
+ """Test parsing tool message with string content."""
40
+ chat_msg = {
41
+ "role": "tool",
42
+ "name": "get_weather",
43
+ "content": "The weather in San Francisco is sunny, 72°F",
44
+ }
45
+
46
+ messages = response_previous_input_to_harmony(chat_msg)
47
+
48
+ assert len(messages) == 1
49
+ assert messages[0].author.role == Role.TOOL
50
+ assert messages[0].author.name == "functions.get_weather"
51
+ assert (
52
+ messages[0].content[0].text == "The weather in San Francisco is sunny, 72°F"
53
+ )
54
+ assert messages[0].channel == "commentary"
55
+
56
+ def test_tool_message_with_array_content(self):
57
+ """Test parsing tool message with array content."""
58
+ chat_msg = {
59
+ "role": "tool",
60
+ "name": "search_results",
61
+ "content": [
62
+ {"type": "text", "text": "Result 1: "},
63
+ {"type": "text", "text": "Result 2: "},
64
+ {
65
+ "type": "image",
66
+ "url": "http://example.com/img.png",
67
+ }, # Should be ignored
68
+ {"type": "text", "text": "Result 3"},
69
+ ],
70
+ }
71
+
72
+ messages = response_previous_input_to_harmony(chat_msg)
73
+
74
+ assert len(messages) == 1
75
+ assert messages[0].author.role == Role.TOOL
76
+ assert messages[0].author.name == "functions.search_results"
77
+ assert messages[0].content[0].text == "Result 1: Result 2: Result 3"
78
+
79
+ def test_tool_message_with_empty_content(self):
80
+ """Test parsing tool message with None content."""
81
+ chat_msg = {
82
+ "role": "tool",
83
+ "name": "empty_tool",
84
+ "content": None,
85
+ }
86
+
87
+ messages = response_previous_input_to_harmony(chat_msg)
88
+
89
+ assert len(messages) == 1
90
+ assert messages[0].author.role == Role.TOOL
91
+ assert messages[0].author.name == "functions.empty_tool"
92
+ assert messages[0].content[0].text == ""
93
+
94
+
95
+class TestHarmonyToResponseOutput:
96
+ """Tests for harmony_to_response_output function."""
97
+
98
+ def test_commentary_with_no_recipient_creates_message(self):
99
+ """Test that commentary with recipient=None (preambles) creates message items.
100
+
101
+ Per Harmony format, preambles are intended to be shown to end-users,
102
+ unlike analysis channel content which is hidden reasoning.
103
+ See: https://cookbook.openai.com/articles/openai-harmony
104
+ """
105
+ message = Message.from_role_and_content(
106
+ Role.ASSISTANT, "I will now search for the weather information."
107
+ )
108
+ message = message.with_channel("commentary")
109
+ # recipient is None by default, representing a preamble
110
+
111
+ output_items = harmony_to_response_output(message)
112
+
113
+ assert len(output_items) == 1
114
+ assert isinstance(output_items[0], ResponseOutputMessage)
115
+ assert output_items[0].type == "message"
116
+ assert output_items[0].role == "assistant"
117
+ assert output_items[0].status == "completed"
118
+ assert len(output_items[0].content) == 1
119
+ assert output_items[0].content[0].type == "output_text"
120
+ assert (
121
+ output_items[0].content[0].text
122
+ == "I will now search for the weather information."
123
+ )
124
+
125
+ def test_commentary_with_function_recipient_creates_function_call(self):
126
+ """Test commentary with recipient='functions.X' creates function calls."""
127
+ message = Message.from_role_and_content(
128
+ Role.ASSISTANT, '{"location": "San Francisco", "units": "celsius"}'
129
+ )
130
+ message = message.with_channel("commentary")
131
+ message = message.with_recipient("functions.get_weather")
132
+
133
+ output_items = harmony_to_response_output(message)
134
+
135
+ assert len(output_items) == 1
136
+ assert isinstance(output_items[0], ResponseFunctionToolCall)
137
+ assert output_items[0].type == "function_call"
138
+ assert output_items[0].name == "get_weather"
139
+ assert (
140
+ output_items[0].arguments
141
+ == '{"location": "San Francisco", "units": "celsius"}'
142
+ )
143
+ assert output_items[0].call_id.startswith("call_")
144
+ assert output_items[0].id.startswith("fc_")
145
+
146
+ def test_commentary_with_python_recipient_creates_reasoning(self):
147
+ """Test that commentary with recipient='python' creates reasoning items."""
148
+ message = Message.from_role_and_content(
149
+ Role.ASSISTANT, "import numpy as np\nprint(np.array([1, 2, 3]))"
150
+ )
151
+ message = message.with_channel("commentary")
152
+ message = message.with_recipient("python")
153
+
154
+ output_items = harmony_to_response_output(message)
155
+
156
+ assert len(output_items) == 1
157
+ assert isinstance(output_items[0], ResponseReasoningItem)
158
+ assert output_items[0].type == "reasoning"
159
+ assert (
160
+ output_items[0].content[0].text
161
+ == "import numpy as np\nprint(np.array([1, 2, 3]))"
162
+ )
163
+
164
+ def test_commentary_with_browser_recipient_creates_reasoning(self):
165
+ """Test that commentary with recipient='browser' creates reasoning items."""
166
+ message = Message.from_role_and_content(
167
+ Role.ASSISTANT, "Navigating to the specified URL"
168
+ )
169
+ message = message.with_channel("commentary")
170
+ message = message.with_recipient("browser")
171
+
172
+ output_items = harmony_to_response_output(message)
173
+
174
+ assert len(output_items) == 1
175
+ assert isinstance(output_items[0], ResponseReasoningItem)
176
+ assert output_items[0].type == "reasoning"
177
+ assert output_items[0].content[0].text == "Navigating to the specified URL"
178
+
179
+ def test_commentary_with_container_recipient_creates_reasoning(self):
180
+ """Test that commentary with recipient='container' creates reasoning items."""
181
+ message = Message.from_role_and_content(
182
+ Role.ASSISTANT, "Running command in container"
183
+ )
184
+ message = message.with_channel("commentary")
185
+ message = message.with_recipient("container")
186
+
187
+ output_items = harmony_to_response_output(message)
188
+
189
+ assert len(output_items) == 1
190
+ assert isinstance(output_items[0], ResponseReasoningItem)
191
+ assert output_items[0].type == "reasoning"
192
+ assert output_items[0].content[0].text == "Running command in container"
193
+
194
+ def test_commentary_with_empty_content_and_no_recipient(self):
195
+ """Test edge case: empty commentary with recipient=None."""
196
+ message = Message.from_role_and_content(Role.ASSISTANT, "")
197
+ message = message.with_channel("commentary")
198
+
199
+ output_items = harmony_to_response_output(message)
200
+
201
+ assert len(output_items) == 1
202
+ assert isinstance(output_items[0], ResponseOutputMessage)
203
+ assert output_items[0].content[0].text == ""
204
+
205
+ def test_commentary_with_multiple_contents_and_no_recipient(self):
206
+ """Test multiple content items in commentary with no recipient."""
207
+ contents = [
208
+ TextContent(text="Step 1: Analyze the request"),
209
+ TextContent(text="Step 2: Prepare to call functions"),
210
+ ]
211
+ message = Message.from_role_and_contents(Role.ASSISTANT, contents)
212
+ message = message.with_channel("commentary")
213
+
214
+ output_items = harmony_to_response_output(message)
215
+
216
+ # _parse_final_message returns single ResponseOutputMessage with
217
+ # multiple contents
218
+ assert len(output_items) == 1
219
+ assert isinstance(output_items[0], ResponseOutputMessage)
220
+ assert len(output_items[0].content) == 2
221
+ assert output_items[0].content[0].text == "Step 1: Analyze the request"
222
+ assert output_items[0].content[1].text == "Step 2: Prepare to call functions"
223
+
224
+ def test_commentary_with_multiple_function_calls(self):
225
+ """Test multiple function calls in commentary channel."""
226
+ contents = [
227
+ TextContent(text='{"location": "San Francisco"}'),
228
+ TextContent(text='{"location": "New York"}'),
229
+ ]
230
+ message = Message.from_role_and_contents(Role.ASSISTANT, contents)
231
+ message = message.with_channel("commentary")
232
+ message = message.with_recipient("functions.get_weather")
233
+
234
+ output_items = harmony_to_response_output(message)
235
+
236
+ assert len(output_items) == 2
237
+ assert all(isinstance(item, ResponseFunctionToolCall) for item in output_items)
238
+ assert output_items[0].name == "get_weather"
239
+ assert output_items[1].name == "get_weather"
240
+ assert output_items[0].arguments == '{"location": "San Francisco"}'
241
+ assert output_items[1].arguments == '{"location": "New York"}'
242
+
243
+ def test_commentary_with_unknown_recipient_creates_mcp_call(self):
244
+ """Test that commentary with unknown recipient creates MCP call."""
245
+ message = Message.from_role_and_content(Role.ASSISTANT, '{"arg": "value"}')
246
+ message = message.with_channel("commentary")
247
+ message = message.with_recipient("custom_tool")
248
+
249
+ output_items = harmony_to_response_output(message)
250
+
251
+ assert len(output_items) == 1
252
+ assert isinstance(output_items[0], McpCall)
253
+ assert output_items[0].type == "mcp_call"
254
+ assert output_items[0].name == "custom_tool"
255
+ assert output_items[0].server_label == "custom_tool"
256
+
257
+ def test_analysis_channel_creates_reasoning(self):
258
+ """Test that analysis channel creates reasoning items."""
259
+ message = Message.from_role_and_content(
260
+ Role.ASSISTANT, "Analyzing the problem step by step..."
261
+ )
262
+ message = message.with_channel("analysis")
263
+
264
+ output_items = harmony_to_response_output(message)
265
+
266
+ assert len(output_items) == 1
267
+ assert isinstance(output_items[0], ResponseReasoningItem)
268
+ assert output_items[0].type == "reasoning"
269
+ assert (
270
+ output_items[0].content[0].text == "Analyzing the problem step by step..."
271
+ )
272
+
273
+ def test_non_assistant_message_returns_empty(self):
274
+ """Test that non-assistant messages return empty list.
275
+
276
+ Per the implementation, tool messages to assistant (e.g., search results)
277
+ are not included in final output to align with OpenAI behavior.
278
+ """
279
+ message = Message.from_author_and_content(
280
+ Author.new(Role.TOOL, "functions.get_weather"),
281
+ "The weather is sunny, 72°F",
282
+ )
283
+
284
+ output_items = harmony_to_response_output(message)
285
+
286
+ assert len(output_items) == 0
287
+
288
+
289
+def test_parse_mcp_call_basic() -> None:
290
+ """Test that MCP calls are parsed with correct type and server_label."""
291
+ message = Message.from_role_and_content(Role.ASSISTANT, '{"path": "/tmp"}')
292
+ message = message.with_recipient("filesystem")
293
+ message = message.with_channel("commentary")
294
+
295
+ output_items = harmony_to_response_output(message)
296
+
297
+ assert len(output_items) == 1
298
+ assert isinstance(output_items[0], McpCall)
299
+ assert output_items[0].type == "mcp_call"
300
+ assert output_items[0].name == "filesystem"
301
+ assert output_items[0].server_label == "filesystem"
302
+ assert output_items[0].arguments == '{"path": "/tmp"}'
303
+ assert output_items[0].status == "completed"
304
+
305
+
306
+def test_parse_mcp_call_dotted_recipient() -> None:
307
+ """Test that dotted recipients extract the tool name correctly."""
308
+ message = Message.from_role_and_content(Role.ASSISTANT, '{"cmd": "ls"}')
309
+ message = message.with_recipient("repo_browser.list")
310
+ message = message.with_channel("commentary")
311
+
312
+ output_items = harmony_to_response_output(message)
313
+
314
+ assert len(output_items) == 1
315
+ assert isinstance(output_items[0], McpCall)
316
+ assert output_items[0].name == "list"
317
+ assert output_items[0].server_label == "repo_browser"
318
+
319
+
320
+def test_mcp_vs_function_call() -> None:
321
+ """Test that function calls are not parsed as MCP calls."""
322
+ func_message = Message.from_role_and_content(Role.ASSISTANT, '{"arg": "value"}')
323
+ func_message = func_message.with_recipient("functions.my_tool")
324
+ func_message = func_message.with_channel("commentary")
325
+
326
+ func_items = harmony_to_response_output(func_message)
327
+
328
+ assert len(func_items) == 1
329
+ assert not isinstance(func_items[0], McpCall)
330
+ assert func_items[0].type == "function_call"
331
+
332
+
333
+def test_mcp_vs_builtin_tools() -> None:
334
+ """Test that built-in tools (python, container) are not parsed as MCP calls."""
335
+ # Test python (built-in tool) - should be reasoning, not MCP
336
+ python_message = Message.from_role_and_content(Role.ASSISTANT, "print('hello')")
337
+ python_message = python_message.with_recipient("python")
338
+ python_message = python_message.with_channel("commentary")
339
+
340
+ python_items = harmony_to_response_output(python_message)
341
+
342
+ assert len(python_items) == 1
343
+ assert not isinstance(python_items[0], McpCall)
344
+ assert python_items[0].type == "reasoning"
345
+
346
+
347
+def test_parser_state_to_response_output_commentary_channel() -> None:
348
+ """Test parser_state_to_response_output with commentary
349
+ channel and various recipients."""
350
+ from unittest.mock import Mock
351
+
352
+ # Test 1: functions.* recipient -> should return function tool call
353
+ parser_func = Mock()
354
+ parser_func.current_content = '{"arg": "value"}'
355
+ parser_func.current_role = Role.ASSISTANT
356
+ parser_func.current_channel = "commentary"
357
+ parser_func.current_recipient = "functions.my_tool"
358
+
359
+ func_items = parser_state_to_response_output(parser_func)
360
+
361
+ assert len(func_items) == 1
362
+ assert not isinstance(func_items[0], McpCall)
363
+ assert func_items[0].type == "function_call"
364
+ assert func_items[0].name == "my_tool"
365
+ assert func_items[0].status == "in_progress"
366
+
367
+ # Test 2: MCP tool (not builtin) -> should return MCP call
368
+ parser_mcp = Mock()
369
+ parser_mcp.current_content = '{"path": "/tmp"}'
370
+ parser_mcp.current_role = Role.ASSISTANT
371
+ parser_mcp.current_channel = "commentary"
372
+ parser_mcp.current_recipient = "filesystem"
373
+
374
+ mcp_items = parser_state_to_response_output(parser_mcp)
375
+
376
+ assert len(mcp_items) == 1
377
+ assert isinstance(mcp_items[0], McpCall)
378
+ assert mcp_items[0].type == "mcp_call"
379
+ assert mcp_items[0].name == "filesystem"
380
+ assert mcp_items[0].server_label == "filesystem"
381
+ assert mcp_items[0].status == "in_progress"
382
+
383
+ # Test 3: Built-in tool (python)
384
+ # should NOT return MCP call, returns reasoning (internal tool interaction)
385
+ parser_builtin = Mock()
386
+ parser_builtin.current_content = "print('hello')"
387
+ parser_builtin.current_role = Role.ASSISTANT
388
+ parser_builtin.current_channel = "commentary"
389
+ parser_builtin.current_recipient = "python"
390
+
391
+ builtin_items = parser_state_to_response_output(parser_builtin)
392
+
393
+ # Built-in tools explicitly return reasoning
394
+ assert len(builtin_items) == 1
395
+ assert not isinstance(builtin_items[0], McpCall)
396
+ assert builtin_items[0].type == "reasoning"
397
+
398
+ # Test 4: No recipient (preamble) → should return message, not reasoning
399
+ parser_preamble = Mock()
400
+ parser_preamble.current_content = "I'll search for that information now."
401
+ parser_preamble.current_role = Role.ASSISTANT
402
+ parser_preamble.current_channel = "commentary"
403
+ parser_preamble.current_recipient = None
404
+
405
+ preamble_items = parser_state_to_response_output(parser_preamble)
406
+
407
+ assert len(preamble_items) == 1
408
+ assert isinstance(preamble_items[0], ResponseOutputMessage)
409
+ assert preamble_items[0].type == "message"
410
+ assert preamble_items[0].content[0].text == "I'll search for that information now."
411
+ assert preamble_items[0].status == "incomplete" # streaming
412
+
413
+
414
+def test_parser_state_to_response_output_analysis_channel() -> None:
415
+ """Test parser_state_to_response_output with analysis
416
+ channel and various recipients."""
417
+ from unittest.mock import Mock
418
+
419
+ # Test 1: functions.* recipient -> should return function tool call
420
+ parser_func = Mock()
421
+ parser_func.current_content = '{"arg": "value"}'
422
+ parser_func.current_role = Role.ASSISTANT
423
+ parser_func.current_channel = "analysis"
424
+ parser_func.current_recipient = "functions.my_tool"
425
+
426
+ func_items = parser_state_to_response_output(parser_func)
427
+
428
+ assert len(func_items) == 1
429
+ assert not isinstance(func_items[0], McpCall)
430
+ assert func_items[0].type == "function_call"
431
+ assert func_items[0].name == "my_tool"
432
+ assert func_items[0].status == "in_progress"
433
+
434
+ # Test 2: MCP tool (not builtin) -> should return MCP call
435
+ parser_mcp = Mock()
436
+ parser_mcp.current_content = '{"query": "test"}'
437
+ parser_mcp.current_role = Role.ASSISTANT
438
+ parser_mcp.current_channel = "analysis"
439
+ parser_mcp.current_recipient = "database"
440
+
441
+ mcp_items = parser_state_to_response_output(parser_mcp)
442
+
443
+ assert len(mcp_items) == 1
444
+ assert isinstance(mcp_items[0], McpCall)
445
+ assert mcp_items[0].type == "mcp_call"
446
+ assert mcp_items[0].name == "database"
447
+ assert mcp_items[0].server_label == "database"
448
+ assert mcp_items[0].status == "in_progress"
449
+
450
+ # Test 3: Built-in tool (container)
451
+ # should NOT return MCP call, falls through to reasoning
452
+ parser_builtin = Mock()
453
+ parser_builtin.current_content = "docker run"
454
+ parser_builtin.current_role = Role.ASSISTANT
455
+ parser_builtin.current_channel = "analysis"
456
+ parser_builtin.current_recipient = "container"
457
+
458
+ builtin_items = parser_state_to_response_output(parser_builtin)
459
+
460
+ # Should fall through to reasoning logic
461
+ assert len(builtin_items) == 1
462
+ assert not isinstance(builtin_items[0], McpCall)
463
+ assert builtin_items[0].type == "reasoning"
tests/entrypoints/openai/responses/test_mcp_tools.py
+5
-5
@@ -97,16 +97,16 @@ class TestMCPToolServerUnit:
97
assert server.get_tool_description("test_server", allowed_tools=[]) is None
98
99
def test_builtin_tools_consistency(self):
100
- """MCP_BUILTIN_TOOLS must match _BUILTIN_TOOL_TO_MCP_SERVER_LABEL values."""
100
+ """MCP_BUILTIN_TOOLS must match BUILTIN_TOOL_TO_MCP_SERVER_LABEL values."""
101
from vllm.entrypoints.openai.parser.harmony_utils import (
102
- _BUILTIN_TOOL_TO_MCP_SERVER_LABEL,
102
+ BUILTIN_TOOL_TO_MCP_SERVER_LABEL,
103
MCP_BUILTIN_TOOLS,
104
)
105
106
- assert set(_BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values()) == MCP_BUILTIN_TOOLS, (
106
+ assert set(BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values()) == MCP_BUILTIN_TOOLS, (
107
f"MCP_BUILTIN_TOOLS {MCP_BUILTIN_TOOLS} does not match "
108
- f"_BUILTIN_TOOL_TO_MCP_SERVER_LABEL values "
109
- f"{set(_BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values())}"
108
+ f"BUILTIN_TOOL_TO_MCP_SERVER_LABEL values "
109
+ f"{set(BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values())}"
110
)
111
112
vllm/entrypoints/openai/parser/harmony_utils.py
+2
-516
@@ -2,27 +2,9 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import datetime
5
-import json
5
from collections.abc import Iterable, Sequence
6
from typing import Literal
7
9
-from openai.types.responses import (
10
- ResponseFunctionToolCall,
11
- ResponseOutputItem,
12
- ResponseOutputMessage,
13
- ResponseOutputText,
14
- ResponseReasoningItem,
15
-)
16
-from openai.types.responses.response_function_web_search import (
17
- ActionFind,
18
- ActionOpenPage,
19
- ActionSearch,
20
- ResponseFunctionWebSearch,
21
-)
22
-from openai.types.responses.response_output_item import McpCall
23
-from openai.types.responses.response_reasoning_item import (
24
- Content as ResponseReasoningTextContent,
25
-)
8
from openai.types.responses.tool import Tool
9
from openai_harmony import (
10
Author,
@@ -38,17 +20,10 @@ from openai_harmony import (
20
ToolDescription,
21
load_harmony_encoding,
22
)
41
-from openai_harmony import Message as OpenAIHarmonyMessage
42
-from openai_harmony import Role as OpenAIHarmonyRole
23
24
from vllm import envs
25
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionToolsParam
46
-from vllm.entrypoints.openai.responses.protocol import (
47
- ResponseInputOutputItem,
48
- ResponsesRequest,
49
-)
26
from vllm.logger import init_logger
51
-from vllm.utils import random_uuid
27
28
logger = init_logger(__name__)
29
@@ -64,14 +39,14 @@ _harmony_encoding = None
39
# they are available and requested by the user.
40
# Tool args are provided by MCP tool descriptions. Output
41
# of the tools are stringified.
67
-_BUILTIN_TOOL_TO_MCP_SERVER_LABEL: dict[str, str] = {
42
+BUILTIN_TOOL_TO_MCP_SERVER_LABEL: dict[str, str] = {
43
"python": "code_interpreter",
44
"browser": "web_search_preview",
45
"container": "container",
46
}
47
48
# Derive MCP_BUILTIN_TOOLS from the canonical mapping
74
-MCP_BUILTIN_TOOLS: set[str] = set(_BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values())
49
+MCP_BUILTIN_TOOLS: set[str] = set(BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values())
50
51
52
def has_custom_tools(tool_types: set[str]) -> bool:
@@ -179,55 +154,6 @@ def get_user_message(content: str) -> Message:
154
return Message.from_role_and_content(Role.USER, content)
155
156
182
-def parse_response_input(
183
- response_msg: ResponseInputOutputItem,
184
- prev_responses: list[ResponseOutputItem | ResponseReasoningItem],
185
-) -> Message:
186
- if not isinstance(response_msg, dict):
187
- response_msg = response_msg.model_dump()
188
- if "type" not in response_msg or response_msg["type"] == "message":
189
- role = response_msg["role"]
190
- content = response_msg["content"]
191
- # Add prefix for developer messages.
192
- # <|start|>developer<|message|># Instructions {instructions}<|end|>
193
- text_prefix = "Instructions:\n" if role == "developer" else ""
194
- if isinstance(content, str):
195
- msg = Message.from_role_and_content(role, text_prefix + content)
196
- else:
197
- contents = [TextContent(text=text_prefix + c["text"]) for c in content]
198
- msg = Message.from_role_and_contents(role, contents)
199
- if role == "assistant":
200
- msg = msg.with_channel("final")
201
- elif response_msg["type"] == "function_call_output":
202
- call_id = response_msg["call_id"]
203
- call_response: ResponseFunctionToolCall | None = None
204
- for prev_response in reversed(prev_responses):
205
- if (
206
- isinstance(prev_response, ResponseFunctionToolCall)
207
- and prev_response.call_id == call_id
208
- ):
209
- call_response = prev_response
210
- break
211
- if call_response is None:
212
- raise ValueError(f"No call message found for {call_id}")
213
- msg = Message.from_author_and_content(
214
- Author.new(Role.TOOL, f"functions.{call_response.name}"),
215
- response_msg["output"],
216
- )
217
- elif response_msg["type"] == "reasoning":
218
- content = response_msg["content"]
219
- assert len(content) == 1
220
- msg = Message.from_role_and_content(Role.ASSISTANT, content[0]["text"])
221
- elif response_msg["type"] == "function_call":
222
- msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"])
223
- msg = msg.with_channel("commentary")
224
- msg = msg.with_recipient(f"functions.{response_msg['name']}")
225
- msg = msg.with_content_type("json")
226
- else:
227
- raise ValueError(f"Unknown input type: {response_msg['type']}")
228
- return msg
229
-
230
-
157
def parse_chat_inputs_to_harmony_messages(chat_msgs: list) -> list[Message]:
158
"""
159
Parse a list of messages from request.messages in the Chat Completion API to
@@ -390,139 +316,6 @@ def parse_chat_input_to_harmony_message(
316
return msgs
317
318
393
-def parse_input_to_harmony_message(chat_msg) -> list[Message]:
394
- """Parse a message from request.previous_input_messages
395
- into Harmony messages.
396
-
397
- Supports both OpenAI chat format ({"role": "..."}) and
398
- Harmony format ({"author": {"role": "..."}}).
399
- """
400
- if not isinstance(chat_msg, dict):
401
- chat_msg = chat_msg.model_dump(exclude_none=True)
402
-
403
- if "author" in chat_msg and isinstance(chat_msg.get("author"), dict):
404
- return [_parse_harmony_format_message(chat_msg)]
405
-
406
- return _parse_chat_format_message(chat_msg)
407
-
408
-
409
-def _parse_harmony_format_message(chat_msg: dict) -> Message:
410
- """Reconstruct a Message from Harmony-format dict,
411
- preserving channel, recipient, and content_type."""
412
- author_dict = chat_msg["author"]
413
- role = author_dict.get("role")
414
- name = author_dict.get("name")
415
-
416
- raw_content = chat_msg.get("content", "")
417
- if isinstance(raw_content, list):
418
- # TODO: Support refusal and non-text content types.
419
- contents = [TextContent(text=c.get("text", "")) for c in raw_content]
420
- elif isinstance(raw_content, str):
421
- contents = [TextContent(text=raw_content)]
422
- else:
423
- contents = [TextContent(text="")]
424
-
425
- if name:
426
- msg = Message.from_author_and_contents(Author.new(Role(role), name), contents)
427
- else:
428
- msg = Message.from_role_and_contents(Role(role), contents)
429
-
430
- channel = chat_msg.get("channel")
431
- if channel:
432
- msg = msg.with_channel(channel)
433
- recipient = chat_msg.get("recipient")
434
- if recipient:
435
- msg = msg.with_recipient(recipient)
436
- content_type = chat_msg.get("content_type")
437
- if content_type:
438
- msg = msg.with_content_type(content_type)
439
-
440
- return msg
441
-
442
-
443
-def _parse_chat_format_message(chat_msg: dict) -> list[Message]:
444
- """Parse an OpenAI chat-format dict into Harmony messages."""
445
- role = chat_msg.get("role")
446
- if role is None:
447
- raise ValueError(f"Message has no 'role' key: {chat_msg}")
448
-
449
- # Assistant message with tool calls
450
- tool_calls = chat_msg.get("tool_calls")
451
- if role == "assistant" and tool_calls:
452
- msgs: list[Message] = []
453
- for call in tool_calls:
454
- func = call.get("function", {})
455
- name = func.get("name", "")
456
- arguments = func.get("arguments", "") or ""
457
- msg = Message.from_role_and_content(Role.ASSISTANT, arguments)
458
- msg = msg.with_channel("commentary")
459
- msg = msg.with_recipient(f"functions.{name}")
460
- msg = msg.with_content_type("json")
461
- msgs.append(msg)
462
- return msgs
463
-
464
- # Tool role message (tool output)
465
- if role == "tool":
466
- name = chat_msg.get("name", "")
467
- if name and not name.startswith("functions."):
468
- name = f"functions.{name}"
469
- content = chat_msg.get("content", "") or ""
470
- content = flatten_chat_text_content(content)
471
- # NOTE: .with_recipient("assistant") is required on tool messages
472
- # to match parse_chat_input_to_harmony_message behavior and ensure
473
- # proper routing in the Harmony protocol.
474
- msg = (
475
- Message.from_author_and_content(Author.new(Role.TOOL, name), content)
476
- .with_channel("commentary")
477
- .with_recipient("assistant")
478
- )
479
- return [msg]
480
-
481
- # Default: user/assistant/system messages
482
- content = chat_msg.get("content", "")
483
- if isinstance(content, str):
484
- contents = [TextContent(text=content)]
485
- else:
486
- # TODO: Support refusal.
487
- contents = [TextContent(text=c.get("text", "")) for c in content]
488
- msg = Message.from_role_and_contents(role, contents)
489
- return [msg]
490
-
491
-
492
-def construct_harmony_previous_input_messages(
493
- request: ResponsesRequest,
494
-) -> list[OpenAIHarmonyMessage]:
495
- messages: list[OpenAIHarmonyMessage] = []
496
- if request.previous_input_messages:
497
- for message in request.previous_input_messages:
498
- # Handle both OpenAIHarmonyMessage objects and dictionary inputs
499
- if isinstance(message, OpenAIHarmonyMessage):
500
- message_role = message.author.role
501
- # To match OpenAI, instructions, reasoning and tools are
502
- # always taken from the most recent Responses API request
503
- # not carried over from previous requests
504
- if (
505
- message_role == OpenAIHarmonyRole.SYSTEM
506
- or message_role == OpenAIHarmonyRole.DEVELOPER
507
- ):
508
- continue
509
- messages.append(message)
510
- else:
511
- harmony_messages = parse_input_to_harmony_message(message)
512
- for harmony_msg in harmony_messages:
513
- message_role = harmony_msg.author.role
514
- # To match OpenAI, instructions, reasoning and tools are
515
- # always taken from the most recent Responses API request
516
- # not carried over from previous requests
517
- if (
518
- message_role == OpenAIHarmonyRole.SYSTEM
519
- or message_role == OpenAIHarmonyRole.DEVELOPER
520
- ):
521
- continue
522
- messages.append(harmony_msg)
523
- return messages
524
-
525
-
319
def render_for_completion(messages: list[Message]) -> list[int]:
320
conversation = Conversation.from_messages(messages)
321
token_ids = get_encoding().render_conversation_for_completion(
@@ -531,313 +324,6 @@ def render_for_completion(messages: list[Message]) -> list[int]:
324
return token_ids
325
326
534
-def _parse_browser_tool_call(message: Message, recipient: str) -> ResponseOutputItem:
535
- """Parse browser tool calls (search, open, find) into web search items."""
536
- if len(message.content) != 1:
537
- raise ValueError("Invalid number of contents in browser message")
538
- content = message.content[0]
539
-
540
- # Parse JSON args (with retry detection)
541
- try:
542
- browser_call = json.loads(content.text)
543
- except json.JSONDecodeError:
544
- logger.warning(
545
- "Invalid JSON in browser tool call, using error placeholder: %s",
546
- content.text,
547
- )
548
- json_retry_output_message = (
549
- f"Invalid JSON args, caught and retried: {content.text}"
550
- )
551
- browser_call = {
552
- "query": json_retry_output_message,
553
- "url": json_retry_output_message,
554
- "pattern": json_retry_output_message,
555
- }
556
-
557
- # Create appropriate action based on recipient
558
- if recipient == "browser.search":
559
- action = ActionSearch(
560
- query=f"cursor:{browser_call.get('query', '')}", type="search"
561
- )
562
- elif recipient == "browser.open":
563
- action = ActionOpenPage(
564
- url=f"cursor:{browser_call.get('url', '')}", type="open_page"
565
- )
566
- elif recipient == "browser.find":
567
- action = ActionFind(
568
- pattern=browser_call.get("pattern", ""),
569
- url=f"cursor:{browser_call.get('url', '')}",
570
- type="find",
571
- )
572
- else:
573
- raise ValueError(f"Unknown browser action: {recipient}")
574
-
575
- return ResponseFunctionWebSearch(
576
- id=f"ws_{random_uuid()}",
577
- action=action,
578
- status="completed",
579
- type="web_search_call",
580
- )
581
-
582
-
583
-def _parse_function_call(message: Message, recipient: str) -> list[ResponseOutputItem]:
584
- """Parse function calls into function tool call items."""
585
- function_name = recipient.split(".")[-1]
586
- output_items = []
587
- for content in message.content:
588
- random_id = random_uuid()
589
- response_item = ResponseFunctionToolCall(
590
- arguments=content.text,
591
- call_id=f"call_{random_id}",
592
- type="function_call",
593
- name=function_name,
594
- id=f"fc_{random_id}",
595
- )
596
- output_items.append(response_item)
597
- return output_items
598
-
599
-
600
-def _parse_reasoning(message: Message) -> list[ResponseOutputItem]:
601
- """Parse reasoning/analysis content into reasoning items."""
602
- output_items = []
603
- for content in message.content:
604
- reasoning_item = ResponseReasoningItem(
605
- id=f"rs_{random_uuid()}",
606
- summary=[],
607
- type="reasoning",
608
- content=[
609
- ResponseReasoningTextContent(text=content.text, type="reasoning_text")
610
- ],
611
- status=None,
612
- )
613
- output_items.append(reasoning_item)
614
- return output_items
615
-
616
-
617
-def _parse_final_message(message: Message) -> ResponseOutputItem:
618
- """Parse final channel messages into output message items."""
619
- contents = []
620
- for content in message.content:
621
- output_text = ResponseOutputText(
622
- text=content.text,
623
- annotations=[], # TODO
624
- type="output_text",
625
- logprobs=None, # TODO
626
- )
627
- contents.append(output_text)
628
- return ResponseOutputMessage(
629
- id=f"msg_{random_uuid()}",
630
- content=contents,
631
- role=message.author.role,
632
- status="completed",
633
- type="message",
634
- )
635
-
636
-
637
-def _parse_mcp_recipient(recipient: str) -> tuple[str, str]:
638
- """
639
- Parse MCP recipient into (server_label, tool_name).
640
-
641
- For dotted recipients like "repo_browser.list":
642
- - server_label: "repo_browser" (namespace/server)
643
- - tool_name: "list" (specific tool)
644
-
645
- For simple recipients like "filesystem":
646
- - server_label: "filesystem"
647
- - tool_name: "filesystem"
648
- """
649
- if "." in recipient:
650
- server_label = recipient.split(".")[0]
651
- tool_name = recipient.split(".")[-1]
652
- else:
653
- server_label = recipient
654
- tool_name = recipient
655
- return server_label, tool_name
656
-
657
-
658
-def _parse_mcp_call(message: Message, recipient: str) -> list[ResponseOutputItem]:
659
- """Parse MCP calls into MCP call items."""
660
- # Handle built-in tools that need server_label mapping
661
- if recipient in _BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
662
- server_label = _BUILTIN_TOOL_TO_MCP_SERVER_LABEL[recipient]
663
- tool_name = recipient
664
- else:
665
- server_label, tool_name = _parse_mcp_recipient(recipient)
666
-
667
- output_items = []
668
- for content in message.content:
669
- response_item = McpCall(
670
- arguments=content.text,
671
- type="mcp_call",
672
- name=tool_name,
673
- server_label=server_label,
674
- id=f"mcp_{random_uuid()}",
675
- status="completed",
676
- )
677
- output_items.append(response_item)
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.
700
- """
701
- if message.author.role != "assistant":
702
- # This is a message from a tool to the assistant (e.g., search result).
703
- # Don't include it in the final output for now. This aligns with
704
- # OpenAI's behavior on models like o4-mini.
705
- return []
706
-
707
- output_items: list[ResponseOutputItem] = []
708
- recipient = message.recipient
709
-
710
- if recipient is not None:
711
- # Browser tool calls (browser.search, browser.open, browser.find)
712
- if recipient.startswith("browser."):
713
- output_items.append(_parse_browser_tool_call(message, recipient))
714
-
715
- # Function calls (should only happen on commentary channel)
716
- elif message.channel == "commentary" and recipient.startswith("functions."):
717
- output_items.extend(_parse_function_call(message, recipient))
718
-
719
- # Built-in MCP tools (python, browser, container)
720
- elif recipient in _BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
721
- output_items.extend(_parse_reasoning(message))
722
-
723
- # All other recipients are MCP calls
724
- else:
725
- output_items.extend(_parse_mcp_call(message, recipient))
726
-
727
- # No recipient - handle based on channel for non-tool messages
728
- else:
729
- output_items.extend(_parse_message_no_recipient(message))
730
-
731
- return output_items
732
-
733
-
734
-def parse_remaining_state(parser: StreamableParser) -> list[ResponseOutputItem]:
735
- if not parser.current_content:
736
- return []
737
- if parser.current_role != Role.ASSISTANT:
738
- return []
739
- current_recipient = parser.current_recipient
740
- if current_recipient is not None and current_recipient.startswith("browser."):
741
- return []
742
-
743
- if current_recipient and parser.current_channel in ("commentary", "analysis"):
744
- if current_recipient.startswith("functions."):
745
- rid = random_uuid()
746
- return [
747
- ResponseFunctionToolCall(
748
- arguments=parser.current_content,
749
- call_id=f"call_{rid}",
750
- type="function_call",
751
- name=current_recipient.split(".")[-1],
752
- id=f"fc_{rid}",
753
- status="in_progress",
754
- )
755
- ]
756
- # Built-in MCP tools (python, browser, container)
757
- elif current_recipient in _BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
758
- return [
759
- ResponseReasoningItem(
760
- id=f"rs_{random_uuid()}",
761
- summary=[],
762
- type="reasoning",
763
- content=[
764
- ResponseReasoningTextContent(
765
- text=parser.current_content, type="reasoning_text"
766
- )
767
- ],
768
- status=None,
769
- )
770
- ]
771
- # All other recipients are MCP calls
772
- else:
773
- rid = random_uuid()
774
- server_label, tool_name = _parse_mcp_recipient(current_recipient)
775
- return [
776
- McpCall(
777
- arguments=parser.current_content,
778
- type="mcp_call",
779
- name=tool_name,
780
- server_label=server_label,
781
- id=f"mcp_{rid}",
782
- status="in_progress",
783
- )
784
- ]
785
-
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()}",
809
- summary=[],
810
- type="reasoning",
811
- content=[
812
- ResponseReasoningTextContent(
813
- text=parser.current_content, type="reasoning_text"
814
- )
815
- ],
816
- status=None,
817
- )
818
- ]
819
-
820
- if parser.current_channel == "final":
821
- output_text = ResponseOutputText(
822
- text=parser.current_content,
823
- annotations=[], # TODO
824
- type="output_text",
825
- logprobs=None, # TODO
826
- )
827
- text_item = ResponseOutputMessage(
828
- id=f"msg_{random_uuid()}",
829
- content=[output_text],
830
- role="assistant",
831
- # if the parser still has messages (ie if the generator got cut
832
- # abruptly), this should be incomplete
833
- status="incomplete",
834
- type="message",
835
- )
836
- return [text_item]
837
-
838
- return []
839
-
840
-
327
def get_stop_tokens_for_assistant_actions() -> list[int]:
328
return get_encoding().stop_tokens_for_assistant_actions()
329
vllm/entrypoints/openai/responses/harmony.py
new
+552
@@ -0,0 +1,552 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""
4
+Harmony ↔ Responses API conversion utilities.
5
+
6
+Handles two directions:
7
+ 1. Response Input → Harmony Messages (input parsing)
8
+ 2. Harmony Messages → Response Output Items (output parsing)
9
+"""
10
+
11
+import json
12
+
13
+from openai.types.responses import (
14
+ ResponseFunctionToolCall,
15
+ ResponseOutputItem,
16
+ ResponseOutputMessage,
17
+ ResponseOutputText,
18
+ ResponseReasoningItem,
19
+)
20
+from openai.types.responses.response_function_web_search import (
21
+ ActionFind,
22
+ ActionOpenPage,
23
+ ActionSearch,
24
+ ResponseFunctionWebSearch,
25
+)
26
+from openai.types.responses.response_output_item import McpCall
27
+from openai.types.responses.response_reasoning_item import (
28
+ Content as ResponseReasoningTextContent,
29
+)
30
+from openai_harmony import Author, Message, Role, StreamableParser, TextContent
31
+
32
+from vllm.entrypoints.openai.parser.harmony_utils import (
33
+ BUILTIN_TOOL_TO_MCP_SERVER_LABEL,
34
+ flatten_chat_text_content,
35
+)
36
+from vllm.entrypoints.openai.responses.protocol import (
37
+ ResponseInputOutputItem,
38
+ ResponsesRequest,
39
+)
40
+from vllm.logger import init_logger
41
+from vllm.utils import random_uuid
42
+
43
+logger = init_logger(__name__)
44
+
45
+# ---------------------------------------------------------------------------
46
+# 1. Private helpers for input parsing
47
+# ---------------------------------------------------------------------------
48
+
49
+
50
+def _parse_harmony_format_message(chat_msg: dict) -> Message:
51
+ """Reconstruct a Message from Harmony-format dict,
52
+ preserving channel, recipient, and content_type."""
53
+ author_dict = chat_msg["author"]
54
+ role = author_dict.get("role")
55
+ name = author_dict.get("name")
56
+
57
+ raw_content = chat_msg.get("content", "")
58
+ if isinstance(raw_content, list):
59
+ # TODO: Support refusal and non-text content types.
60
+ contents = [TextContent(text=c.get("text", "")) for c in raw_content]
61
+ elif isinstance(raw_content, str):
62
+ contents = [TextContent(text=raw_content)]
63
+ else:
64
+ contents = [TextContent(text="")]
65
+
66
+ if name:
67
+ msg = Message.from_author_and_contents(Author.new(Role(role), name), contents)
68
+ else:
69
+ msg = Message.from_role_and_contents(Role(role), contents)
70
+
71
+ channel = chat_msg.get("channel")
72
+ if channel:
73
+ msg = msg.with_channel(channel)
74
+ recipient = chat_msg.get("recipient")
75
+ if recipient:
76
+ msg = msg.with_recipient(recipient)
77
+ content_type = chat_msg.get("content_type")
78
+ if content_type:
79
+ msg = msg.with_content_type(content_type)
80
+
81
+ return msg
82
+
83
+
84
+def _parse_chat_format_message(chat_msg: dict) -> list[Message]:
85
+ """Parse an OpenAI chat-format dict into Harmony messages."""
86
+ role = chat_msg.get("role")
87
+ if role is None:
88
+ raise ValueError(f"Message has no 'role' key: {chat_msg}")
89
+
90
+ # Assistant message with tool calls
91
+ tool_calls = chat_msg.get("tool_calls")
92
+ if role == "assistant" and tool_calls:
93
+ msgs: list[Message] = []
94
+ for call in tool_calls:
95
+ func = call.get("function", {})
96
+ name = func.get("name", "")
97
+ arguments = func.get("arguments", "") or ""
98
+ msg = Message.from_role_and_content(Role.ASSISTANT, arguments)
99
+ msg = msg.with_channel("commentary")
100
+ msg = msg.with_recipient(f"functions.{name}")
101
+ msg = msg.with_content_type("json")
102
+ msgs.append(msg)
103
+ return msgs
104
+
105
+ # Tool role message (tool output)
106
+ if role == "tool":
107
+ name = chat_msg.get("name", "")
108
+ if name and not name.startswith("functions."):
109
+ name = f"functions.{name}"
110
+ content = chat_msg.get("content", "") or ""
111
+ content = flatten_chat_text_content(content)
112
+ # NOTE: .with_recipient("assistant") is required on tool messages
113
+ # to match parse_chat_input_to_harmony_message behavior and ensure
114
+ # proper routing in the Harmony protocol.
115
+ msg = (
116
+ Message.from_author_and_content(Author.new(Role.TOOL, name), content)
117
+ .with_channel("commentary")
118
+ .with_recipient("assistant")
119
+ )
120
+ return [msg]
121
+
122
+ # Default: user/assistant/system messages
123
+ content = chat_msg.get("content", "")
124
+ if isinstance(content, str):
125
+ contents = [TextContent(text=content)]
126
+ else:
127
+ # TODO: Support refusal.
128
+ contents = [TextContent(text=c.get("text", "")) for c in content]
129
+ msg = Message.from_role_and_contents(role, contents)
130
+ return [msg]
131
+
132
+
133
+# ---------------------------------------------------------------------------
134
+# 2. Public input parsing functions
135
+# ---------------------------------------------------------------------------
136
+
137
+
138
+def response_input_to_harmony(
139
+ response_msg: ResponseInputOutputItem,
140
+ prev_responses: list[ResponseOutputItem | ResponseReasoningItem],
141
+) -> Message:
142
+ """Convert a single ResponseInputOutputItem into a Harmony Message."""
143
+ if not isinstance(response_msg, dict):
144
+ response_msg = response_msg.model_dump()
145
+ if "type" not in response_msg or response_msg["type"] == "message":
146
+ role = response_msg["role"]
147
+ content = response_msg["content"]
148
+ # Add prefix for developer messages.
149
+ # <|start|>developer<|message|># Instructions {instructions}<|end|>
150
+ text_prefix = "Instructions:\n" if role == "developer" else ""
151
+ if isinstance(content, str):
152
+ msg = Message.from_role_and_content(role, text_prefix + content)
153
+ else:
154
+ contents = [TextContent(text=text_prefix + c["text"]) for c in content]
155
+ msg = Message.from_role_and_contents(role, contents)
156
+ if role == "assistant":
157
+ msg = msg.with_channel("final")
158
+ elif response_msg["type"] == "function_call_output":
159
+ call_id = response_msg["call_id"]
160
+ call_response: ResponseFunctionToolCall | None = None
161
+ for prev_response in reversed(prev_responses):
162
+ if (
163
+ isinstance(prev_response, ResponseFunctionToolCall)
164
+ and prev_response.call_id == call_id
165
+ ):
166
+ call_response = prev_response
167
+ break
168
+ if call_response is None:
169
+ raise ValueError(f"No call message found for {call_id}")
170
+ msg = Message.from_author_and_content(
171
+ Author.new(Role.TOOL, f"functions.{call_response.name}"),
172
+ response_msg["output"],
173
+ )
174
+ elif response_msg["type"] == "reasoning":
175
+ content = response_msg["content"]
176
+ assert len(content) == 1
177
+ msg = Message.from_role_and_content(Role.ASSISTANT, content[0]["text"])
178
+ elif response_msg["type"] == "function_call":
179
+ msg = Message.from_role_and_content(Role.ASSISTANT, response_msg["arguments"])
180
+ msg = msg.with_channel("commentary")
181
+ msg = msg.with_recipient(f"functions.{response_msg['name']}")
182
+ msg = msg.with_content_type("json")
183
+ else:
184
+ raise ValueError(f"Unknown input type: {response_msg['type']}")
185
+ return msg
186
+
187
+
188
+def response_previous_input_to_harmony(chat_msg) -> list[Message]:
189
+ """Parse a message from request.previous_input_messages
190
+ into Harmony messages.
191
+
192
+ Supports both OpenAI chat format ({"role": "..."}) and
193
+ Harmony format ({"author": {"role": "..."}}).
194
+ """
195
+ if not isinstance(chat_msg, dict):
196
+ chat_msg = chat_msg.model_dump(exclude_none=True)
197
+
198
+ if "author" in chat_msg and isinstance(chat_msg.get("author"), dict):
199
+ return [_parse_harmony_format_message(chat_msg)]
200
+
201
+ return _parse_chat_format_message(chat_msg)
202
+
203
+
204
+def construct_harmony_previous_input_messages(
205
+ request: ResponsesRequest,
206
+) -> list[Message]:
207
+ """Build a Harmony message list from request.previous_input_messages.
208
+
209
+ Filters out system/developer messages to match OpenAI behavior where
210
+ instructions are always taken from the most recent Responses API request.
211
+ """
212
+ messages: list[Message] = []
213
+ if request.previous_input_messages:
214
+ for message in request.previous_input_messages:
215
+ # Handle both Message objects and dictionary inputs
216
+ if isinstance(message, Message):
217
+ message_role = message.author.role
218
+ if message_role == Role.SYSTEM or message_role == Role.DEVELOPER:
219
+ continue
220
+ messages.append(message)
221
+ else:
222
+ harmony_messages = response_previous_input_to_harmony(message)
223
+ for harmony_msg in harmony_messages:
224
+ message_role = harmony_msg.author.role
225
+ if message_role == Role.SYSTEM or message_role == Role.DEVELOPER:
226
+ continue
227
+ messages.append(harmony_msg)
228
+ return messages
229
+
230
+
231
+# ---------------------------------------------------------------------------
232
+# 3. Private helpers for output parsing
233
+# ---------------------------------------------------------------------------
234
+
235
+
236
+def _parse_browser_tool_call(message: Message, recipient: str) -> ResponseOutputItem:
237
+ """Parse browser tool calls (search, open, find) into web search items."""
238
+ if len(message.content) != 1:
239
+ raise ValueError("Invalid number of contents in browser message")
240
+ content = message.content[0]
241
+
242
+ # Parse JSON args (with retry detection)
243
+ try:
244
+ browser_call = json.loads(content.text)
245
+ except json.JSONDecodeError:
246
+ logger.warning(
247
+ "Invalid JSON in browser tool call, using error placeholder: %s",
248
+ content.text,
249
+ )
250
+ json_retry_output_message = (
251
+ f"Invalid JSON args, caught and retried: {content.text}"
252
+ )
253
+ browser_call = {
254
+ "query": json_retry_output_message,
255
+ "url": json_retry_output_message,
256
+ "pattern": json_retry_output_message,
257
+ }
258
+
259
+ # Create appropriate action based on recipient
260
+ if recipient == "browser.search":
261
+ action = ActionSearch(
262
+ query=f"cursor:{browser_call.get('query', '')}", type="search"
263
+ )
264
+ elif recipient == "browser.open":
265
+ action = ActionOpenPage(
266
+ url=f"cursor:{browser_call.get('url', '')}", type="open_page"
267
+ )
268
+ elif recipient == "browser.find":
269
+ action = ActionFind(
270
+ pattern=browser_call.get("pattern", ""),
271
+ url=f"cursor:{browser_call.get('url', '')}",
272
+ type="find",
273
+ )
274
+ else:
275
+ raise ValueError(f"Unknown browser action: {recipient}")
276
+
277
+ return ResponseFunctionWebSearch(
278
+ id=f"ws_{random_uuid()}",
279
+ action=action,
280
+ status="completed",
281
+ type="web_search_call",
282
+ )
283
+
284
+
285
+def _parse_function_call(message: Message, recipient: str) -> list[ResponseOutputItem]:
286
+ """Parse function calls into function tool call items."""
287
+ function_name = recipient.split(".")[-1]
288
+ output_items = []
289
+ for content in message.content:
290
+ random_id = random_uuid()
291
+ response_item = ResponseFunctionToolCall(
292
+ arguments=content.text,
293
+ call_id=f"call_{random_id}",
294
+ type="function_call",
295
+ name=function_name,
296
+ id=f"fc_{random_id}",
297
+ )
298
+ output_items.append(response_item)
299
+ return output_items
300
+
301
+
302
+def _parse_reasoning(message: Message) -> list[ResponseOutputItem]:
303
+ """Parse reasoning/analysis content into reasoning items."""
304
+ output_items = []
305
+ for content in message.content:
306
+ reasoning_item = ResponseReasoningItem(
307
+ id=f"rs_{random_uuid()}",
308
+ summary=[],
309
+ type="reasoning",
310
+ content=[
311
+ ResponseReasoningTextContent(text=content.text, type="reasoning_text")
312
+ ],
313
+ status=None,
314
+ )
315
+ output_items.append(reasoning_item)
316
+ return output_items
317
+
318
+
319
+def _parse_final_message(message: Message) -> ResponseOutputItem:
320
+ """Parse final channel messages into output message items."""
321
+ contents = []
322
+ for content in message.content:
323
+ output_text = ResponseOutputText(
324
+ text=content.text,
325
+ annotations=[], # TODO
326
+ type="output_text",
327
+ logprobs=None, # TODO
328
+ )
329
+ contents.append(output_text)
330
+ return ResponseOutputMessage(
331
+ id=f"msg_{random_uuid()}",
332
+ content=contents,
333
+ role=message.author.role,
334
+ status="completed",
335
+ type="message",
336
+ )
337
+
338
+
339
+def _parse_mcp_recipient(recipient: str) -> tuple[str, str]:
340
+ """Parse MCP recipient into (server_label, tool_name).
341
+
342
+ For dotted recipients like "repo_browser.list":
343
+ - server_label: "repo_browser" (namespace/server)
344
+ - tool_name: "list" (specific tool)
345
+
346
+ For simple recipients like "filesystem":
347
+ - server_label: "filesystem"
348
+ - tool_name: "filesystem"
349
+ """
350
+ if "." in recipient:
351
+ server_label = recipient.split(".")[0]
352
+ tool_name = recipient.split(".")[-1]
353
+ else:
354
+ server_label = recipient
355
+ tool_name = recipient
356
+ return server_label, tool_name
357
+
358
+
359
+def _parse_mcp_call(message: Message, recipient: str) -> list[ResponseOutputItem]:
360
+ """Parse MCP calls into MCP call items."""
361
+ # Handle built-in tools that need server_label mapping
362
+ if recipient in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
363
+ server_label = BUILTIN_TOOL_TO_MCP_SERVER_LABEL[recipient]
364
+ tool_name = recipient
365
+ else:
366
+ server_label, tool_name = _parse_mcp_recipient(recipient)
367
+
368
+ output_items = []
369
+ for content in message.content:
370
+ response_item = McpCall(
371
+ arguments=content.text,
372
+ type="mcp_call",
373
+ name=tool_name,
374
+ server_label=server_label,
375
+ id=f"mcp_{random_uuid()}",
376
+ status="completed",
377
+ )
378
+ output_items.append(response_item)
379
+ return output_items
380
+
381
+
382
+def _parse_message_no_recipient(
383
+ message: Message,
384
+) -> list[ResponseOutputItem]:
385
+ """Parse a Harmony message with no recipient based on its channel."""
386
+ if message.channel == "analysis":
387
+ return _parse_reasoning(message)
388
+
389
+ if message.channel in ("commentary", "final"):
390
+ # Per Harmony format, preambles (commentary with no recipient) and
391
+ # final channel content are both intended to be shown to end-users.
392
+ # See: https://cookbook.openai.com/articles/openai-harmony
393
+ return [_parse_final_message(message)]
394
+
395
+ raise ValueError(f"Unknown channel: {message.channel}")
396
+
397
+
398
+# ---------------------------------------------------------------------------
399
+# 4. Public output parsing functions
400
+# ---------------------------------------------------------------------------
401
+
402
+
403
+def harmony_to_response_output(message: Message) -> list[ResponseOutputItem]:
404
+ """Parse a Harmony message into a list of output response items.
405
+
406
+ This is the main dispatcher that routes based on channel and recipient.
407
+ """
408
+ if message.author.role != "assistant":
409
+ # This is a message from a tool to the assistant (e.g., search result).
410
+ # Don't include it in the final output for now. This aligns with
411
+ # OpenAI's behavior on models like o4-mini.
412
+ return []
413
+
414
+ output_items: list[ResponseOutputItem] = []
415
+ recipient = message.recipient
416
+
417
+ if recipient is not None:
418
+ # Browser tool calls (browser.search, browser.open, browser.find)
419
+ if recipient.startswith("browser."):
420
+ output_items.append(_parse_browser_tool_call(message, recipient))
421
+
422
+ # Function calls (should only happen on commentary channel)
423
+ elif message.channel == "commentary" and recipient.startswith("functions."):
424
+ output_items.extend(_parse_function_call(message, recipient))
425
+
426
+ # Built-in MCP tools (python, browser, container)
427
+ elif recipient in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
428
+ output_items.extend(_parse_reasoning(message))
429
+
430
+ # All other recipients are MCP calls
431
+ else:
432
+ output_items.extend(_parse_mcp_call(message, recipient))
433
+
434
+ # No recipient - handle based on channel for non-tool messages
435
+ else:
436
+ output_items.extend(_parse_message_no_recipient(message))
437
+
438
+ return output_items
439
+
440
+
441
+def parser_state_to_response_output(
442
+ parser: StreamableParser,
443
+) -> list[ResponseOutputItem]:
444
+ """Extract in-progress response items from incomplete parser state.
445
+
446
+ Called when the parser has buffered content that hasn't formed a
447
+ complete message yet (e.g., generation was cut short).
448
+ """
449
+ if not parser.current_content:
450
+ return []
451
+ if parser.current_role != Role.ASSISTANT:
452
+ return []
453
+ current_recipient = parser.current_recipient
454
+ if current_recipient is not None and current_recipient.startswith("browser."):
455
+ return []
456
+
457
+ if current_recipient and parser.current_channel in ("commentary", "analysis"):
458
+ if current_recipient.startswith("functions."):
459
+ rid = random_uuid()
460
+ return [
461
+ ResponseFunctionToolCall(
462
+ arguments=parser.current_content,
463
+ call_id=f"call_{rid}",
464
+ type="function_call",
465
+ name=current_recipient.split(".")[-1],
466
+ id=f"fc_{rid}",
467
+ status="in_progress",
468
+ )
469
+ ]
470
+ # Built-in MCP tools (python, browser, container)
471
+ elif current_recipient in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
472
+ return [
473
+ ResponseReasoningItem(
474
+ id=f"rs_{random_uuid()}",
475
+ summary=[],
476
+ type="reasoning",
477
+ content=[
478
+ ResponseReasoningTextContent(
479
+ text=parser.current_content, type="reasoning_text"
480
+ )
481
+ ],
482
+ status=None,
483
+ )
484
+ ]
485
+ # All other recipients are MCP calls
486
+ else:
487
+ rid = random_uuid()
488
+ server_label, tool_name = _parse_mcp_recipient(current_recipient)
489
+ return [
490
+ McpCall(
491
+ arguments=parser.current_content,
492
+ type="mcp_call",
493
+ name=tool_name,
494
+ server_label=server_label,
495
+ id=f"mcp_{rid}",
496
+ status="in_progress",
497
+ )
498
+ ]
499
+
500
+ if parser.current_channel == "commentary":
501
+ # Per Harmony format, preambles (commentary with no recipient) are
502
+ # intended to be shown to end-users, unlike analysis channel content.
503
+ output_text = ResponseOutputText(
504
+ text=parser.current_content,
505
+ annotations=[],
506
+ type="output_text",
507
+ logprobs=None,
508
+ )
509
+ return [
510
+ ResponseOutputMessage(
511
+ id=f"msg_{random_uuid()}",
512
+ content=[output_text],
513
+ role="assistant",
514
+ status="incomplete",
515
+ type="message",
516
+ )
517
+ ]
518
+
519
+ if parser.current_channel == "analysis":
520
+ return [
521
+ ResponseReasoningItem(
522
+ id=f"rs_{random_uuid()}",
523
+ summary=[],
524
+ type="reasoning",
525
+ content=[
526
+ ResponseReasoningTextContent(
527
+ text=parser.current_content, type="reasoning_text"
528
+ )
529
+ ],
530
+ status=None,
531
+ )
532
+ ]
533
+
534
+ if parser.current_channel == "final":
535
+ output_text = ResponseOutputText(
536
+ text=parser.current_content,
537
+ annotations=[], # TODO
538
+ type="output_text",
539
+ logprobs=None, # TODO
540
+ )
541
+ text_item = ResponseOutputMessage(
542
+ id=f"msg_{random_uuid()}",
543
+ content=[output_text],
544
+ role="assistant",
545
+ # if the parser still has messages (ie if the generator got cut
546
+ # abruptly), this should be incomplete
547
+ status="incomplete",
548
+ type="message",
549
+ )
550
+ return [text_item]
551
+
552
+ return []
vllm/entrypoints/openai/responses/serving.py
+11
-9
@@ -58,15 +58,11 @@ from vllm.entrypoints.openai.engine.serving import (
58
)
59
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
60
from vllm.entrypoints.openai.parser.harmony_utils import (
61
- construct_harmony_previous_input_messages,
61
get_developer_message,
62
get_stop_tokens_for_assistant_actions,
63
get_system_message,
64
get_user_message,
65
has_custom_tools,
67
- parse_output_message,
68
- parse_remaining_state,
69
- parse_response_input,
66
render_for_completion,
67
)
68
from vllm.entrypoints.openai.responses.context import (
@@ -76,6 +72,12 @@ from vllm.entrypoints.openai.responses.context import (
72
SimpleContext,
73
StreamingHarmonyContext,
74
)
75
+from vllm.entrypoints.openai.responses.harmony import (
76
+ construct_harmony_previous_input_messages,
77
+ harmony_to_response_output,
78
+ parser_state_to_response_output,
79
+ response_input_to_harmony,
80
+)
81
from vllm.entrypoints.openai.responses.protocol import (
82
InputTokensDetails,
83
OutputTokensDetails,
@@ -954,9 +956,9 @@ class OpenAIServingResponses(OpenAIServing):
956
output_items: list[ResponseOutputItem] = []
957
num_init_messages = context.num_init_messages
958
for msg in context.messages[num_init_messages:]:
957
- output_items.extend(parse_output_message(msg))
959
+ output_items.extend(harmony_to_response_output(msg))
960
# Handle the generation stopped in the middle (if any).
959
- last_items = parse_remaining_state(context.parser)
961
+ last_items = parser_state_to_response_output(context.parser)
962
if last_items:
963
output_items.extend(last_items)
964
return output_items
@@ -1103,13 +1105,13 @@ class OpenAIServingResponses(OpenAIServing):
1105
else:
1106
prev_outputs = []
1107
for response_msg in request.input:
1106
- new_msg = parse_response_input(response_msg, prev_outputs)
1108
+ new_msg = response_input_to_harmony(response_msg, prev_outputs)
1109
if new_msg.author.role != "system":
1110
messages.append(new_msg)
1111
1112
# User passes in a tool call request and its output. We need
1111
- # to add the tool call request to prev_outputs so that the
1112
- # parse_response_input can find the tool call request when
1113
+ # to add the tool call request to prev_outputs so that
1114
+ # response_input_to_harmony can find the tool call request when
1115
# parsing the tool call output.
1116
if isinstance(response_msg, ResponseFunctionToolCall):
1117
prev_outputs.append(response_msg)