@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7f65f84428
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 3, 21:30 UTC

[Bugfix] Fix empty channel/recipient in harmony for /v1/responses (#35540)

Signed-off-by: kg6-sleipnir <christopherhazen42@gmail.com> Signed-off-by: chazen <45186108+kg6-sleipnir@users.noreply.github.com> Co-authored-by: Chauncey <chaunceyjiang@gmail.com>

chazen committed May 12, 2026 at 04:45 UTC 7f65f8442827196970b1f3b09cb56ca2f6ea7c5d
3 files changed +768
tests/entrypoints/openai/parser/test_harmony_render_parity.py new
+492
@@ -0,0 +1,492 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Cross-API render parity tests.
4 +
5 +Verifies that the chat completion input path (parse_chat_input_to_harmony_message)
6 +and the responses API input path (response_input_to_harmony) produce identical
7 +Harmony messages and identical rendered token sequences when given equivalent
8 +conversation representations.
9 +
10 +The chat completion API encodes reasoning and tool calls as fields on a single
11 +assistant message dict; the responses API encodes them as separate typed items
12 +in request.input. Both paths must converge on the same Harmony message list and
13 +therefore the same rendered prompt.
14 +
15 +Each test:
16 + 1. Builds Harmony messages from each path for a single message or sequence.
17 + 2. Asserts message-level properties (role, channel, recipient, content)
18 + using verify_harmony_messages.
19 + 3. Asserts that render_for_completion produces identical token sequences.
20 +"""
21 +
22 +from openai.types.responses import ResponseFunctionToolCall
23 +
24 +from tests.entrypoints.openai.utils import verify_harmony_messages
25 +from vllm.entrypoints.openai.parser.harmony_utils import (
26 + get_system_message,
27 + parse_chat_input_to_harmony_message,
28 + render_for_completion,
29 +)
30 +from vllm.entrypoints.openai.responses.harmony import response_input_to_harmony
31 +
32 +# Use a fixed date so the system message is deterministic across both paths.
33 +_DATE = "2025-01-01"
34 +
35 +
36 +def _system():
37 + return get_system_message(start_date=_DATE)
38 +
39 +
40 +class TestResponseInputToHarmonyRenderParity:
41 + """Each test drives the same conversation through both APIs and asserts
42 + identical Harmony messages and rendered token sequences."""
43 +
44 + # -----------------------------------------------------------------------
45 + # Single-message cases
46 + # -----------------------------------------------------------------------
47 +
48 + def test_user_message(self):
49 + chat_msgs = parse_chat_input_to_harmony_message(
50 + {"role": "user", "content": "What's the weather in Paris?"}
51 + )
52 + resp_msgs = [
53 + response_input_to_harmony(
54 + {
55 + "type": "message",
56 + "role": "user",
57 + "content": "What's the weather in Paris?",
58 + },
59 + prev_responses=[],
60 + )
61 + ]
62 +
63 + expected = [{"role": "user", "content": "What's the weather in Paris?"}]
64 + verify_harmony_messages(chat_msgs, expected)
65 + verify_harmony_messages(resp_msgs, expected)
66 +
67 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
68 + [_system()] + resp_msgs
69 + )
70 +
71 + def test_assistant_final_message(self):
72 + chat_msgs = parse_chat_input_to_harmony_message(
73 + {"role": "assistant", "content": "It is 18°C in Paris."}
74 + )
75 + resp_msgs = [
76 + response_input_to_harmony(
77 + {
78 + "type": "message",
79 + "role": "assistant",
80 + "content": "It is 18°C in Paris.",
81 + },
82 + prev_responses=[],
83 + )
84 + ]
85 +
86 + expected = [
87 + {"role": "assistant", "channel": "final", "content": "It is 18°C in Paris."}
88 + ]
89 + verify_harmony_messages(chat_msgs, expected)
90 + verify_harmony_messages(resp_msgs, expected)
91 +
92 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
93 + [_system()] + resp_msgs
94 + )
95 +
96 + def test_reasoning_item(self):
97 + # Chat path: assistant message with only a reasoning field and no content.
98 + chat_msgs = parse_chat_input_to_harmony_message(
99 + {
100 + "role": "assistant",
101 + "reasoning": "I should call get_weather.",
102 + "content": "",
103 + }
104 + )
105 + resp_msgs = [
106 + response_input_to_harmony(
107 + {
108 + "type": "reasoning",
109 + "content": [
110 + {"type": "reasoning_text", "text": "I should call get_weather."}
111 + ],
112 + },
113 + prev_responses=[],
114 + )
115 + ]
116 +
117 + expected = [
118 + {
119 + "role": "assistant",
120 + "channel": "analysis",
121 + "content": "I should call get_weather.",
122 + }
123 + ]
124 + verify_harmony_messages(chat_msgs, expected)
125 + verify_harmony_messages(resp_msgs, expected)
126 +
127 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
128 + [_system()] + resp_msgs
129 + )
130 +
131 + def test_function_call(self):
132 + chat_msgs = parse_chat_input_to_harmony_message(
133 + {
134 + "role": "assistant",
135 + "tool_calls": [
136 + {
137 + "id": "call_1",
138 + "function": {
139 + "name": "get_weather",
140 + "arguments": '{"location": "Paris"}',
141 + },
142 + }
143 + ],
144 + }
145 + )
146 + resp_msgs = [
147 + response_input_to_harmony(
148 + {
149 + "type": "function_call",
150 + "name": "get_weather",
151 + "arguments": '{"location": "Paris"}',
152 + },
153 + prev_responses=[],
154 + )
155 + ]
156 +
157 + expected = [
158 + {
159 + "role": "assistant",
160 + "channel": "commentary",
161 + "recipient": "functions.get_weather",
162 + "content": '{"location": "Paris"}',
163 + "content_type": "json",
164 + }
165 + ]
166 + verify_harmony_messages(chat_msgs, expected)
167 + verify_harmony_messages(resp_msgs, expected)
168 +
169 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
170 + [_system()] + resp_msgs
171 + )
172 +
173 + def test_tool_output(self):
174 + prev_call = ResponseFunctionToolCall(
175 + id="fc_1",
176 + call_id="call_1",
177 + name="get_weather",
178 + arguments='{"location": "Paris"}',
179 + type="function_call",
180 + )
181 +
182 + chat_msgs = parse_chat_input_to_harmony_message(
183 + {"role": "tool", "tool_call_id": "call_1", "content": "18°C, clear skies."},
184 + tool_id_names={"call_1": "get_weather"},
185 + )
186 + resp_msgs = [
187 + response_input_to_harmony(
188 + {
189 + "type": "function_call_output",
190 + "call_id": "call_1",
191 + "output": "18°C, clear skies.",
192 + },
193 + prev_responses=[prev_call],
194 + )
195 + ]
196 +
197 + expected = [
198 + {
199 + "role": "tool",
200 + "author_name": "functions.get_weather",
201 + "channel": "commentary",
202 + "recipient": "assistant",
203 + "content": "18°C, clear skies.",
204 + }
205 + ]
206 + verify_harmony_messages(chat_msgs, expected)
207 + verify_harmony_messages(resp_msgs, expected)
208 +
209 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
210 + [_system()] + resp_msgs
211 + )
212 +
213 + # -----------------------------------------------------------------------
214 + # Combined and multi-turn cases
215 + # -----------------------------------------------------------------------
216 +
217 + def test_reasoning_combined_with_function_call(self):
218 + """Chat API packs reasoning + tool_calls into one dict; responses API
219 + represents them as two separate items. Both must produce the same two
220 + Harmony messages in the same order: analysis then commentary."""
221 + chat_msgs = parse_chat_input_to_harmony_message(
222 + {
223 + "role": "assistant",
224 + "reasoning": "I should get the weather for Paris.",
225 + "tool_calls": [
226 + {
227 + "id": "call_1",
228 + "function": {
229 + "name": "get_weather",
230 + "arguments": '{"location": "Paris"}',
231 + },
232 + }
233 + ],
234 + }
235 + )
236 + resp_msgs = [
237 + response_input_to_harmony(
238 + {
239 + "type": "reasoning",
240 + "content": [
241 + {
242 + "type": "reasoning_text",
243 + "text": "I should get the weather for Paris.",
244 + }
245 + ],
246 + },
247 + prev_responses=[],
248 + ),
249 + response_input_to_harmony(
250 + {
251 + "type": "function_call",
252 + "name": "get_weather",
253 + "arguments": '{"location": "Paris"}',
254 + },
255 + prev_responses=[],
256 + ),
257 + ]
258 +
259 + expected = [
260 + {
261 + "role": "assistant",
262 + "channel": "analysis",
263 + "content": "I should get the weather for Paris.",
264 + },
265 + {
266 + "role": "assistant",
267 + "channel": "commentary",
268 + "recipient": "functions.get_weather",
269 + "content": '{"location": "Paris"}',
270 + "content_type": "json",
271 + },
272 + ]
273 + verify_harmony_messages(chat_msgs, expected)
274 + verify_harmony_messages(resp_msgs, expected)
275 +
276 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
277 + [_system()] + resp_msgs
278 + )
279 +
280 + def test_full_multi_turn_tool_call_conversation(self):
281 + """Full conversation: user -> reasoning + tool_call -> tool_output -> final.
282 +
283 + Both APIs must render the complete conversation to identical token sequences.
284 + This exercises the entire input pipeline including all message types and
285 + the Rust harmony encoder.
286 + """
287 + prev_call = ResponseFunctionToolCall(
288 + id="fc_1",
289 + call_id="call_1",
290 + name="get_weather",
291 + arguments='{"location": "Paris"}',
292 + type="function_call",
293 + )
294 +
295 + # --- Chat completion API path ---
296 + tool_id_names = {"call_1": "get_weather"}
297 + chat_msgs = []
298 + chat_msgs += parse_chat_input_to_harmony_message(
299 + {"role": "user", "content": "What's the weather in Paris?"}
300 + )
301 + chat_msgs += parse_chat_input_to_harmony_message(
302 + {
303 + "role": "assistant",
304 + "reasoning": "I should call get_weather for Paris.",
305 + "tool_calls": [
306 + {
307 + "id": "call_1",
308 + "function": {
309 + "name": "get_weather",
310 + "arguments": '{"location": "Paris"}',
311 + },
312 + }
313 + ],
314 + }
315 + )
316 + chat_msgs += parse_chat_input_to_harmony_message(
317 + {"role": "tool", "tool_call_id": "call_1", "content": "18°C, clear skies."},
318 + tool_id_names=tool_id_names,
319 + )
320 + chat_msgs += parse_chat_input_to_harmony_message(
321 + {
322 + "role": "assistant",
323 + "content": "It is currently 18°C in Paris with clear skies.",
324 + }
325 + )
326 +
327 + # --- Responses API path ---
328 + resp_input = [
329 + {
330 + "type": "message",
331 + "role": "user",
332 + "content": "What's the weather in Paris?",
333 + },
334 + {
335 + "type": "reasoning",
336 + "content": [
337 + {
338 + "type": "reasoning_text",
339 + "text": "I should call get_weather for Paris.",
340 + }
341 + ],
342 + },
343 + {
344 + "type": "function_call",
345 + "name": "get_weather",
346 + "arguments": '{"location": "Paris"}',
347 + },
348 + {
349 + "type": "function_call_output",
350 + "call_id": "call_1",
351 + "output": "18°C, clear skies.",
352 + },
353 + {
354 + "type": "message",
355 + "role": "assistant",
356 + "content": "It is currently 18°C in Paris with clear skies.",
357 + },
358 + ]
359 + resp_msgs = [
360 + response_input_to_harmony(item, prev_responses=[prev_call])
361 + for item in resp_input
362 + ]
363 +
364 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
365 + [_system()] + resp_msgs
366 + )
367 +
368 + def test_multi_turn_two_tool_calls_with_reasoning_between(self):
369 + """Validates parity for a chain of two tool calls, each with its own
370 + reasoning trace. Reasoning traces in between commentary-channel tool
371 + calls must survive as analysis-channel messages in both paths.
372 + """
373 + prev_call_1 = ResponseFunctionToolCall(
374 + id="fc_1",
375 + call_id="call_1",
376 + name="get_weather",
377 + arguments='{"location": "Paris"}',
378 + type="function_call",
379 + )
380 + prev_call_2 = ResponseFunctionToolCall(
381 + id="fc_2",
382 + call_id="call_2",
383 + name="get_forecast",
384 + arguments='{"location": "Paris", "days": 7}',
385 + type="function_call",
386 + )
387 +
388 + # --- Chat completion API path ---
389 + tool_id_names = {"call_1": "get_weather", "call_2": "get_forecast"}
390 + chat_msgs = []
391 + chat_msgs += parse_chat_input_to_harmony_message(
392 + {"role": "user", "content": "What's the weather and forecast for Paris?"}
393 + )
394 + # First reasoning + tool call
395 + chat_msgs += parse_chat_input_to_harmony_message(
396 + {
397 + "role": "assistant",
398 + "reasoning": "I need current weather first.",
399 + "tool_calls": [
400 + {
401 + "id": "call_1",
402 + "function": {
403 + "name": "get_weather",
404 + "arguments": '{"location": "Paris"}',
405 + },
406 + }
407 + ],
408 + }
409 + )
410 + chat_msgs += parse_chat_input_to_harmony_message(
411 + {"role": "tool", "tool_call_id": "call_1", "content": "18°C, clear skies."},
412 + tool_id_names=tool_id_names,
413 + )
414 + # Second reasoning + tool call
415 + chat_msgs += parse_chat_input_to_harmony_message(
416 + {
417 + "role": "assistant",
418 + "reasoning": "Now I need the weekly forecast.",
419 + "tool_calls": [
420 + {
421 + "id": "call_2",
422 + "function": {
423 + "name": "get_forecast",
424 + "arguments": '{"location": "Paris", "days": 7}',
425 + },
426 + }
427 + ],
428 + }
429 + )
430 + chat_msgs += parse_chat_input_to_harmony_message(
431 + {
432 + "role": "tool",
433 + "tool_call_id": "call_2",
434 + "content": "Mon 17°C, Tue 19°C, Wed 16°C",
435 + },
436 + tool_id_names=tool_id_names,
437 + )
438 +
439 + # --- Responses API path ---
440 + prev_responses = [prev_call_1, prev_call_2]
441 + resp_input = [
442 + {
443 + "type": "message",
444 + "role": "user",
445 + "content": "What's the weather and forecast for Paris?",
446 + },
447 + # First reasoning + tool call
448 + {
449 + "type": "reasoning",
450 + "content": [
451 + {"type": "reasoning_text", "text": "I need current weather first."}
452 + ],
453 + },
454 + {
455 + "type": "function_call",
456 + "name": "get_weather",
457 + "arguments": '{"location": "Paris"}',
458 + },
459 + {
460 + "type": "function_call_output",
461 + "call_id": "call_1",
462 + "output": "18°C, clear skies.",
463 + },
464 + # Second reasoning + tool call
465 + {
466 + "type": "reasoning",
467 + "content": [
468 + {
469 + "type": "reasoning_text",
470 + "text": "Now I need the weekly forecast.",
471 + }
472 + ],
473 + },
474 + {
475 + "type": "function_call",
476 + "name": "get_forecast",
477 + "arguments": '{"location": "Paris", "days": 7}',
478 + },
479 + {
480 + "type": "function_call_output",
481 + "call_id": "call_2",
482 + "output": "Mon 17°C, Tue 19°C, Wed 16°C",
483 + },
484 + ]
485 + resp_msgs = [
486 + response_input_to_harmony(item, prev_responses=prev_responses)
487 + for item in resp_input
488 + ]
489 +
490 + assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
491 + [_system()] + resp_msgs
492 + )
tests/entrypoints/openai/responses/test_response_input_to_harmony.py new
+274
@@ -0,0 +1,274 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Unit tests for response_input_to_harmony.
4 +
5 +Covers every type branch in the function and verifies that each produced
6 +Harmony Message has the correct role, channel, recipient, content_type,
7 +author name, and text content.
8 +"""
9 +
10 +import pytest
11 +from openai.types.responses import ResponseFunctionToolCall, ResponseReasoningItem
12 +from openai.types.responses.response_reasoning_item import (
13 + Content as ReasoningTextContent,
14 +)
15 +from openai_harmony import Role
16 +
17 +from vllm.entrypoints.openai.responses.harmony import response_input_to_harmony
18 +
19 +# ---------------------------------------------------------------------------
20 +# Shared fixtures
21 +# ---------------------------------------------------------------------------
22 +
23 +_PREV_CALL = ResponseFunctionToolCall(
24 + id="fc_test",
25 + call_id="call_test",
26 + name="get_weather",
27 + arguments='{"location": "Paris"}',
28 + type="function_call",
29 +)
30 +
31 +_REASONING_ITEM = ResponseReasoningItem(
32 + id="rs_test",
33 + type="reasoning",
34 + content=[ReasoningTextContent(type="reasoning_text", text="Thinking hard.")],
35 + summary=[],
36 + status=None,
37 +)
38 +
39 +
40 +class TestResponseInputToHarmonyMessage:
41 + """Unit tests for every message type handled by response_input_to_harmony."""
42 +
43 + # -----------------------------------------------------------------------
44 + # type="message" (or no type key)
45 + # -----------------------------------------------------------------------
46 +
47 + def test_user_message_string_content(self):
48 + msg = response_input_to_harmony(
49 + {"type": "message", "role": "user", "content": "Hello"},
50 + prev_responses=[],
51 + )
52 +
53 + assert msg.author.role == Role.USER
54 + assert msg.content[0].text == "Hello"
55 + assert msg.channel is None
56 +
57 + def test_no_type_key_defaults_to_message_branch(self):
58 + """Omitting 'type' should fall through to the message branch."""
59 + msg = response_input_to_harmony(
60 + {"role": "user", "content": "Hello"},
61 + prev_responses=[],
62 + )
63 +
64 + assert msg.author.role == Role.USER
65 + assert msg.content[0].text == "Hello"
66 +
67 + def test_system_message(self):
68 + msg = response_input_to_harmony(
69 + {"type": "message", "role": "system", "content": "Be helpful."},
70 + prev_responses=[],
71 + )
72 +
73 + assert msg.author.role == Role.SYSTEM
74 + assert msg.content[0].text == "Be helpful."
75 + assert msg.channel is None
76 +
77 + def test_assistant_message_gets_final_channel(self):
78 + msg = response_input_to_harmony(
79 + {"type": "message", "role": "assistant", "content": "The answer is 42."},
80 + prev_responses=[],
81 + )
82 +
83 + assert msg.author.role == Role.ASSISTANT
84 + assert msg.channel == "final"
85 + assert msg.content[0].text == "The answer is 42."
86 +
87 + def test_developer_message_gets_instructions_prefix(self):
88 + msg = response_input_to_harmony(
89 + {"type": "message", "role": "developer", "content": "Be concise."},
90 + prev_responses=[],
91 + )
92 +
93 + assert msg.author.role == Role.DEVELOPER
94 + assert msg.content[0].text == "Instructions:\nBe concise."
95 + assert msg.channel is None
96 +
97 + def test_message_with_array_content(self):
98 + msg = response_input_to_harmony(
99 + {
100 + "type": "message",
101 + "role": "user",
102 + "content": [
103 + {"type": "text", "text": "Part one. "},
104 + {"type": "text", "text": "Part two."},
105 + ],
106 + },
107 + prev_responses=[],
108 + )
109 +
110 + assert msg.author.role == Role.USER
111 + assert len(msg.content) == 2
112 + assert msg.content[0].text == "Part one. "
113 + assert msg.content[1].text == "Part two."
114 +
115 + def test_developer_message_array_content_gets_prefix_on_each_part(self):
116 + msg = response_input_to_harmony(
117 + {
118 + "type": "message",
119 + "role": "developer",
120 + "content": [
121 + {"type": "text", "text": "Rule 1."},
122 + {"type": "text", "text": "Rule 2."},
123 + ],
124 + },
125 + prev_responses=[],
126 + )
127 +
128 + assert msg.content[0].text == "Instructions:\nRule 1."
129 + assert msg.content[1].text == "Instructions:\nRule 2."
130 +
131 + # -----------------------------------------------------------------------
132 + # type="reasoning"
133 + # -----------------------------------------------------------------------
134 +
135 + def test_reasoning_gets_analysis_channel(self):
136 + msg = response_input_to_harmony(
137 + {
138 + "type": "reasoning",
139 + "content": [
140 + {"type": "reasoning_text", "text": "I should call get_weather."}
141 + ],
142 + },
143 + prev_responses=[],
144 + )
145 +
146 + assert msg.author.role == Role.ASSISTANT
147 + assert msg.channel == "analysis"
148 + assert msg.content[0].text == "I should call get_weather."
149 +
150 + def test_reasoning_pydantic_model_input(self):
151 + """A Pydantic ResponseReasoningItem should be model_dump()'d before parsing."""
152 + msg = response_input_to_harmony(_REASONING_ITEM, prev_responses=[])
153 +
154 + assert msg.author.role == Role.ASSISTANT
155 + assert msg.channel == "analysis"
156 + assert msg.content[0].text == "Thinking hard."
157 +
158 + # -----------------------------------------------------------------------
159 + # type="function_call"
160 + # -----------------------------------------------------------------------
161 +
162 + def test_function_call_channel_recipient_and_content_type(self):
163 + msg = response_input_to_harmony(
164 + {
165 + "type": "function_call",
166 + "name": "get_weather",
167 + "arguments": '{"location": "Paris"}',
168 + },
169 + prev_responses=[],
170 + )
171 +
172 + assert msg.author.role == Role.ASSISTANT
173 + assert msg.channel == "commentary"
174 + assert msg.recipient == "functions.get_weather"
175 + assert msg.content_type == "json"
176 + assert msg.content[0].text == '{"location": "Paris"}'
177 +
178 + def test_function_call_empty_arguments(self):
179 + msg = response_input_to_harmony(
180 + {"type": "function_call", "name": "ping", "arguments": ""},
181 + prev_responses=[],
182 + )
183 +
184 + assert msg.recipient == "functions.ping"
185 + assert msg.content[0].text == ""
186 +
187 + # -----------------------------------------------------------------------
188 + # type="function_call_output"
189 + # -----------------------------------------------------------------------
190 +
191 + def test_function_call_output_channel_recipient_and_author_name(self):
192 + msg = response_input_to_harmony(
193 + {"type": "function_call_output", "call_id": "call_test", "output": "18°C"},
194 + prev_responses=[_PREV_CALL],
195 + )
196 +
197 + assert msg.author.role == Role.TOOL
198 + assert msg.author.name == "functions.get_weather"
199 + assert msg.channel == "commentary"
200 + assert msg.recipient == "assistant"
201 + assert msg.content[0].text == "18°C"
202 +
203 + def test_function_call_output_uses_most_recent_matching_call(self):
204 + """When multiple prev_responses share a call_id, the last one wins
205 + because the search is reversed."""
206 + earlier = ResponseFunctionToolCall(
207 + id="fc_old",
208 + call_id="call_test",
209 + name="old_func",
210 + arguments="{}",
211 + type="function_call",
212 + )
213 + later = ResponseFunctionToolCall(
214 + id="fc_new",
215 + call_id="call_test",
216 + name="get_weather",
217 + arguments="{}",
218 + type="function_call",
219 + )
220 +
221 + msg = response_input_to_harmony(
222 + {
223 + "type": "function_call_output",
224 + "call_id": "call_test",
225 + "output": "result",
226 + },
227 + prev_responses=[earlier, later],
228 + )
229 +
230 + assert msg.author.name == "functions.get_weather"
231 +
232 + def test_function_call_output_skips_non_function_call_items_in_prev_responses(
233 + self,
234 + ):
235 + """ResponseReasoningItem entries in prev_responses should be ignored."""
236 + msg = response_input_to_harmony(
237 + {
238 + "type": "function_call_output",
239 + "call_id": "call_test",
240 + "output": "18°C",
241 + },
242 + prev_responses=[_REASONING_ITEM, _PREV_CALL],
243 + )
244 +
245 + assert msg.author.name == "functions.get_weather"
246 +
247 + def test_function_call_output_raises_if_no_matching_call(self):
248 + with pytest.raises(ValueError, match="No call message found for"):
249 + response_input_to_harmony(
250 + {
251 + "type": "function_call_output",
252 + "call_id": "no_such_id",
253 + "output": "x",
254 + },
255 + prev_responses=[_PREV_CALL],
256 + )
257 +
258 + def test_function_call_output_raises_on_empty_prev_responses(self):
259 + with pytest.raises(ValueError, match="No call message found for"):
260 + response_input_to_harmony(
261 + {"type": "function_call_output", "call_id": "call_test", "output": "x"},
262 + prev_responses=[],
263 + )
264 +
265 + # -----------------------------------------------------------------------
266 + # Error cases
267 + # -----------------------------------------------------------------------
268 +
269 + def test_unknown_type_raises_value_error(self):
270 + with pytest.raises(ValueError, match="Unknown input type"):
271 + response_input_to_harmony(
272 + {"type": "image_url", "url": "https://example.com/img.png"},
273 + prev_responses=[],
274 + )
vllm/entrypoints/openai/responses/harmony.py
+2
@@ -175,6 +175,8 @@ def response_input_to_harmony(
175 Author.new(Role.TOOL, f"functions.{call_response.name}"),
176 response_msg["output"],
177 )
178 + msg = msg.with_channel("commentary")
179 + msg = msg.with_recipient("assistant")
180 elif response_msg["type"] == "reasoning":
181 content = response_msg.get("content")
182 if content and len(content) >= 1: