From cd329413e2bb2086c2c97c373dcc3bd1ff29fa9f Mon Sep 17 00:00:00 2001 From: Frank <147031176+frank-suwen@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:52:27 -0700 Subject: [PATCH] [Misc] Use VLLMValidationError in Cohere request validation (#52467) Signed-off-by: frank-suwen Co-authored-by: Codex --- tests/entrypoints/cohere/test_api_router.py | 11 ++++++++--- tests/entrypoints/cohere/test_protocol.py | 20 +++++++++++++++++--- vllm/entrypoints/cohere/protocol.py | 14 +++++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/tests/entrypoints/cohere/test_api_router.py b/tests/entrypoints/cohere/test_api_router.py index 45e2ec5cb0..41094f774d 100644 --- a/tests/entrypoints/cohere/test_api_router.py +++ b/tests/entrypoints/cohere/test_api_router.py @@ -36,6 +36,10 @@ from vllm.entrypoints.serve.exception_handling.handlers.http import ( from vllm.entrypoints.serve.exception_handling.handlers.validation import ( validation_exception_handler, ) +from vllm.entrypoints.serve.exception_handling.handlers.vllm_error import ( + vllm_error_handler, +) +from vllm.exceptions import VLLMError @pytest.fixture(autouse=True) @@ -83,7 +87,7 @@ def _build_app(handler: _Handler | None) -> FastAPI: def _build_app_with_vllm_handlers(handler: _Handler | None) -> FastAPI: """Build a FastAPI app that mirrors the real vLLM setup by installing - ``validation_exception_handler`` and ``http_exception_handler``. The + validation, HTTP, and vLLM exception handlers. The :class:`CohereErrorEnvelopeMiddleware` registered by ``attach_router`` is expected to translate any resulting vLLM ``ErrorResponse`` body into the ``CohereError`` wire shape. @@ -96,6 +100,7 @@ def _build_app_with_vllm_handlers(handler: _Handler | None) -> FastAPI: app.state.args = Namespace(log_error_stack=False) app.exception_handler(RequestValidationError)(validation_exception_handler) app.exception_handler(HTTPException)(http_exception_handler) + app.exception_handler(VLLMError)(vllm_error_handler) return app @@ -333,8 +338,8 @@ class TestCohereErrorEnvelope: def test_validation_error_body_is_cohere_shaped(self): # ``model=""`` and ``messages=[]`` trip our custom field - # validators, which raise pydantic ValueErrors and are routed - # through ``validation_exception_handler`` in the real vLLM + # validators, which raise VLLMValidationError and are routed + # through ``vllm_error_handler`` in the real vLLM # server (producing the ``{"error": {...}}`` shape). app = _build_app_with_vllm_handlers(handler=None) with TestClient(app) as client: diff --git a/tests/entrypoints/cohere/test_protocol.py b/tests/entrypoints/cohere/test_protocol.py index 7d9d811047..3e21c1f9ec 100644 --- a/tests/entrypoints/cohere/test_protocol.py +++ b/tests/entrypoints/cohere/test_protocol.py @@ -6,7 +6,8 @@ The module is mostly a thin wrapper around the official ``cohere`` SDK types plus a few local additions: * :class:`CohereError` envelope. -* :class:`CohereChatV2Request` (model required, ``max_tokens`` non-negative). +* :class:`CohereChatV2Request` (model and messages required, + ``max_tokens`` non-negative). * :class:`CohereChatV2Response` plus the usage / logprob helpers. * The streaming event subclasses that bake a wire-format ``type`` discriminator into ``model_dump()`` so SSE consumers can demux on it. @@ -33,6 +34,7 @@ from vllm.entrypoints.cohere.protocol import ( ToolCallStartEvent, ToolPlanDeltaEvent, ) +from vllm.exceptions import VLLMValidationError # ====================================================================== # CohereError @@ -52,16 +54,28 @@ class TestCohereError: class TestCohereChatV2Request: def test_empty_model_rejected(self): - with pytest.raises(ValidationError, match="model is required"): + with pytest.raises(VLLMValidationError, match="model is required") as exc_info: CohereChatV2Request(model="", messages=[{"role": "user", "content": "hi"}]) + assert exc_info.value.parameter == "model" + assert exc_info.value.value is None def test_negative_max_tokens_rejected(self): - with pytest.raises(ValidationError, match="non-negative"): + with pytest.raises(VLLMValidationError, match="non-negative") as exc_info: CohereChatV2Request( model="m", messages=[{"role": "user", "content": "hi"}], max_tokens=-1, ) + assert exc_info.value.parameter == "max_tokens" + assert exc_info.value.value == -1 + + def test_empty_messages_rejected(self): + with pytest.raises( + VLLMValidationError, match="messages must contain at least one message" + ) as exc_info: + CohereChatV2Request(model="m", messages=[]) + assert exc_info.value.parameter == "messages" + assert exc_info.value.value is None def test_zero_max_tokens_allowed(self): # Zero is allowed (the docs allow 0 -> return prompt only). diff --git a/vllm/entrypoints/cohere/protocol.py b/vllm/entrypoints/cohere/protocol.py index 2850e8edda..8f3b1f4289 100644 --- a/vllm/entrypoints/cohere/protocol.py +++ b/vllm/entrypoints/cohere/protocol.py @@ -46,6 +46,8 @@ from cohere.types import ( ) from pydantic import BaseModel, Field, field_validator +from vllm.exceptions import VLLMValidationError + # Re-export the SDK wire-format types alongside our local extensions so # ``vllm.entrypoints.cohere.serving`` and friends can import everything # they need from this module. @@ -186,14 +188,18 @@ class CohereChatV2Request(BaseModel): @classmethod def _validate_model(cls, v: str) -> str: if not v: - raise ValueError("model is required") + raise VLLMValidationError("model is required", parameter="model") return v @field_validator("max_tokens") @classmethod def _validate_max_tokens(cls, v: int | None) -> int | None: if v is not None and v < 0: - raise ValueError("max_tokens must be non-negative") + raise VLLMValidationError( + "max_tokens must be non-negative", + parameter="max_tokens", + value=v, + ) return v @field_validator("messages", mode="before") @@ -233,7 +239,9 @@ class CohereChatV2Request(BaseModel): @classmethod def _validate_messages(cls, v: list[ChatMessageV2]) -> list[ChatMessageV2]: if not v: - raise ValueError("messages must contain at least one message") + raise VLLMValidationError( + "messages must contain at least one message", parameter="messages" + ) return v