# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import json import pytest from openai_harmony import ( Conversation, DeveloperContent, HarmonyEncodingName, Message, Role, SystemContent, load_harmony_encoding, ) from vllm.entrypoints.openai.protocol import FunctionCall, ToolCall from vllm.entrypoints.openai.tool_parsers.openai_tool_parser import OpenAIToolParser from vllm.transformers_utils.tokenizer import get_tokenizer MODEL = "gpt2" @pytest.fixture(scope="module") def openai_tokenizer(): # The parser does not use the tokenizer, but the constructor requires it. return get_tokenizer(MODEL) @pytest.fixture def openai_tool_parser(openai_tokenizer): return OpenAIToolParser(openai_tokenizer) @pytest.fixture(scope="module") def harmony_encoding(): return load_harmony_encoding(HarmonyEncodingName.HARMONY_GPT_OSS) def assert_tool_calls( actual_tool_calls: list[ToolCall], expected_tool_calls: list[ToolCall], ): assert len(actual_tool_calls) == len(expected_tool_calls) for actual_tool_call, expected_tool_call in zip( actual_tool_calls, expected_tool_calls ): assert isinstance(actual_tool_call.id, str) assert len(actual_tool_call.id) > 16 # Default from protocol.py assert actual_tool_call.type == "function" assert actual_tool_call.function == expected_tool_call.function def test_extract_tool_calls_no_tools(openai_tool_parser, harmony_encoding): convo = Conversation.from_messages( [ Message.from_role_and_content( Role.SYSTEM, SystemContent.new(), ), Message.from_role_and_content( Role.DEVELOPER, DeveloperContent.new().with_instructions("Talk like a pirate!"), ), Message.from_role_and_content(Role.USER, "Arrr, how be you?"), Message.from_role_and_content( Role.ASSISTANT, "This is a test" ).with_channel("final"), ] ) token_ids = harmony_encoding.render_conversation_for_completion( convo, Role.ASSISTANT ) extracted_info = openai_tool_parser.extract_tool_calls( "", request=None, token_ids=token_ids, ) assert not extracted_info.tools_called assert extracted_info.tool_calls == [] assert extracted_info.content == "This is a test" @pytest.mark.parametrize( "tool_args", [ '{"location": "Tokyo"}', '{\n"location": "Tokyo"\n}', ], ) def test_extract_tool_calls_single_tool( openai_tool_parser, harmony_encoding, tool_args ): convo = Conversation.from_messages( [ Message.from_role_and_content(Role.USER, "What is the weather in Tokyo?"), Message.from_role_and_content( Role.ASSISTANT, 'User asks: "What is the weather in Tokyo?" We need to use get_current_weather tool.', # noqa: E501 ).with_channel("analysis"), Message.from_role_and_content(Role.ASSISTANT, tool_args) .with_channel("commentary") .with_recipient("functions.get_current_weather") .with_content_type("json"), ] ) token_ids = harmony_encoding.render_conversation_for_completion( convo, Role.ASSISTANT ) extracted_info = openai_tool_parser.extract_tool_calls( "", request=None, token_ids=token_ids, ) assert extracted_info.tools_called expected_tool_calls = [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps({"location": "Tokyo"}), ) ) ] assert_tool_calls(extracted_info.tool_calls, expected_tool_calls) assert extracted_info.content is None def test_extract_tool_calls_multiple_tools( openai_tool_parser, harmony_encoding, ): convo = Conversation.from_messages( [ Message.from_role_and_content( Role.USER, "What is the weather in Tokyo based on where I'm at?" ), Message.from_role_and_content( Role.ASSISTANT, 'User asks: "What is the weather in Tokyo?" based on their location. We need to use get_current_weather tool and get_user_location tool.', # noqa: E501 ).with_channel("analysis"), Message.from_role_and_content(Role.ASSISTANT, '{"location": "Tokyo"}') .with_channel("commentary") .with_recipient("functions.get_current_weather") .with_content_type("json"), Message.from_role_and_content(Role.ASSISTANT, '{"location": "Tokyo"}') .with_channel("commentary") .with_recipient("functions.get_user_location") .with_content_type("json"), Message.from_role_and_content(Role.ASSISTANT, '{"location": "Tokyo"}') .with_channel("commentary") .with_recipient("functions.no_content_type"), Message.from_role_and_content(Role.ASSISTANT, "foo") .with_channel("commentary") .with_recipient("functions.not_json_no_content_type"), Message.from_role_and_content(Role.ASSISTANT, "{}") .with_channel("commentary") .with_recipient("functions.empty_args") .with_content_type("json"), Message.from_role_and_content(Role.ASSISTANT, "") .with_channel("commentary") .with_recipient("functions.no_args") .with_content_type("json"), ] ) token_ids = harmony_encoding.render_conversation_for_completion( convo, Role.ASSISTANT, ) extracted_info = openai_tool_parser.extract_tool_calls( "", request=None, token_ids=token_ids, ) assert extracted_info.tools_called expected_tool_calls = [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps({"location": "Tokyo"}), ) ), ToolCall( function=FunctionCall( name="get_user_location", arguments=json.dumps({"location": "Tokyo"}), ) ), ToolCall( function=FunctionCall( name="no_content_type", arguments=json.dumps({"location": "Tokyo"}), ) ), ToolCall( function=FunctionCall( name="not_json_no_content_type", arguments="foo", ) ), ToolCall( function=FunctionCall( name="empty_args", arguments=json.dumps({}), ) ), ToolCall( function=FunctionCall( name="no_args", arguments="", ) ), ] assert_tool_calls(extracted_info.tool_calls, expected_tool_calls) assert extracted_info.content is None def test_extract_tool_calls_with_content( openai_tool_parser, harmony_encoding, ): final_content = "This tool call will get the weather." convo = Conversation.from_messages( [ Message.from_role_and_content( Role.USER, "What is the weather in Tokyo based on where I'm at?" ), Message.from_role_and_content( Role.ASSISTANT, 'User asks: "What is the weather in Tokyo?" based on their location. We need to use get_current_weather tool and get_user_location tool.', # noqa: E501 ).with_channel("analysis"), Message.from_role_and_content(Role.ASSISTANT, '{"location": "Tokyo"}') .with_channel("commentary") .with_recipient("functions.get_current_weather") .with_content_type("json"), Message.from_role_and_content(Role.ASSISTANT, final_content).with_channel( "final" ), ] ) token_ids = harmony_encoding.render_conversation_for_completion( convo, Role.ASSISTANT, ) extracted_info = openai_tool_parser.extract_tool_calls( "", request=None, token_ids=token_ids, ) assert extracted_info.tools_called expected_tool_calls = [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps({"location": "Tokyo"}), ) ), ] assert_tool_calls(extracted_info.tool_calls, expected_tool_calls) assert extracted_info.content == final_content