|
2 | 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
3 | 3 |
|
4 | 4 | from openai.types.responses import ResponseFunctionToolCall, ResponseReasoningItem |
| 5 | +from openai.types.responses.response_output_item import McpCall |
5 | 6 | from openai_harmony import Author, Message, Role, TextContent |
6 | 7 |
|
7 | 8 | from vllm.entrypoints.harmony_utils import ( |
@@ -451,3 +452,167 @@ def test_has_custom_tools() -> None: |
451 | 452 | assert has_custom_tools( |
452 | 453 | {"web_search_preview", "code_interpreter", "container", "others"} |
453 | 454 | ) |
| 455 | + |
| 456 | + |
| 457 | +def test_parse_mcp_call_basic() -> None: |
| 458 | + """Test that MCP calls are parsed with correct type and server_label.""" |
| 459 | + message = Message.from_role_and_content(Role.ASSISTANT, '{"path": "/tmp"}') |
| 460 | + message = message.with_recipient("filesystem") |
| 461 | + message = message.with_channel("commentary") |
| 462 | + |
| 463 | + output_items = parse_output_message(message) |
| 464 | + |
| 465 | + assert len(output_items) == 1 |
| 466 | + assert isinstance(output_items[0], McpCall) |
| 467 | + assert output_items[0].type == "mcp_call" |
| 468 | + assert output_items[0].name == "filesystem" |
| 469 | + assert output_items[0].server_label == "filesystem" |
| 470 | + assert output_items[0].arguments == '{"path": "/tmp"}' |
| 471 | + assert output_items[0].status == "completed" |
| 472 | + |
| 473 | + |
| 474 | +def test_parse_mcp_call_dotted_recipient() -> None: |
| 475 | + """Test that dotted recipients extract the tool name correctly.""" |
| 476 | + message = Message.from_role_and_content(Role.ASSISTANT, '{"cmd": "ls"}') |
| 477 | + message = message.with_recipient("repo_browser.list") |
| 478 | + message = message.with_channel("commentary") |
| 479 | + |
| 480 | + output_items = parse_output_message(message) |
| 481 | + |
| 482 | + assert len(output_items) == 1 |
| 483 | + assert isinstance(output_items[0], McpCall) |
| 484 | + assert output_items[0].name == "list" |
| 485 | + assert output_items[0].server_label == "repo_browser" |
| 486 | + |
| 487 | + |
| 488 | +def test_mcp_vs_function_call() -> None: |
| 489 | + """Test that function calls are not parsed as MCP calls.""" |
| 490 | + func_message = Message.from_role_and_content(Role.ASSISTANT, '{"arg": "value"}') |
| 491 | + func_message = func_message.with_recipient("functions.my_tool") |
| 492 | + func_message = func_message.with_channel("commentary") |
| 493 | + |
| 494 | + func_items = parse_output_message(func_message) |
| 495 | + |
| 496 | + assert len(func_items) == 1 |
| 497 | + assert not isinstance(func_items[0], McpCall) |
| 498 | + assert func_items[0].type == "function_call" |
| 499 | + |
| 500 | + |
| 501 | +def test_mcp_vs_builtin_tools() -> None: |
| 502 | + """Test that built-in tools (python, container) are not parsed as MCP calls.""" |
| 503 | + # Test python (built-in tool) - should be reasoning, not MCP |
| 504 | + python_message = Message.from_role_and_content(Role.ASSISTANT, "print('hello')") |
| 505 | + python_message = python_message.with_recipient("python") |
| 506 | + python_message = python_message.with_channel("commentary") |
| 507 | + |
| 508 | + python_items = parse_output_message(python_message) |
| 509 | + |
| 510 | + assert len(python_items) == 1 |
| 511 | + assert not isinstance(python_items[0], McpCall) |
| 512 | + assert python_items[0].type == "reasoning" |
| 513 | + |
| 514 | + |
| 515 | +def test_parse_remaining_state_commentary_channel() -> None: |
| 516 | + """Test parse_remaining_state with commentary channel and various recipients.""" |
| 517 | + from unittest.mock import Mock |
| 518 | + |
| 519 | + from vllm.entrypoints.harmony_utils import parse_remaining_state |
| 520 | + |
| 521 | + # Test 1: functions.* recipient → should return function tool call |
| 522 | + parser_func = Mock() |
| 523 | + parser_func.current_content = '{"arg": "value"}' |
| 524 | + parser_func.current_role = Role.ASSISTANT |
| 525 | + parser_func.current_channel = "commentary" |
| 526 | + parser_func.current_recipient = "functions.my_tool" |
| 527 | + |
| 528 | + func_items = parse_remaining_state(parser_func) |
| 529 | + |
| 530 | + assert len(func_items) == 1 |
| 531 | + assert not isinstance(func_items[0], McpCall) |
| 532 | + assert func_items[0].type == "function_call" |
| 533 | + assert func_items[0].name == "my_tool" |
| 534 | + assert func_items[0].status == "in_progress" |
| 535 | + |
| 536 | + # Test 2: MCP tool (not builtin) → should return MCP call |
| 537 | + parser_mcp = Mock() |
| 538 | + parser_mcp.current_content = '{"path": "/tmp"}' |
| 539 | + parser_mcp.current_role = Role.ASSISTANT |
| 540 | + parser_mcp.current_channel = "commentary" |
| 541 | + parser_mcp.current_recipient = "filesystem" |
| 542 | + |
| 543 | + mcp_items = parse_remaining_state(parser_mcp) |
| 544 | + |
| 545 | + assert len(mcp_items) == 1 |
| 546 | + assert isinstance(mcp_items[0], McpCall) |
| 547 | + assert mcp_items[0].type == "mcp_call" |
| 548 | + assert mcp_items[0].name == "filesystem" |
| 549 | + assert mcp_items[0].server_label == "filesystem" |
| 550 | + assert mcp_items[0].status == "in_progress" |
| 551 | + |
| 552 | + # Test 3: Built-in tool (python) |
| 553 | + # should NOT return MCP call, falls through to reasoning |
| 554 | + parser_builtin = Mock() |
| 555 | + parser_builtin.current_content = "print('hello')" |
| 556 | + parser_builtin.current_role = Role.ASSISTANT |
| 557 | + parser_builtin.current_channel = "commentary" |
| 558 | + parser_builtin.current_recipient = "python" |
| 559 | + |
| 560 | + builtin_items = parse_remaining_state(parser_builtin) |
| 561 | + |
| 562 | + # Should fall through to reasoning logic |
| 563 | + assert len(builtin_items) == 1 |
| 564 | + assert not isinstance(builtin_items[0], McpCall) |
| 565 | + assert builtin_items[0].type == "reasoning" |
| 566 | + |
| 567 | + |
| 568 | +def test_parse_remaining_state_analysis_channel() -> None: |
| 569 | + """Test parse_remaining_state with analysis channel and various recipients.""" |
| 570 | + from unittest.mock import Mock |
| 571 | + |
| 572 | + from vllm.entrypoints.harmony_utils import parse_remaining_state |
| 573 | + |
| 574 | + # Test 1: functions.* recipient → should return function tool call |
| 575 | + parser_func = Mock() |
| 576 | + parser_func.current_content = '{"arg": "value"}' |
| 577 | + parser_func.current_role = Role.ASSISTANT |
| 578 | + parser_func.current_channel = "analysis" |
| 579 | + parser_func.current_recipient = "functions.my_tool" |
| 580 | + |
| 581 | + func_items = parse_remaining_state(parser_func) |
| 582 | + |
| 583 | + assert len(func_items) == 1 |
| 584 | + assert not isinstance(func_items[0], McpCall) |
| 585 | + assert func_items[0].type == "function_call" |
| 586 | + assert func_items[0].name == "my_tool" |
| 587 | + assert func_items[0].status == "in_progress" |
| 588 | + |
| 589 | + # Test 2: MCP tool (not builtin) → should return MCP call |
| 590 | + parser_mcp = Mock() |
| 591 | + parser_mcp.current_content = '{"query": "test"}' |
| 592 | + parser_mcp.current_role = Role.ASSISTANT |
| 593 | + parser_mcp.current_channel = "analysis" |
| 594 | + parser_mcp.current_recipient = "database" |
| 595 | + |
| 596 | + mcp_items = parse_remaining_state(parser_mcp) |
| 597 | + |
| 598 | + assert len(mcp_items) == 1 |
| 599 | + assert isinstance(mcp_items[0], McpCall) |
| 600 | + assert mcp_items[0].type == "mcp_call" |
| 601 | + assert mcp_items[0].name == "database" |
| 602 | + assert mcp_items[0].server_label == "database" |
| 603 | + assert mcp_items[0].status == "in_progress" |
| 604 | + |
| 605 | + # Test 3: Built-in tool (container) |
| 606 | + # should NOT return MCP call, falls through to reasoning |
| 607 | + parser_builtin = Mock() |
| 608 | + parser_builtin.current_content = "docker run" |
| 609 | + parser_builtin.current_role = Role.ASSISTANT |
| 610 | + parser_builtin.current_channel = "analysis" |
| 611 | + parser_builtin.current_recipient = "container" |
| 612 | + |
| 613 | + builtin_items = parse_remaining_state(parser_builtin) |
| 614 | + |
| 615 | + # Should fall through to reasoning logic |
| 616 | + assert len(builtin_items) == 1 |
| 617 | + assert not isinstance(builtin_items[0], McpCall) |
| 618 | + assert builtin_items[0].type == "reasoning" |
0 commit comments