diff --git a/tests/entrypoints/cohere/test_api_router.py b/tests/entrypoints/cohere/test_api_router.py index 209ceb4e3e..2c2fce1902 100644 --- a/tests/entrypoints/cohere/test_api_router.py +++ b/tests/entrypoints/cohere/test_api_router.py @@ -598,6 +598,21 @@ class TestCohereErrorEnvelope: assert "message" in body assert isinstance(body["message"], str) and body["message"] + @pytest.mark.parametrize("path", ["/cohere/v2/chat", "/cohere/v2/chat/render"]) + def test_required_tool_choice_without_tools_returns_400(self, path): + """Reject an impossible tool choice before reaching either handler.""" + app = _build_app_with_vllm_handlers(handler=None) + with TestClient(app) as client: + response = client.post( + path, + json={**_minimal_request_body(), "tool_choice": "REQUIRED"}, + ) + assert response.status_code == HTTPStatus.BAD_REQUEST + body = response.json() + assert "error" not in body + assert "REQUIRED" in body["message"] + assert "tools" in body["message"] + def test_http_error_body_is_cohere_shaped(self): # A raised ``HTTPException`` from anywhere in the request cycle # is routed through ``http_exception_handler`` (producing the diff --git a/tests/entrypoints/cohere/test_protocol.py b/tests/entrypoints/cohere/test_protocol.py index dc37ff7cdd..30e625cac2 100644 --- a/tests/entrypoints/cohere/test_protocol.py +++ b/tests/entrypoints/cohere/test_protocol.py @@ -95,6 +95,27 @@ class TestCohereChatV2Request: tool_choice="ANY", # not REQUIRED/NONE ) + @pytest.mark.parametrize("tool_fields", [{}, {"tools": None}, {"tools": []}]) + def test_required_tool_choice_requires_tools(self, tool_fields): + with pytest.raises(VLLMValidationError, match="REQUIRED.*tools") as exc_info: + CohereChatV2Request( + model="m", + messages=[{"role": "user", "content": "hi"}], + tool_choice="REQUIRED", + **tool_fields, + ) + assert exc_info.value.parameter == "tool_choice" + + @pytest.mark.parametrize("tool_choice", [None, "NONE"]) + def test_nonrequired_tool_choice_allows_empty_tools(self, tool_choice): + request = CohereChatV2Request( + model="m", + messages=[{"role": "user", "content": "hi"}], + tool_choice=tool_choice, + tools=[], + ) + assert request.tool_choice == tool_choice + def test_stop_sequences_bounded_by_env_limit(self): messages = [{"role": "user", "content": "hi"}] limit = envs.VLLM_MAX_STOP_STRINGS diff --git a/vllm/entrypoints/cohere/protocol.py b/vllm/entrypoints/cohere/protocol.py index 45e0d310dc..577462f592 100644 --- a/vllm/entrypoints/cohere/protocol.py +++ b/vllm/entrypoints/cohere/protocol.py @@ -44,7 +44,7 @@ from cohere.types import ( ToolV2, UserChatMessageV2, ) -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, model_validator import vllm.envs as envs from vllm.exceptions import VLLMValidationError @@ -211,6 +211,16 @@ class CohereChatV2Request(BaseModel): ) return v + @model_validator(mode="after") + def _validate_tool_choice(self) -> CohereChatV2Request: + if self.tool_choice == "REQUIRED" and not self.tools: + raise VLLMValidationError( + 'tool_choice="REQUIRED" requires at least one tool in tools.', + parameter="tool_choice", + value=self.tool_choice, + ) + return self + @field_validator("messages", mode="before") @classmethod def _normalize_message_roles(cls, v: Any) -> Any: