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 11, 23:45 UTC
[Bugfix] Fix parse_output_message crash on commentary with no recipient (#29972)
Signed-off-by: Shai Trinczer <strinczer@icloud.com> Signed-off-by: strinczer <strinczer@icloud.com>
strinczer committed
Dec 5, 2025 at 10:51 UTC
b73b158ab0715d860e5a86218e0e9605fc2c1fe0
2 files changed
+194
-5
tests/entrypoints/test_harmony_utils.py
+188
-1
@@ -1,11 +1,13 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from openai_harmony import Role
4
+from openai.types.responses import ResponseFunctionToolCall, ResponseReasoningItem
5
+from openai_harmony import Author, Message, Role, TextContent
6
7
from vllm.entrypoints.harmony_utils import (
8
has_custom_tools,
9
parse_input_to_harmony_message,
10
+ parse_output_message,
11
)
12
13
@@ -257,6 +259,191 @@ class TestParseInputToHarmonyMessage:
259
assert messages[0].content[1].text == "actual text"
260
261
262
+class TestParseOutputMessage:
263
+ """Tests for parse_output_message function."""
264
+
265
+ def test_commentary_with_no_recipient_creates_reasoning(self):
266
+ """Test that commentary with recipient=None (preambles) creates reasoning items.
267
+
268
+ Per Harmony format, commentary channel can contain preambles to calling
269
+ multiple functions - explanatory text with no recipient.
270
+ """
271
+ message = Message.from_role_and_content(
272
+ Role.ASSISTANT, "I will now search for the weather information."
273
+ )
274
+ message = message.with_channel("commentary")
275
+ # recipient is None by default, representing a preamble
276
+
277
+ output_items = parse_output_message(message)
278
+
279
+ assert len(output_items) == 1
280
+ assert isinstance(output_items[0], ResponseReasoningItem)
281
+ assert output_items[0].type == "reasoning"
282
+ assert (
283
+ output_items[0].content[0].text
284
+ == "I will now search for the weather information."
285
+ )
286
+ assert output_items[0].content[0].type == "reasoning_text"
287
+
288
+ def test_commentary_with_function_recipient_creates_function_call(self):
289
+ """Test commentary with recipient='functions.X' creates function calls."""
290
+ message = Message.from_role_and_content(
291
+ Role.ASSISTANT, '{"location": "San Francisco", "units": "celsius"}'
292
+ )
293
+ message = message.with_channel("commentary")
294
+ message = message.with_recipient("functions.get_weather")
295
+
296
+ output_items = parse_output_message(message)
297
+
298
+ assert len(output_items) == 1
299
+ assert isinstance(output_items[0], ResponseFunctionToolCall)
300
+ assert output_items[0].type == "function_call"
301
+ assert output_items[0].name == "get_weather"
302
+ assert (
303
+ output_items[0].arguments
304
+ == '{"location": "San Francisco", "units": "celsius"}'
305
+ )
306
+ assert output_items[0].call_id.startswith("call_")
307
+ assert output_items[0].id.startswith("fc_")
308
+
309
+ def test_commentary_with_python_recipient_creates_reasoning(self):
310
+ """Test that commentary with recipient='python' creates reasoning items."""
311
+ message = Message.from_role_and_content(
312
+ Role.ASSISTANT, "import numpy as np\nprint(np.array([1, 2, 3]))"
313
+ )
314
+ message = message.with_channel("commentary")
315
+ message = message.with_recipient("python")
316
+
317
+ output_items = parse_output_message(message)
318
+
319
+ assert len(output_items) == 1
320
+ assert isinstance(output_items[0], ResponseReasoningItem)
321
+ assert output_items[0].type == "reasoning"
322
+ assert (
323
+ output_items[0].content[0].text
324
+ == "import numpy as np\nprint(np.array([1, 2, 3]))"
325
+ )
326
+
327
+ def test_commentary_with_browser_recipient_creates_reasoning(self):
328
+ """Test that commentary with recipient='browser' creates reasoning items."""
329
+ message = Message.from_role_and_content(
330
+ Role.ASSISTANT, "Navigating to the specified URL"
331
+ )
332
+ message = message.with_channel("commentary")
333
+ message = message.with_recipient("browser")
334
+
335
+ output_items = parse_output_message(message)
336
+
337
+ assert len(output_items) == 1
338
+ assert isinstance(output_items[0], ResponseReasoningItem)
339
+ assert output_items[0].type == "reasoning"
340
+ assert output_items[0].content[0].text == "Navigating to the specified URL"
341
+
342
+ def test_commentary_with_container_recipient_creates_reasoning(self):
343
+ """Test that commentary with recipient='container' creates reasoning items."""
344
+ message = Message.from_role_and_content(
345
+ Role.ASSISTANT, "Running command in container"
346
+ )
347
+ message = message.with_channel("commentary")
348
+ message = message.with_recipient("container")
349
+
350
+ output_items = parse_output_message(message)
351
+
352
+ assert len(output_items) == 1
353
+ assert isinstance(output_items[0], ResponseReasoningItem)
354
+ assert output_items[0].type == "reasoning"
355
+ assert output_items[0].content[0].text == "Running command in container"
356
+
357
+ def test_commentary_with_empty_content_and_no_recipient(self):
358
+ """Test edge case: empty commentary with recipient=None."""
359
+ message = Message.from_role_and_content(Role.ASSISTANT, "")
360
+ message = message.with_channel("commentary")
361
+
362
+ output_items = parse_output_message(message)
363
+
364
+ assert len(output_items) == 1
365
+ assert isinstance(output_items[0], ResponseReasoningItem)
366
+ assert output_items[0].content[0].text == ""
367
+
368
+ def test_commentary_with_multiple_contents_and_no_recipient(self):
369
+ """Test multiple content items in commentary with no recipient."""
370
+ contents = [
371
+ TextContent(text="Step 1: Analyze the request"),
372
+ TextContent(text="Step 2: Prepare to call functions"),
373
+ ]
374
+ message = Message.from_role_and_contents(Role.ASSISTANT, contents)
375
+ message = message.with_channel("commentary")
376
+
377
+ output_items = parse_output_message(message)
378
+
379
+ assert len(output_items) == 2
380
+ assert all(isinstance(item, ResponseReasoningItem) for item in output_items)
381
+ assert output_items[0].content[0].text == "Step 1: Analyze the request"
382
+ assert output_items[1].content[0].text == "Step 2: Prepare to call functions"
383
+
384
+ def test_commentary_with_multiple_function_calls(self):
385
+ """Test multiple function calls in commentary channel."""
386
+ contents = [
387
+ TextContent(text='{"location": "San Francisco"}'),
388
+ TextContent(text='{"location": "New York"}'),
389
+ ]
390
+ message = Message.from_role_and_contents(Role.ASSISTANT, contents)
391
+ message = message.with_channel("commentary")
392
+ message = message.with_recipient("functions.get_weather")
393
+
394
+ output_items = parse_output_message(message)
395
+
396
+ assert len(output_items) == 2
397
+ assert all(isinstance(item, ResponseFunctionToolCall) for item in output_items)
398
+ assert output_items[0].name == "get_weather"
399
+ assert output_items[1].name == "get_weather"
400
+ assert output_items[0].arguments == '{"location": "San Francisco"}'
401
+ assert output_items[1].arguments == '{"location": "New York"}'
402
+
403
+ def test_commentary_with_unknown_recipient_raises_error(self):
404
+ """Test that commentary with unknown recipient raises ValueError."""
405
+ message = Message.from_role_and_content(Role.ASSISTANT, "some content")
406
+ message = message.with_channel("commentary")
407
+ message = message.with_recipient("unknown_recipient")
408
+
409
+ try:
410
+ parse_output_message(message)
411
+ raise AssertionError("Expected ValueError to be raised")
412
+ except ValueError as e:
413
+ assert "Unknown recipient: unknown_recipient" in str(e)
414
+
415
+ def test_analysis_channel_creates_reasoning(self):
416
+ """Test that analysis channel creates reasoning items."""
417
+ message = Message.from_role_and_content(
418
+ Role.ASSISTANT, "Analyzing the problem step by step..."
419
+ )
420
+ message = message.with_channel("analysis")
421
+
422
+ output_items = parse_output_message(message)
423
+
424
+ assert len(output_items) == 1
425
+ assert isinstance(output_items[0], ResponseReasoningItem)
426
+ assert output_items[0].type == "reasoning"
427
+ assert (
428
+ output_items[0].content[0].text == "Analyzing the problem step by step..."
429
+ )
430
+
431
+ def test_non_assistant_message_returns_empty(self):
432
+ """Test that non-assistant messages return empty list.
433
+
434
+ Per the implementation, tool messages to assistant (e.g., search results)
435
+ are not included in final output to align with OpenAI behavior.
436
+ """
437
+ message = Message.from_author_and_content(
438
+ Author.new(Role.TOOL, "functions.get_weather"),
439
+ "The weather is sunny, 72°F",
440
+ )
441
+
442
+ output_items = parse_output_message(message)
443
+
444
+ assert len(output_items) == 0
445
+
446
+
447
def test_has_custom_tools() -> None:
448
assert not has_custom_tools(set())
449
assert not has_custom_tools({"web_search_preview", "code_interpreter", "container"})
vllm/entrypoints/harmony_utils.py
+6
-4
@@ -455,11 +455,13 @@ def parse_output_message(message: Message) -> list[ResponseOutputItem]:
455
output_items.extend(_parse_function_call(message, recipient))
456
457
# Built-in tools on commentary channel are treated as reasoning for now
458
- elif recipient is not None and (
459
- recipient.startswith("python")
460
- or recipient.startswith("browser")
461
- or recipient.startswith("container")
458
+ elif (
459
+ recipient is None # Preambles: explanatory text before tool calls
460
+ or recipient.startswith(("python", "browser", "container"))
461
):
462
+ # Per Harmony format, commentary channel can contain preambles to calling
463
+ # multiple functions - explanatory text with no recipient. Built-in tool
464
+ # recipients (python/browser/container) also generate reasoning output.
465
output_items.extend(_parse_reasoning_content(message))
466
else:
467
raise ValueError(f"Unknown recipient: {recipient}")