diff --git a/tests/tool_use/test_chat_completion_request_validations.py b/tests/tool_use/test_chat_completion_request_validations.py index d832feda7f..7adf4beb9d 100644 --- a/tests/tool_use/test_chat_completion_request_validations.py +++ b/tests/tool_use/test_chat_completion_request_validations.py @@ -116,3 +116,68 @@ def test_no_reasoning_fields_unchanged(): assistant_msg = request.messages[1] assert assistant_msg.get("reasoning") is None assert "reasoning_content" not in assistant_msg + + +SAMPLE_TOOL = { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the weather", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + }, + }, +} + + +def test_structured_outputs_with_named_tool_choice_rejected(): + """structured_outputs cannot be combined with a named tool_choice.""" + with pytest.raises( + ValueError, + match="structured outputs or tools, not both", + ): + ChatCompletionRequest.model_validate( + { + "messages": [{"role": "user", "content": "Hello"}], + "model": "facebook/opt-125m", + "tools": [SAMPLE_TOOL], + "tool_choice": { + "type": "function", + "function": {"name": "get_weather"}, + }, + "structured_outputs": {"json": {"type": "object"}}, + } + ) + + +def test_structured_outputs_with_auto_tool_choice_allowed(): + """structured_outputs with tool_choice 'auto' should be allowed.""" + request = ChatCompletionRequest.model_validate( + { + "messages": [{"role": "user", "content": "Hello"}], + "model": "facebook/opt-125m", + "tools": [SAMPLE_TOOL], + "tool_choice": "auto", + "structured_outputs": {"json": {"type": "object"}}, + } + ) + assert request.tool_choice == "auto" + + +def test_multiple_structured_outputs_rejected(): + """Only one kind of structured output constraint is allowed.""" + with pytest.raises( + ValueError, + match="You can only use one kind of constraints", + ): + ChatCompletionRequest.model_validate( + { + "messages": [{"role": "user", "content": "Hello"}], + "model": "facebook/opt-125m", + "structured_outputs": { + "json": {"type": "object"}, + "regex": ".*", + }, + } + ) diff --git a/vllm/entrypoints/openai/chat_completion/protocol.py b/vllm/entrypoints/openai/chat_completion/protocol.py index 73ecb3f35a..0be220fff7 100644 --- a/vllm/entrypoints/openai/chat_completion/protocol.py +++ b/vllm/entrypoints/openai/chat_completion/protocol.py @@ -740,19 +740,19 @@ class ChatCompletionRequest(OpenAIBaseModel): ) # you can only use one kind of constraints for structured outputs if count > 1: - raise ValueError( + raise VLLMValidationError( "You can only use one kind of constraints for structured " - "outputs ('json', 'regex' or 'choice')." + "outputs ('json', 'regex' or 'choice').", ) # you can only either use structured outputs or tools, not both - if count > 1 and data.get("tool_choice", "none") not in ( + if count > 0 and data.get("tool_choice", "none") not in ( "none", "auto", "required", ): - raise ValueError( + raise VLLMValidationError( "You can only either use constraints for structured outputs " - "or tools, not both." + "or tools, not both.", ) return data @@ -784,17 +784,21 @@ class ChatCompletionRequest(OpenAIBaseModel): if "tool_choice" in data and data["tool_choice"] is not None: # ensure that if "tool choice" is specified, tools are present if "tools" not in data or data["tools"] is None: - raise ValueError("When using `tool_choice`, `tools` must be set.") + raise VLLMValidationError( + "When using `tool_choice`, `tools` must be set.", + parameter="tool_choice", + ) # make sure that tool choice is either a named tool # OR that it's set to "auto" or "required" if data["tool_choice"] not in ["auto", "required"] and not isinstance( data["tool_choice"], dict ): - raise ValueError( + raise VLLMValidationError( f"Invalid value for `tool_choice`: {data['tool_choice']}! " 'Only named tools, "none", "auto" or "required" ' - "are supported." + "are supported.", + parameter="tool_choice", ) # ensure that if "tool_choice" is specified as an object, @@ -807,29 +811,33 @@ class ChatCompletionRequest(OpenAIBaseModel): valid_tool = False function = data["tool_choice"].get("function") if not isinstance(function, dict): - raise ValueError( + raise VLLMValidationError( f"Invalid value for `function`: `{function}` in " - f"`tool_choice`! {correct_usage_message}" + f"`tool_choice`! {correct_usage_message}", + parameter="tool_choice.function", ) if "name" not in function: - raise ValueError( + raise VLLMValidationError( f"Expected field `name` in `function` in " - f"`tool_choice`! {correct_usage_message}" + f"`tool_choice`! {correct_usage_message}", + parameter="tool_choice.function.name", ) function_name = function["name"] if not isinstance(function_name, str) or len(function_name) == 0: - raise ValueError( + raise VLLMValidationError( f"Invalid `name` in `function`: `{function_name}`" - f" in `tool_choice`! {correct_usage_message}" + f" in `tool_choice`! {correct_usage_message}", + parameter="tool_choice.function.name", ) for tool in data["tools"]: if tool["function"]["name"] == function_name: valid_tool = True break if not valid_tool: - raise ValueError( + raise VLLMValidationError( "The tool specified in `tool_choice` does not match any" - " of the specified `tools`" + " of the specified `tools`", + parameter="tool_choice", ) return data @@ -837,9 +845,9 @@ class ChatCompletionRequest(OpenAIBaseModel): @classmethod def check_generation_prompt(cls, data): if data.get("continue_final_message") and data.get("add_generation_prompt"): - raise ValueError( + raise VLLMValidationError( "Cannot set both `continue_final_message` and " - "`add_generation_prompt` to True." + "`add_generation_prompt` to True.", ) return data @@ -849,8 +857,9 @@ class ChatCompletionRequest(OpenAIBaseModel): if data.get("cache_salt") is not None and ( not isinstance(data["cache_salt"], str) or not data["cache_salt"] ): - raise ValueError( - "Parameter 'cache_salt' must be a non-empty string if provided." + raise VLLMValidationError( + "Parameter 'cache_salt' must be a non-empty string if provided.", + parameter="cache_salt", ) return data diff --git a/vllm/entrypoints/openai/completion/protocol.py b/vllm/entrypoints/openai/completion/protocol.py index cb793a4156..a6c3f9c93d 100644 --- a/vllm/entrypoints/openai/completion/protocol.py +++ b/vllm/entrypoints/openai/completion/protocol.py @@ -447,8 +447,9 @@ class CompletionRequest(OpenAIBaseModel): ) if prompt_is_empty and embeds_is_empty: - raise ValueError( - "Either prompt or prompt_embeds must be provided and non-empty." + raise VLLMValidationError( + "Either prompt or prompt_embeds must be provided and non-empty.", + parameter="prompt", ) return data @@ -459,8 +460,9 @@ class CompletionRequest(OpenAIBaseModel): if data.get("cache_salt") is not None and ( not isinstance(data["cache_salt"], str) or not data["cache_salt"] ): - raise ValueError( - "Parameter 'cache_salt' must be a non-empty string if provided." + raise VLLMValidationError( + "Parameter 'cache_salt' must be a non-empty string if provided.", + parameter="cache_salt", ) return data