|
1 | 1 | # SPDX-License-Identifier: Apache-2.0 |
2 | 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
3 | 3 |
|
4 | | -from openai_harmony import Role |
| 4 | +from openai.types.responses.response_output_item import McpCall |
| 5 | +from openai_harmony import Message, Role |
5 | 6 |
|
6 | 7 | from vllm.entrypoints.harmony_utils import ( |
7 | 8 | has_custom_tools, |
8 | 9 | parse_input_to_harmony_message, |
| 10 | + parse_output_message, |
9 | 11 | ) |
10 | 12 |
|
11 | 13 |
|
@@ -264,3 +266,165 @@ def test_has_custom_tools() -> None: |
264 | 266 | assert has_custom_tools( |
265 | 267 | {"web_search_preview", "code_interpreter", "container", "others"} |
266 | 268 | ) |
| 269 | + |
| 270 | + |
| 271 | +def test_parse_mcp_call_basic() -> None: |
| 272 | + """Test that MCP calls are parsed with correct type and server_label.""" |
| 273 | + message = Message.from_role_and_content(Role.ASSISTANT, '{"path": "/tmp"}') |
| 274 | + message = message.with_recipient("filesystem") |
| 275 | + message = message.with_channel("commentary") |
| 276 | + |
| 277 | + output_items = parse_output_message(message) |
| 278 | + |
| 279 | + assert len(output_items) == 1 |
| 280 | + assert isinstance(output_items[0], McpCall) |
| 281 | + assert output_items[0].type == "mcp_call" |
| 282 | + assert output_items[0].name == "filesystem" |
| 283 | + assert output_items[0].server_label == "filesystem" |
| 284 | + assert output_items[0].arguments == '{"path": "/tmp"}' |
| 285 | + assert output_items[0].status == "completed" |
| 286 | + |
| 287 | + |
| 288 | +def test_parse_mcp_call_dotted_recipient() -> None: |
| 289 | + """Test that dotted recipients extract the tool name correctly.""" |
| 290 | + message = Message.from_role_and_content(Role.ASSISTANT, '{"cmd": "ls"}') |
| 291 | + message = message.with_recipient("repo_browser.list") |
| 292 | + message = message.with_channel("commentary") |
| 293 | + |
| 294 | + output_items = parse_output_message(message) |
| 295 | + |
| 296 | + assert len(output_items) == 1 |
| 297 | + assert isinstance(output_items[0], McpCall) |
| 298 | + assert output_items[0].name == "list" |
| 299 | + assert output_items[0].server_label == "repo_browser" |
| 300 | + |
| 301 | + |
| 302 | +def test_mcp_vs_function_call() -> None: |
| 303 | + """Test that function calls are not parsed as MCP calls.""" |
| 304 | + func_message = Message.from_role_and_content(Role.ASSISTANT, '{"arg": "value"}') |
| 305 | + func_message = func_message.with_recipient("functions.my_tool") |
| 306 | + func_message = func_message.with_channel("commentary") |
| 307 | + |
| 308 | + func_items = parse_output_message(func_message) |
| 309 | + |
| 310 | + assert len(func_items) == 1 |
| 311 | + assert not isinstance(func_items[0], McpCall) |
| 312 | + assert func_items[0].type == "function_call" |
| 313 | + |
| 314 | + |
| 315 | +def test_mcp_vs_builtin_tools() -> None: |
| 316 | + """Test that built-in tools (python, container) are not parsed as MCP calls.""" |
| 317 | + # Test python (built-in tool) - should be reasoning, not MCP |
| 318 | + python_message = Message.from_role_and_content(Role.ASSISTANT, "print('hello')") |
| 319 | + python_message = python_message.with_recipient("python") |
| 320 | + python_message = python_message.with_channel("commentary") |
| 321 | + |
| 322 | + python_items = parse_output_message(python_message) |
| 323 | + |
| 324 | + assert len(python_items) == 1 |
| 325 | + assert not isinstance(python_items[0], McpCall) |
| 326 | + assert python_items[0].type == "reasoning" |
| 327 | + |
| 328 | + |
| 329 | +def test_parse_remaining_state_commentary_channel() -> None: |
| 330 | + """Test parse_remaining_state with commentary channel and various recipients.""" |
| 331 | + from unittest.mock import Mock |
| 332 | + |
| 333 | + from vllm.entrypoints.harmony_utils import parse_remaining_state |
| 334 | + |
| 335 | + # Test 1: functions.* recipient → should return function tool call |
| 336 | + parser_func = Mock() |
| 337 | + parser_func.current_content = '{"arg": "value"}' |
| 338 | + parser_func.current_role = Role.ASSISTANT |
| 339 | + parser_func.current_channel = "commentary" |
| 340 | + parser_func.current_recipient = "functions.my_tool" |
| 341 | + |
| 342 | + func_items = parse_remaining_state(parser_func) |
| 343 | + |
| 344 | + assert len(func_items) == 1 |
| 345 | + assert not isinstance(func_items[0], McpCall) |
| 346 | + assert func_items[0].type == "function_call" |
| 347 | + assert func_items[0].name == "my_tool" |
| 348 | + assert func_items[0].status == "in_progress" |
| 349 | + |
| 350 | + # Test 2: MCP tool (not builtin) → should return MCP call |
| 351 | + parser_mcp = Mock() |
| 352 | + parser_mcp.current_content = '{"path": "/tmp"}' |
| 353 | + parser_mcp.current_role = Role.ASSISTANT |
| 354 | + parser_mcp.current_channel = "commentary" |
| 355 | + parser_mcp.current_recipient = "filesystem" |
| 356 | + |
| 357 | + mcp_items = parse_remaining_state(parser_mcp) |
| 358 | + |
| 359 | + assert len(mcp_items) == 1 |
| 360 | + assert isinstance(mcp_items[0], McpCall) |
| 361 | + assert mcp_items[0].type == "mcp_call" |
| 362 | + assert mcp_items[0].name == "filesystem" |
| 363 | + assert mcp_items[0].server_label == "filesystem" |
| 364 | + assert mcp_items[0].status == "in_progress" |
| 365 | + |
| 366 | + # Test 3: Built-in tool (python) → should NOT return MCP call, falls through to reasoning |
| 367 | + parser_builtin = Mock() |
| 368 | + parser_builtin.current_content = "print('hello')" |
| 369 | + parser_builtin.current_role = Role.ASSISTANT |
| 370 | + parser_builtin.current_channel = "commentary" |
| 371 | + parser_builtin.current_recipient = "python" |
| 372 | + |
| 373 | + builtin_items = parse_remaining_state(parser_builtin) |
| 374 | + |
| 375 | + # Should fall through to reasoning logic |
| 376 | + assert len(builtin_items) == 1 |
| 377 | + assert not isinstance(builtin_items[0], McpCall) |
| 378 | + assert builtin_items[0].type == "reasoning" |
| 379 | + |
| 380 | + |
| 381 | +def test_parse_remaining_state_analysis_channel() -> None: |
| 382 | + """Test parse_remaining_state with analysis channel and various recipients.""" |
| 383 | + from unittest.mock import Mock |
| 384 | + |
| 385 | + from vllm.entrypoints.harmony_utils import parse_remaining_state |
| 386 | + |
| 387 | + # Test 1: functions.* recipient → should return function tool call |
| 388 | + parser_func = Mock() |
| 389 | + parser_func.current_content = '{"arg": "value"}' |
| 390 | + parser_func.current_role = Role.ASSISTANT |
| 391 | + parser_func.current_channel = "analysis" |
| 392 | + parser_func.current_recipient = "functions.my_tool" |
| 393 | + |
| 394 | + func_items = parse_remaining_state(parser_func) |
| 395 | + |
| 396 | + assert len(func_items) == 1 |
| 397 | + assert not isinstance(func_items[0], McpCall) |
| 398 | + assert func_items[0].type == "function_call" |
| 399 | + assert func_items[0].name == "my_tool" |
| 400 | + assert func_items[0].status == "in_progress" |
| 401 | + |
| 402 | + # Test 2: MCP tool (not builtin) → should return MCP call |
| 403 | + parser_mcp = Mock() |
| 404 | + parser_mcp.current_content = '{"query": "test"}' |
| 405 | + parser_mcp.current_role = Role.ASSISTANT |
| 406 | + parser_mcp.current_channel = "analysis" |
| 407 | + parser_mcp.current_recipient = "database" |
| 408 | + |
| 409 | + mcp_items = parse_remaining_state(parser_mcp) |
| 410 | + |
| 411 | + assert len(mcp_items) == 1 |
| 412 | + assert isinstance(mcp_items[0], McpCall) |
| 413 | + assert mcp_items[0].type == "mcp_call" |
| 414 | + assert mcp_items[0].name == "database" |
| 415 | + assert mcp_items[0].server_label == "database" |
| 416 | + assert mcp_items[0].status == "in_progress" |
| 417 | + |
| 418 | + # Test 3: Built-in tool (container) → should NOT return MCP call, falls through to reasoning |
| 419 | + parser_builtin = Mock() |
| 420 | + parser_builtin.current_content = "docker run" |
| 421 | + parser_builtin.current_role = Role.ASSISTANT |
| 422 | + parser_builtin.current_channel = "analysis" |
| 423 | + parser_builtin.current_recipient = "container" |
| 424 | + |
| 425 | + builtin_items = parse_remaining_state(parser_builtin) |
| 426 | + |
| 427 | + # Should fall through to reasoning logic |
| 428 | + assert len(builtin_items) == 1 |
| 429 | + assert not isinstance(builtin_items[0], McpCall) |
| 430 | + assert builtin_items[0].type == "reasoning" |
0 commit comments