@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / b73b158ab0
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 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}")