diff --git a/tests/entrypoints/openai/test_serving_chat.py b/tests/entrypoints/openai/test_serving_chat.py index 2befa40d636d..0d05d31687d6 100644 --- a/tests/entrypoints/openai/test_serving_chat.py +++ b/tests/entrypoints/openai/test_serving_chat.py @@ -15,6 +15,7 @@ from vllm.entrypoints.openai.protocol import ( ChatCompletionRequest, ChatCompletionResponse, + ErrorResponse, RequestResponseMetadata, ) from vllm.entrypoints.openai.serving_chat import OpenAIServingChat @@ -1367,3 +1368,69 @@ async def test_non_tool_reasoning_empty_content_list(self, serving_chat): }, ], ) + + +@pytest.mark.asyncio +async def test_tool_choice_validation_without_parser(): + """Test that tool_choice='required' or named tool without tool_parser + returns an appropriate error message.""" + mock_engine = MagicMock(spec=AsyncLLM) + mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME) + mock_engine.errored = False + mock_engine.model_config = MockModelConfig() + mock_engine.input_processor = MagicMock() + mock_engine.io_processor = MagicMock() + + models = OpenAIServingModels( + engine_client=mock_engine, + base_model_paths=BASE_MODEL_PATHS, + ) + # Create serving_chat without tool_parser (enable_auto_tools=False) + serving_chat = OpenAIServingChat( + mock_engine, + models, + response_role="assistant", + chat_template=CHAT_TEMPLATE, + chat_template_content_format="auto", + request_logger=None, + enable_auto_tools=False, # No tool parser + ) + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the weather in a given location", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + }, + } + ] + + # Test tool_choice="required" without tool_parser + req_required = ChatCompletionRequest( + model=MODEL_NAME, + messages=[{"role": "user", "content": "What's the weather?"}], + tools=tools, + tool_choice="required", + ) + response_required = await serving_chat.create_chat_completion(req_required) + assert isinstance(response_required, ErrorResponse) + assert "tool_choice" in response_required.error.message + assert "--tool-call-parser" in response_required.error.message + + # Test named tool_choice without tool_parser + req_named = ChatCompletionRequest( + model=MODEL_NAME, + messages=[{"role": "user", "content": "What's the weather?"}], + tools=tools, + tool_choice={"type": "function", "function": {"name": "get_weather"}}, + ) + response_named = await serving_chat.create_chat_completion(req_named) + assert isinstance(response_named, ErrorResponse) + assert "tool_choice" in response_named.error.message + assert "--tool-call-parser" in response_named.error.message diff --git a/vllm/entrypoints/openai/serving_chat.py b/vllm/entrypoints/openai/serving_chat.py index 95df373502bf..961aae53be98 100644 --- a/vllm/entrypoints/openai/serving_chat.py +++ b/vllm/entrypoints/openai/serving_chat.py @@ -253,18 +253,31 @@ async def create_chat_completion( truncate_tool_call_ids(request) validate_request_params(request) - if ( - request.tool_choice == "auto" - and not (self.enable_auto_tools and tool_parser is not None) + # Check if tool parsing is unavailable (common condition) + tool_parsing_unavailable = ( + tool_parser is None and not isinstance(tokenizer, MistralTokenizer) and not self.use_harmony + ) + + # Validate tool_choice when tool parsing is required but unavailable + if tool_parsing_unavailable and request.tool_choice not in ( + None, + "none", ): - # for hf tokenizers, "auto" tools requires - # --enable-auto-tool-choice and --tool-call-parser - return self.create_error_response( - '"auto" tool choice requires ' - "--enable-auto-tool-choice and --tool-call-parser to be set" - ) + if request.tool_choice == "auto" and not self.enable_auto_tools: + # for hf tokenizers, "auto" tools requires + # --enable-auto-tool-choice and --tool-call-parser + return self.create_error_response( + '"auto" tool choice requires ' + "--enable-auto-tool-choice and --tool-call-parser to be set" + ) + elif request.tool_choice != "auto": + # "required" or named tool requires tool parser + return self.create_error_response( + f'tool_choice="{request.tool_choice}" requires ' + "--tool-call-parser to be set" + ) if request.tools is None or ( request.tool_choice == "none"