[Frontend][Core] Standardize request error handling with VLLMError hierarchy (#49665)

Signed-off-by: Zach Zhu <zzqshu@126.com>

Zach Zhu committed Jul 29, 2026 at 12:20 UTC 58f96593971b6287cdc8867024a6dfb9985c545c
46 files changed +375 -253
tests/engine/test_short_mm_context.py
+5 -1
@@ -3,6 +3,8 @@
3
4 import pytest
5
6 +from vllm.exceptions import VLLMValidationError
7 +
8 from ..conftest import IMAGE_ASSETS
9
10 HF_IMAGE_PROMPTS = IMAGE_ASSETS.prompts(
@@ -19,7 +21,9 @@ models = ["llava-hf/llava-1.5-7b-hf"]
21 def test_context_length_too_short(vllm_runner, image_assets, model):
22 images = [asset.pil_image for asset in image_assets]
23
22 - with pytest.raises(ValueError, match="longer than the maximum model length"):
24 + with pytest.raises(
25 + VLLMValidationError, match="longer than the maximum model length"
26 + ):
27 vllm_model = vllm_runner(
28 model,
29 # LLaVA has a feature size of 576
tests/entrypoints/llm/test_chat.py
+2 -1
@@ -6,6 +6,7 @@ import pytest
6
7 from vllm import LLM
8 from vllm.distributed import cleanup_dist_env_and_memory
9 +from vllm.exceptions import VLLMValidationError
10 from vllm.sampling_params import SamplingParams
11
12
@@ -157,7 +158,7 @@ def test_chat_batch_failure_cleanup(llm_for_failure_test):
158 batch_2 = [valid_msg, valid_msg]
159 sampling_params = SamplingParams(temperature=0, max_tokens=10)
160
160 - with pytest.raises(ValueError, match="maximum context length is"):
161 + with pytest.raises(VLLMValidationError, match="maximum context length is"):
162 llm.chat(batch_1, sampling_params=sampling_params)
163 assert llm.llm_engine.get_num_unfinished_requests() == 0
164
tests/entrypoints/llm/test_prompt_validation.py
+3 -2
@@ -5,17 +5,18 @@ import pytest
5 import torch
6
7 from vllm import LLM
8 +from vllm.exceptions import VLLMValidationError
9
10
11 def test_empty_prompt():
12 llm = LLM(model="openai-community/gpt2", enforce_eager=True)
12 - with pytest.raises(ValueError, match="decoder prompt cannot be empty"):
13 + with pytest.raises(VLLMValidationError, match="decoder prompt cannot be empty"):
14 llm.generate([""])
15
16
17 def test_out_of_vocab_token():
18 llm = LLM(model="openai-community/gpt2", enforce_eager=True)
18 - with pytest.raises(ValueError, match="out of vocabulary"):
19 + with pytest.raises(VLLMValidationError, match="out of vocabulary"):
20 llm.generate({"prompt_token_ids": [999999]})
21
22
tests/entrypoints/multimodal/llm/test_mm_embeds_only.py
+2 -1
@@ -6,6 +6,7 @@ import pytest
6 from tests.entrypoints.multimodal.conftest import managed_llm
7 from vllm import LLM, SamplingParams
8 from vllm.assets.image import ImageAsset
9 +from vllm.exceptions import VLLMValidationError
10
11 MODEL = "llava-hf/llava-1.5-7b-hf"
12 PROMPT = "USER: <image>\nDescribe this image briefly.\nASSISTANT:"
@@ -42,7 +43,7 @@ def test_generate_with_embedding(llm: LLM):
43 def test_raw_image_rejected(llm: LLM):
44 """Raw image input is still rejected when limit=0."""
45 raw_image = ImageAsset("stop_sign").pil_image
45 - with pytest.raises(ValueError, match=r"At most 0 image\(s\)"):
46 + with pytest.raises(VLLMValidationError, match=r"At most 0 image\(s\)"):
47 llm.generate(
48 {"prompt": PROMPT, "multi_modal_data": {"image": raw_image}},
49 sampling_params=SamplingParams(max_tokens=16),
tests/entrypoints/openai/chat_completion/test_chat.py
+4 -3
@@ -18,6 +18,7 @@ from tests.utils import RemoteOpenAIServer
18 from vllm.entrypoints.openai.chat_completion.protocol import (
19 ChatCompletionRequest,
20 )
21 +from vllm.exceptions import VLLMValidationError
22 from vllm.sampling_params import SamplingParams
23
24 # any model with a chat template should work here
@@ -1074,7 +1075,7 @@ def test_chat_completion_request_n_parameter_exceeds_default_limit(
1075 max_tokens=10,
1076 )
1077
1077 - with pytest.raises(ValueError, match="n must be at most"):
1078 + with pytest.raises(VLLMValidationError, match="n must be at most"):
1079 request.to_sampling_params(
1080 max_tokens=10,
1081 default_sampling_params={},
@@ -1136,7 +1137,7 @@ def test_chat_completion_request_n_parameter_custom_limit(
1137 max_tokens=10,
1138 )
1139
1139 - with pytest.raises(ValueError, match="n must be at most 128"):
1140 + with pytest.raises(VLLMValidationError, match="n must be at most 128"):
1141 request_over.to_sampling_params(
1142 max_tokens=10,
1143 default_sampling_params={},
@@ -1160,7 +1161,7 @@ def test_chat_completion_request_n_parameter_massive_value(
1161 max_tokens=1,
1162 )
1163
1163 - with pytest.raises(ValueError, match="n must be at most"):
1164 + with pytest.raises(VLLMValidationError, match="n must be at most"):
1165 request.to_sampling_params(
1166 max_tokens=1,
1167 default_sampling_params={},
tests/entrypoints/openai/chat_completion/test_chat_error.py
+5 -5
@@ -6,7 +6,6 @@ from typing import Any
6 from unittest.mock import AsyncMock, MagicMock, patch
7
8 import pytest
9 -from pydantic import ValidationError
9
10 from vllm.config.multimodal import MultiModalConfig
11 from vllm.entrypoints.openai.chat_completion.protocol import (
@@ -18,6 +17,7 @@ from vllm.entrypoints.openai.engine.protocol import GenerationError
17 from vllm.entrypoints.openai.models.protocol import BaseModelPath
18 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
19 from vllm.entrypoints.scale_out.render.serving import ServingRender
20 +from vllm.exceptions import VLLMValidationError
21 from vllm.outputs import CompletionOutput, RequestOutput
22 from vllm.renderers.hf import HfRenderer
23 from vllm.renderers.online_renderer import OnlineRenderer
@@ -479,7 +479,7 @@ def test_json_schema_response_format_missing_schema():
479 def test_structural_tag_response_format_invalid(format_value):
480 """Malformed structural tags should be rejected during request validation."""
481 with pytest.raises(
482 - ValidationError,
482 + VLLMValidationError,
483 match="Invalid response_format structural_tag",
484 ):
485 ChatCompletionRequest(
@@ -493,7 +493,7 @@ def test_structural_tag_response_format_invalid(format_value):
493 def test_batch_structural_tag_response_format_invalid(format_value):
494 """Batch chat should reject malformed structural tags at request parsing."""
495 with pytest.raises(
496 - ValidationError,
496 + VLLMValidationError,
497 match="Invalid response_format structural_tag",
498 ):
499 BatchChatCompletionRequest(
@@ -507,7 +507,7 @@ def test_batch_structural_tag_response_format_invalid(format_value):
507 def test_structured_outputs_structural_tag_invalid(structural_tag):
508 """Malformed direct structured_outputs structural tags should be rejected."""
509 with pytest.raises(
510 - ValidationError,
510 + VLLMValidationError,
511 match="Invalid structured_outputs structural_tag",
512 ):
513 ChatCompletionRequest(
@@ -521,7 +521,7 @@ def test_structured_outputs_structural_tag_invalid(structural_tag):
521 def test_non_numeric_logprobs_rejected(field_name):
522 """A non-numeric logprobs value must be a clean 400 validation error, not a
523 TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
524 - with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
524 + with pytest.raises(VLLMValidationError, match=f"`{field_name}` must be an integer"):
525 ChatCompletionRequest(
526 model=MODEL_NAME,
527 messages=[{"role": "user", "content": "hello"}],
tests/entrypoints/openai/chat_completion/test_logprob_token_ids.py
+4 -4
@@ -14,11 +14,11 @@ digit-token vocab id).
14 import math
15
16 import pytest
17 -from pydantic import ValidationError
17
18 from tests.utils import RemoteOpenAIServer
19 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
20 from vllm.entrypoints.openai.completion.protocol import CompletionRequest
21 +from vllm.exceptions import VLLMValidationError
22
23 MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
24
@@ -87,7 +87,7 @@ def test_completion_request_decouples_top_k_from_explicit_token_ids():
87
88
89 def test_completion_rejects_explicit_token_ids_without_generated_tokens():
90 - with pytest.raises(ValidationError, match="no output tokens are generated"):
90 + with pytest.raises(VLLMValidationError, match="no output tokens are generated"):
91 CompletionRequest(
92 model=MODEL_NAME,
93 prompt="Hello",
@@ -99,7 +99,7 @@ def test_completion_rejects_explicit_token_ids_without_generated_tokens():
99
100
101 def test_requests_reject_explicit_token_ids_with_beam_search():
102 - with pytest.raises(ValidationError, match="not supported with beam search"):
102 + with pytest.raises(VLLMValidationError, match="not supported with beam search"):
103 ChatCompletionRequest(
104 model=MODEL_NAME,
105 messages=[{"role": "user", "content": "Hello"}],
@@ -108,7 +108,7 @@ def test_requests_reject_explicit_token_ids_with_beam_search():
108 use_beam_search=True,
109 )
110
111 - with pytest.raises(ValidationError, match="not supported with beam search"):
111 + with pytest.raises(VLLMValidationError, match="not supported with beam search"):
112 CompletionRequest(
113 model=MODEL_NAME,
114 prompt="Hello",
tests/entrypoints/openai/chat_completion/test_thinking_token_budget_validation.py
+3 -3
@@ -2,15 +2,15 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 -from pydantic import ValidationError
5
6 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
7 from vllm.entrypoints.openai.completion.protocol import CompletionRequest
8 +from vllm.exceptions import VLLMValidationError
9
10
11 @pytest.mark.parametrize("raw_value", [-2, 0.6, 10.5])
12 def test_chat_completion_request_rejects_invalid_thinking_token_budget(raw_value):
13 - with pytest.raises(ValidationError, match="thinking_token_budget"):
13 + with pytest.raises(VLLMValidationError, match="thinking_token_budget"):
14 ChatCompletionRequest.model_validate(
15 {
16 "model": "qwen",
@@ -44,7 +44,7 @@ def test_chat_completion_request_accepts_minus_one_as_unlimited():
44
45 @pytest.mark.parametrize("raw_value", [0.6, 3.14, -2])
46 def test_completion_request_rejects_invalid_thinking_token_budget(raw_value):
47 - with pytest.raises(ValidationError, match="thinking_token_budget"):
47 + with pytest.raises(VLLMValidationError, match="thinking_token_budget"):
48 CompletionRequest.model_validate(
49 {
50 "model": "qwen",
tests/entrypoints/openai/completion/test_completion_error.py
+4 -4
@@ -6,7 +6,6 @@ from typing import Any
6 from unittest.mock import AsyncMock, MagicMock
7
8 import pytest
9 -from pydantic import ValidationError
9
10 from vllm.config.multimodal import MultiModalConfig
11 from vllm.entrypoints.openai.completion.protocol import CompletionRequest
@@ -18,6 +17,7 @@ from vllm.entrypoints.openai.engine.protocol import (
17 from vllm.entrypoints.openai.models.protocol import BaseModelPath
18 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
19 from vllm.entrypoints.scale_out.render.serving import ServingRender
20 +from vllm.exceptions import VLLMValidationError
21 from vllm.outputs import CompletionOutput, RequestOutput
22 from vllm.renderers.hf import HfRenderer
23 from vllm.renderers.online_renderer import OnlineRenderer
@@ -430,7 +430,7 @@ def test_json_schema_response_format_missing_schema():
430 def test_structural_tag_response_format_invalid(format_value):
431 """Malformed structural tags should be rejected during request validation."""
432 with pytest.raises(
433 - ValidationError,
433 + VLLMValidationError,
434 match="Invalid response_format structural_tag",
435 ):
436 CompletionRequest(
@@ -445,7 +445,7 @@ def test_structural_tag_response_format_invalid(format_value):
445 def test_structured_outputs_structural_tag_invalid(structural_tag):
446 """Malformed direct structured_outputs structural tags should be rejected."""
447 with pytest.raises(
448 - ValidationError,
448 + VLLMValidationError,
449 match="Invalid structured_outputs structural_tag",
450 ):
451 CompletionRequest(
@@ -616,7 +616,7 @@ class TestCompletionPromptListLimit:
616 def test_non_numeric_logprobs_rejected(field_name):
617 """A non-numeric logprobs value must be a clean 400 validation error, not a
618 TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
619 - with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
619 + with pytest.raises(VLLMValidationError, match=f"`{field_name}` must be an integer"):
620 CompletionRequest(
621 model=MODEL_NAME,
622 prompt="Test prompt",
tests/entrypoints/openai/completion/test_prompt_validation.py
+2 -1
@@ -13,6 +13,7 @@ import torch
13
14 from tests.utils import RemoteOpenAIServer
15 from vllm.config import ModelConfig
16 +from vllm.exceptions import VLLMValidationError
17 from vllm.renderers.embed_utils import safe_load_prompt_embeds
18
19
@@ -111,5 +112,5 @@ def test_disable_prompt_embeds(dtype: torch.dtype, seq_len: int, hidden_size: in
112 buffer.seek(0)
113 encoded_tensor = pybase64.b64encode(buffer.getvalue())
114
114 - with pytest.raises(ValueError, match="--enable-prompt-embeds"):
115 + with pytest.raises(VLLMValidationError, match="--enable-prompt-embeds"):
116 safe_load_prompt_embeds(model_config, encoded_tensor)
tests/entrypoints/openai/responses/test_sampling_params.py
+2 -1
@@ -14,6 +14,7 @@ from vllm.entrypoints.openai.responses.protocol import (
14 ResponsesRequest,
15 ResponseTextConfig,
16 )
17 +from vllm.exceptions import VLLMValidationError
18 from vllm.sampling_params import StructuredOutputsParams
19
20
@@ -163,7 +164,7 @@ class TestResponsesRequestSamplingParams:
164 text=text_config,
165 )
166
166 - with pytest.raises(ValueError) as exc_info:
167 + with pytest.raises(VLLMValidationError) as exc_info:
168 request.to_sampling_params(default_max_tokens=1000)
169
170 assert "Cannot specify both structured_outputs and text.format" in str(
tests/entrypoints/serve/instrumentator/test_http_status_metrics.py
+98 -80
@@ -8,54 +8,95 @@ PrometheusInstrumentatorMiddleware before being caught by ServerErrorMiddleware.
8 """
9
10 from argparse import Namespace
11 -from http import HTTPStatus
11
12 import httpx
13 import pytest
15 -from fastapi import FastAPI, HTTPException, Request
16 -from fastapi.exceptions import RequestValidationError
17 -from fastapi.responses import JSONResponse
14 +from fastapi import HTTPException
15 from prometheus_client import CollectorRegistry
19 -from prometheus_fastapi_instrumentator import Instrumentator
16
21 -from vllm.entrypoints.serve.utils.server_utils import exception_handler
22 -from vllm.exceptions import VLLMNotFoundError, VLLMValidationError
17 +from vllm.entrypoints.openai.api_server import build_app
18 +from vllm.exceptions import (
19 + VLLMNotFoundError,
20 + VLLMServerError,
21 + VLLMValidationError,
22 +)
23 +
24 +
25 +@pytest.fixture(scope="module")
26 +def should_do_global_cleanup_after_test() -> bool:
27 + # This suite never initializes distributed/accelerator state.
28 + return False
29 +
30 +
31 +def _build_args() -> Namespace:
32 + """Minimal args for ``build_app``; avoids ``make_arg_parser`` device probing."""
33 + return Namespace(
34 + disable_fastapi_docs=True,
35 + enable_offline_docs=False,
36 + root_path=None,
37 + allowed_origins=["*"],
38 + allow_credentials=False,
39 + allowed_methods=["*"],
40 + allowed_headers=["*"],
41 + api_key=None,
42 + enable_request_id_headers=False,
43 + enable_fault_tolerance=False,
44 + middleware=[],
45 + log_error_stack=False,
46 + )
47
48
25 -@pytest.fixture
49 +@pytest.fixture(scope="module")
50 def registry():
27 - """Create a fresh Prometheus registry for each test."""
51 + """Shared Prometheus registry for the module-scoped app."""
52 return CollectorRegistry()
53
54
31 -@pytest.fixture
55 +@pytest.fixture(scope="module")
56 def app(registry):
33 - """Create a minimal FastAPI app that mirrors vLLM's exception handler
34 - and Prometheus middleware setup."""
35 -
36 - app = FastAPI()
37 -
38 - # Mock app state that exception_handler needs
39 - app.state.args = Namespace(log_error_stack=False)
40 -
41 - # Register exception handlers exactly as vLLM does in build_app()
42 - app.exception_handler(HTTPException)(_http_exception_handler)
43 - app.exception_handler(RequestValidationError)(_validation_exception_handler)
44 - app.exception_handler(ValueError)(exception_handler)
45 - app.exception_handler(TypeError)(exception_handler)
46 - app.exception_handler(OverflowError)(exception_handler)
47 - app.exception_handler(NotImplementedError)(exception_handler)
48 - app.exception_handler(VLLMValidationError)(exception_handler)
49 - app.exception_handler(VLLMNotFoundError)(exception_handler)
50 - app.exception_handler(Exception)(exception_handler)
51 -
52 - # Instrument with Prometheus (same as vLLM's attach_router)
53 - Instrumentator(
54 - excluded_handlers=["/metrics"],
55 - registry=registry,
56 - ).add().instrument(app)
57 -
58 - # Test routes that raise different exception types
57 + """Build the real vLLM FastAPI app once and attach probe routes that raise.
58 +
59 + Patch the name used by ``attach_router`` (imported into the instrumentator
60 + metrics module), not ``vllm.v1.metrics.prometheus`` alone — that binding is
61 + captured at import time.
62 + """
63 + import vllm.entrypoints.serve.instrumentator.metrics as metrics_mod
64 +
65 + original = metrics_mod.get_prometheus_registry
66 + metrics_mod.get_prometheus_registry = lambda: registry
67 + try:
68 + app = build_app(_build_args(), supported_tasks=())
69 + finally:
70 + metrics_mod.get_prometheus_registry = original
71 +
72 + @app.get("/raise_http_exception_400")
73 + async def raise_http_exception_400():
74 + raise HTTPException(status_code=400, detail="bad request")
75 +
76 + @app.get("/raise_http_exception_404")
77 + async def raise_http_exception_404():
78 + raise HTTPException(status_code=404, detail="not found")
79 +
80 + @app.get("/raise_request_validation_error")
81 + async def raise_request_validation_error(n: int):
82 + # Invalid ``n`` triggers FastAPI's RequestValidationError.
83 + return {"n": n}
84 +
85 + @app.get("/raise_vllm_validation_error")
86 + async def raise_vllm_validation_error():
87 + raise VLLMValidationError("bad parameter", parameter="temperature")
88 +
89 + @app.get("/raise_vllm_not_found_error")
90 + async def raise_vllm_not_found_error():
91 + raise VLLMNotFoundError("model not found")
92 +
93 + @app.get("/raise_vllm_server_error")
94 + async def raise_vllm_server_error():
95 + # Bare VLLMServerError goes through vllm_error_handler → 500.
96 + # EngineGenerateError / EngineDeadError are not used here: they call
97 + # terminate_if_errored and need engine/server state.
98 + raise VLLMServerError("internal server failure")
99 +
100 @app.get("/raise_value_error")
101 async def raise_value_error():
102 raise ValueError("invalid input value")
@@ -72,22 +113,6 @@ def app(registry):
113 async def raise_not_implemented_error():
114 raise NotImplementedError("feature not supported")
115
75 - @app.get("/raise_vllm_validation_error")
76 - async def raise_vllm_validation_error():
77 - raise VLLMValidationError("bad parameter", parameter="temperature")
78 -
79 - @app.get("/raise_vllm_not_found_error")
80 - async def raise_vllm_not_found_error():
81 - raise VLLMNotFoundError("model not found")
82 -
83 - @app.get("/raise_http_exception_400")
84 - async def raise_http_exception_400():
85 - raise HTTPException(status_code=400, detail="bad request")
86 -
87 - @app.get("/raise_http_exception_404")
88 - async def raise_http_exception_404():
89 - raise HTTPException(status_code=404, detail="not found")
90 -
116 @app.get("/raise_runtime_error")
117 async def raise_runtime_error():
118 raise RuntimeError("unexpected server error")
@@ -99,14 +124,6 @@ def app(registry):
124 return app
125
126
102 -async def _http_exception_handler(req: Request, exc: HTTPException):
103 - return JSONResponse({"error": exc.detail}, status_code=exc.status_code)
104 -
105 -
106 -async def _validation_exception_handler(req: Request, exc: RequestValidationError):
107 - return JSONResponse({"error": str(exc)}, status_code=HTTPStatus.BAD_REQUEST)
108 -
109 -
127 def _get_http_requests_total(registry, method: str, handler: str):
128 """Extract the http_requests_total metric values grouped by status.
129
@@ -128,31 +145,31 @@ def _get_http_requests_total(registry, method: str, handler: str):
145
146 @pytest.mark.asyncio
147 @pytest.mark.parametrize(
131 - "endpoint,expected_status_group,expected_http_code",
148 + "endpoint,expected_status_group,expected_http_code,request_kwargs",
149 [
133 - # These should record as 4xx in Prometheus
134 - ("/raise_value_error", "4xx", 400),
135 - ("/raise_type_error", "4xx", 400),
136 - ("/raise_overflow_error", "4xx", 400),
137 - ("/raise_vllm_validation_error", "4xx", 400),
138 - ("/raise_vllm_not_found_error", "4xx", 404),
139 - ("/raise_http_exception_400", "4xx", 400),
140 - ("/raise_http_exception_404", "4xx", 404),
141 - # NotImplementedError returns 501 which is still 5xx group
142 - ("/raise_not_implemented_error", "5xx", 501),
143 - # These should record as 5xx in Prometheus (genuine server errors)
144 - ("/raise_runtime_error", "5xx", 500),
145 - # Successful requests should record as 2xx
146 - ("/success", "2xx", 200),
150 + ("/raise_http_exception_400", "4xx", 400, {}),
151 + ("/raise_http_exception_404", "4xx", 404, {}),
152 + ("/raise_request_validation_error", "4xx", 400, {"params": {"n": "x"}}),
153 + ("/raise_vllm_validation_error", "4xx", 400, {}),
154 + ("/raise_vllm_not_found_error", "4xx", 404, {}),
155 + ("/raise_vllm_server_error", "5xx", 500, {}),
156 + ("/raise_value_error", "4xx", 400, {}),
157 + ("/raise_type_error", "4xx", 400, {}),
158 + ("/raise_overflow_error", "4xx", 400, {}),
159 + ("/raise_not_implemented_error", "5xx", 501, {}),
160 + ("/raise_runtime_error", "5xx", 500, {}),
161 + ("/success", "2xx", 200, {}),
162 ],
163 ids=[
164 + "HTTPException(400)->4xx",
165 + "HTTPException(404)->4xx",
166 + "RequestValidationError->4xx",
167 + "VLLMValidationError->4xx",
168 + "VLLMNotFoundError->4xx",
169 + "VLLMServerError->5xx",
170 "ValueError->4xx",
171 "TypeError->4xx",
172 "OverflowError->4xx",
152 - "VLLMValidationError->4xx",
153 - "VLLMNotFoundError->4xx",
154 - "HTTPException(400)->4xx",
155 - "HTTPException(404)->4xx",
173 "NotImplementedError->5xx",
174 "RuntimeError->5xx",
175 "success->2xx",
@@ -164,6 +181,7 @@ async def test_http_requests_total_records_correct_status(
181 endpoint,
182 expected_status_group,
183 expected_http_code,
184 + request_kwargs,
185 ):
186 """Verify that http_requests_total records the correct status group.
187
@@ -177,7 +195,7 @@ async def test_http_requests_total_records_correct_status(
195 async with httpx.AsyncClient(
196 transport=transport, base_url="http://testserver"
197 ) as client:
180 - response = await client.get(endpoint)
198 + response = await client.get(endpoint, **request_kwargs)
199
200 # Verify the HTTP response code returned to the client is correct
201 assert response.status_code == expected_http_code, (
tests/entrypoints/unit_tests/test_chat_utils.py
+3 -2
@@ -21,6 +21,7 @@ from vllm.entrypoints.chat_utils import (
21 parse_chat_messages,
22 parse_chat_messages_async,
23 )
24 +from vllm.exceptions import VLLMValidationError
25 from vllm.inputs import MultiModalDataDict, MultiModalUUIDDict
26 from vllm.multimodal.utils import (
27 encode_audio_url,
@@ -1504,7 +1505,7 @@ def test_parse_chat_messages_rejects_too_many_images_in_one_message(
1505 "ignore",
1506 message="coroutine 'async_get_and_parse_image' was never awaited",
1507 )
1507 - with pytest.raises(ValueError, match="At most"):
1508 + with pytest.raises(VLLMValidationError, match="At most"):
1509 parse_chat_messages(
1510 [
1511 {
@@ -1540,7 +1541,7 @@ def test_parse_chat_messages_rejects_too_many_images_across_messages(
1541 "ignore",
1542 message="coroutine 'async_get_and_parse_image' was never awaited",
1543 )
1543 - with pytest.raises(ValueError, match="At most"):
1544 + with pytest.raises(VLLMValidationError, match="At most"):
1545 parse_chat_messages(
1546 [
1547 {
tests/models/multimodal/processing/test_gemma4.py
+2 -1
@@ -7,6 +7,7 @@ import pytest
7 import torch
8 from PIL import Image as PILImage
9
10 +from vllm.exceptions import VLLMValidationError
11 from vllm.model_executor.models.gemma4_mm import (
12 Gemma4ForConditionalGeneration,
13 Gemma4ImagePixelInputs,
@@ -222,7 +223,7 @@ def test_limit_mm_per_prompt(
223 mm_data = {"image": images}
224
225 # Expect ValueError when exceeding limit
225 - with pytest.raises(ValueError, match="At most 1 image"):
226 + with pytest.raises(VLLMValidationError, match="At most 1 image"):
227 processor(
228 prompt,
229 mm_items=processor.info.parse_mm_data(mm_data),
tests/models/multimodal/processing/test_gemma4_unified.py
+2 -1
@@ -7,6 +7,7 @@ import pytest
7 import torch
8 from PIL import Image as PILImage
9
10 +from vllm.exceptions import VLLMValidationError
11 from vllm.model_executor.models.gemma4_mm import Gemma4ImagePixelInputs
12 from vllm.multimodal import MULTIMODAL_REGISTRY
13 from vllm.multimodal.inputs import MultiModalFieldConfig
@@ -197,7 +198,7 @@ def test_limit_mm_per_prompt(
198
199 mm_data = {"image": images}
200
200 - with pytest.raises(ValueError, match="At most 1 image"):
201 + with pytest.raises(VLLMValidationError, match="At most 1 image"):
202 processor(
203 prompt,
204 mm_items=processor.info.parse_mm_data(mm_data),
tests/multimodal/media/test_unprocessable_entity_error.py
+3 -3
@@ -14,7 +14,7 @@ import aiohttp
14 import pytest
15
16 from vllm.entrypoints.serve.utils.error_response import create_error_response
17 -from vllm.exceptions import VLLMUnprocessableEntityError
17 +from vllm.exceptions import VLLMClientError, VLLMUnprocessableEntityError
18 from vllm.multimodal.media import MediaConnector
19
20
@@ -35,9 +35,9 @@ class TestVLLMUnprocessableEntityError:
35 assert "parameter=image_url" in str(exc)
36 assert "value=https://example.com/image.jpg" in str(exc)
37
38 - def test_is_value_error_subclass(self):
38 + def test_is_client_error_subclass(self):
39 exc = VLLMUnprocessableEntityError("Test")
40 - assert isinstance(exc, ValueError)
40 + assert isinstance(exc, VLLMClientError)
41
42
43 class TestMediaConnectorErrorHandling:
tests/multimodal/test_processing.py
+6 -1
@@ -8,6 +8,7 @@ import numpy as np
8 import pytest
9
10 from vllm.config import ModelConfig
11 +from vllm.exceptions import VLLMValidationError
12 from vllm.multimodal import MULTIMODAL_REGISTRY
13 from vllm.multimodal.processing.context import InputProcessingContext
14 from vllm.multimodal.processing.processor import (
@@ -931,7 +932,11 @@ def test_limit_mm_per_prompt_apply(model_id, num_images, limit, is_valid):
932 else:
933 mm_data = {"image": [image] * num_images}
934
934 - exc_ctx = nullcontext() if is_valid else pytest.raises(ValueError, match="At most")
935 + exc_ctx = (
936 + nullcontext()
937 + if is_valid
938 + else pytest.raises(VLLMValidationError, match="At most")
939 + )
940
941 with exc_ctx:
942 processor(
tests/renderers/test_chat_utils_prompt_embeds.py
+3 -2
@@ -26,6 +26,7 @@ from vllm.entrypoints.chat_utils import (
26 parse_chat_messages,
27 parse_chat_messages_async,
28 )
29 +from vllm.exceptions import VLLMValidationError
30 from vllm.renderers.hf import (
31 _PROMPT_EMBEDS_PLACEHOLDER_SPAN_MISMATCH_ERROR,
32 _build_mixed_prompt_embeds,
@@ -264,7 +265,7 @@ def test_parse_chat_messages_requires_flag():
265 "content": [{"type": "prompt_embeds", "data": b64}],
266 }
267 ]
267 - with pytest.raises(ValueError, match=_ENABLE_PROMPT_EMBEDS_ERROR):
268 + with pytest.raises(VLLMValidationError, match=_ENABLE_PROMPT_EMBEDS_ERROR):
269 parse_chat_messages(
270 messages,
271 mc,
@@ -283,7 +284,7 @@ def test_parse_chat_messages_rejects_missing_data():
284 "content": [{"type": "prompt_embeds"}], # no `data`
285 }
286 ]
286 - with pytest.raises(ValueError, match=_PROMPT_EMBEDS_MISSING_DATA_ERROR):
287 + with pytest.raises(VLLMValidationError, match=_PROMPT_EMBEDS_MISSING_DATA_ERROR):
288 parse_chat_messages(
289 messages,
290 mc,
tests/renderers/test_completions.py
+4 -3
@@ -11,6 +11,7 @@ import pytest
11 import torch
12
13 from vllm.config import ModelConfig
14 +from vllm.exceptions import VLLMValidationError
15 from vllm.inputs import SingletonPrompt
16 from vllm.renderers import TokenizeParams
17 from vllm.renderers.hf import HfRenderer
@@ -286,7 +287,7 @@ class TestRenderPrompt:
287 )
288
289 with pytest.raises(
289 - ValueError,
290 + VLLMValidationError,
291 match="maximum context length is",
292 ):
293 renderer.tokenize_prompts(
@@ -307,7 +308,7 @@ class TestRenderPrompt:
308 )
309
310 with pytest.raises(
310 - ValueError,
311 + VLLMValidationError,
312 match="maximum context length is",
313 ):
314 renderer.tokenize_prompts(
@@ -328,7 +329,7 @@ class TestRenderPrompt:
329 )
330
331 with pytest.raises(
331 - ValueError,
332 + VLLMValidationError,
333 match="maximum context length is",
334 ):
335 renderer.tokenize_prompts(
tests/samplers/test_non_finite_params.py
+1 -1
@@ -42,7 +42,7 @@ class TestNonFiniteRepetitionPenalty:
42 ids=["nan", "inf", "-inf", "math.nan", "math.inf"],
43 )
44 def test_non_finite_repetition_penalty_rejected(self, value: float):
45 - with pytest.raises(ValueError, match="repetition_penalty"):
45 + with pytest.raises(VLLMValidationError, match="repetition_penalty"):
46 SamplingParams(repetition_penalty=value)
47
48 def test_finite_repetition_penalty_accepted(self):
tests/test_envs.py
+3 -2
@@ -15,6 +15,7 @@ from vllm.envs import (
15 env_with_choices,
16 environment_variables,
17 )
18 +from vllm.exceptions import VLLMValidationError
19
20
21 def test_getattr_without_cache(monkeypatch: pytest.MonkeyPatch):
@@ -547,7 +548,7 @@ class TestVllmMaxNSequences:
548 max_n = envs.VLLM_MAX_N_SEQUENCES
549 SamplingParams(n=max_n)
550
550 - with pytest.raises(ValueError, match="n must be at most"):
551 + with pytest.raises(VLLMValidationError, match="n must be at most"):
552 SamplingParams(n=max_n + 1)
553
554 def test_sampling_params_respects_custom_limit(
@@ -563,5 +564,5 @@ class TestVllmMaxNSequences:
564
565 SamplingParams(n=128)
566
566 - with pytest.raises(ValueError, match="n must be at most 128"):
567 + with pytest.raises(VLLMValidationError, match="n must be at most 128"):
568 SamplingParams(n=129)
tests/test_pooling_params.py
+17 -11
@@ -52,13 +52,19 @@ class MockModelConfig:
52 def test_removed_pooling_parameters(parameter: str, value: Any, message: str):
53 data = {"input": "hello", parameter: value}
54 for request_type in (EmbeddingRequest, ClassificationRequest, PoolingRequest):
55 - with pytest.raises(ValidationError, match=message) as exc_info:
55 + with pytest.raises(VLLMValidationError, match=message):
56 TypeAdapter(request_type).validate_python(data)
57 - assert len(exc_info.value.errors()) == 1
57
59 - with pytest.raises(ValidationError, match=message) as exc_info:
60 - TypeAdapter(PoolerConfig).validate_python({parameter: value})
61 - assert len(exc_info.value.errors()) == 1
58 + # PoolerConfig still raises bare ValueError for `normalize`
59 + # (wrapped to ValidationError by Pydantic), but `check_removed_pooling_task`
60 + # raises VLLMValidationError for removed tasks.
61 + if parameter == "normalize":
62 + with pytest.raises(ValidationError, match=message) as exc_info:
63 + TypeAdapter(PoolerConfig).validate_python({parameter: value})
64 + assert len(exc_info.value.errors()) == 1
65 + else:
66 + with pytest.raises(VLLMValidationError, match=message):
67 + TypeAdapter(PoolerConfig).validate_python({parameter: value})
68
69 if parameter == "task":
70 with pytest.raises(VLLMValidationError, match=message):
@@ -80,7 +86,7 @@ def test_embed():
86
87 invalid_parameters = classify_parameters + step_pooling_parameters
88 for p in set(invalid_parameters) - set(embed_parameters):
83 - with pytest.raises(ValueError):
89 + with pytest.raises(VLLMValidationError):
90 pooling_params = PoolingParams(task=task, **{p: True})
91 pooling_params.verify(model_config)
92
@@ -100,7 +106,7 @@ def test_embed_dimensions(model_info: EmbedModelInfo):
106 pooling_params = PoolingParams(task=task, dimensions=None)
107 pooling_params.verify(model_config)
108
103 - with pytest.raises(ValueError):
109 + with pytest.raises(VLLMValidationError):
110 pooling_params = PoolingParams(task=task, dimensions=1)
111 pooling_params.verify(model_config)
112
@@ -131,7 +137,7 @@ def test_embed_dimensions_matryoshka_without_list_upper_bound():
137
138 PoolingParams(task=task, dimensions=16).verify(model_config)
139
134 - with pytest.raises(ValueError):
140 + with pytest.raises(VLLMValidationError):
141 PoolingParams(task=task, dimensions=64).verify(model_config)
142
143
@@ -150,7 +156,7 @@ def test_classify(task):
156
157 invalid_parameters = embed_parameters + step_pooling_parameters
158 for p in set(invalid_parameters) - set(classify_parameters):
153 - with pytest.raises(ValueError):
159 + with pytest.raises(VLLMValidationError):
160 pooling_params = PoolingParams(task=task, **{p: True})
161 pooling_params.verify(model_config)
162
@@ -176,7 +182,7 @@ def test_token_embed(pooling_type: str):
182 invalid_parameters = classify_parameters + step_pooling_parameters
183
184 for p in set(invalid_parameters) - set(embed_parameters):
179 - with pytest.raises(ValueError):
185 + with pytest.raises(VLLMValidationError):
186 pooling_params = PoolingParams(task=task, **{p: True})
187 pooling_params.verify(model_config)
188
@@ -202,6 +208,6 @@ def test_token_classify(pooling_type: str):
208 invalid_parameters = embed_parameters + step_pooling_parameters
209
210 for p in set(invalid_parameters) - set(classify_parameters):
205 - with pytest.raises(ValueError):
211 + with pytest.raises(VLLMValidationError):
212 pooling_params = PoolingParams(task=task, **{p: True})
213 pooling_params.verify(model_config)
tests/test_sampling_params.py
+2 -1
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5 import pytest
6
7 from vllm import SamplingParams
8 +from vllm.exceptions import VLLMValidationError
9
10
11 @dataclass
@@ -32,7 +33,7 @@ class MockModelConfig:
33 )
34 def test_diffusion_rejects_unsupported_params(kwargs: dict):
35 params = SamplingParams(**kwargs)
35 - with pytest.raises(ValueError, match="not yet supported with diffusion"):
36 + with pytest.raises(VLLMValidationError, match="not yet supported with diffusion"):
37 params.verify(MockModelConfig(is_diffusion=True), None, None, None)
38
39
tests/tool_use/test_chat_completion_request_validations.py
+6 -5
@@ -4,6 +4,7 @@
4 import pytest
5
6 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
7 +from vllm.exceptions import VLLMValidationError
8
9
10 def test_chat_completion_request_with_no_tools():
@@ -27,7 +28,7 @@ def test_chat_completion_request_with_no_tools():
28 assert request.tool_choice == "none"
29
30 # tools key present but empty -- should be rejected
30 - with pytest.raises(ValueError, match="must not be an empty array"):
31 + with pytest.raises(VLLMValidationError, match="must not be an empty array"):
32 ChatCompletionRequest.model_validate(
33 {
34 "messages": [{"role": "user", "content": "Hello"}],
@@ -40,7 +41,7 @@ def test_chat_completion_request_with_no_tools():
41 @pytest.mark.parametrize("tool_choice", ["auto", "required"])
42 def test_chat_completion_request_with_tool_choice_but_no_tools(tool_choice):
43 with pytest.raises(
43 - ValueError, match="When using `tool_choice`, `tools` must be set."
44 + VLLMValidationError, match="When using `tool_choice`, `tools` must be set."
45 ):
46 ChatCompletionRequest.model_validate(
47 {
@@ -51,7 +52,7 @@ def test_chat_completion_request_with_tool_choice_but_no_tools(tool_choice):
52 )
53
54 with pytest.raises(
54 - ValueError, match="When using `tool_choice`, `tools` must be set."
55 + VLLMValidationError, match="When using `tool_choice`, `tools` must be set."
56 ):
57 ChatCompletionRequest.model_validate(
58 {
@@ -134,7 +135,7 @@ SAMPLE_TOOL = {
135 def test_structured_outputs_with_named_tool_choice_rejected():
136 """structured_outputs cannot be combined with a named tool_choice."""
137 with pytest.raises(
137 - ValueError,
138 + VLLMValidationError,
139 match="structured outputs or tools, not both",
140 ):
141 ChatCompletionRequest.model_validate(
@@ -168,7 +169,7 @@ def test_structured_outputs_with_auto_tool_choice_allowed():
169 def test_multiple_structured_outputs_rejected():
170 """Only one kind of structured output constraint is allowed."""
171 with pytest.raises(
171 - ValueError,
172 + VLLMValidationError,
173 match="You can only use one kind of constraints",
174 ):
175 ChatCompletionRequest.model_validate(
tests/tool_use/test_responses_request_validations.py
+6 -6
@@ -2,12 +2,12 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import pytest
5 -from pydantic import ValidationError
5
6 from vllm.entrypoints.openai.responses.protocol import (
7 ResponsesRequest,
8 ResponsesResponse,
9 )
10 +from vllm.exceptions import VLLMValidationError
11
12 SAMPLE_TOOL = {
13 "type": "function",
@@ -58,13 +58,13 @@ def test_responses_request_required_without_tools(tools):
58 if tools is not None:
59 kwargs["tools"] = tools
60 with pytest.raises(
61 - ValidationError, match="Tool choice 'required' must be specified"
61 + VLLMValidationError, match="Tool choice 'required' must be specified"
62 ):
63 ResponsesRequest.model_validate(kwargs)
64
65
66 def test_responses_request_named_tool_choice_without_tools():
67 - with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
67 + with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
68 ResponsesRequest.model_validate(
69 {
70 "input": "Hello",
@@ -107,7 +107,7 @@ def test_responses_request_named_tool_choice_matching():
107
108
109 def test_responses_request_named_tool_choice_not_matching():
110 - with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
110 + with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
111 ResponsesRequest.model_validate(
112 {
113 "input": "Hello",
@@ -164,7 +164,7 @@ def test_responses_request_empty_tools_tool_choice_auto():
164 ],
165 )
166 def test_responses_request_named_tool_choice_missing_name(tool_choice):
167 - with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
167 + with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
168 ResponsesRequest.model_validate(
169 {
170 "input": "Hello",
@@ -176,7 +176,7 @@ def test_responses_request_named_tool_choice_missing_name(tool_choice):
176
177
178 def test_responses_request_empty_tools_named_tool_choice():
179 - with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
179 + with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
180 ResponsesRequest.model_validate(
181 {
182 "input": "Hello",
tests/v1/e2e/general/test_context_length.py
+1 -1
@@ -59,7 +59,7 @@ def test_decoder_max_context_length_validation(
59 "Make sure that `max_model_len` is no smaller than the number of "
60 "text tokens (prompt + requested output tokens)."
61 )
62 - with pytest.raises(ValueError) as excinfo:
62 + with pytest.raises(VLLMValidationError) as excinfo:
63 vllm_model.generate_greedy(prompt_ids, max_tokens)
64 assert expected_msg in str(excinfo.value)
65
tests/v1/e2e/general/test_min_tokens.py
+3 -2
@@ -16,6 +16,7 @@ Covers:
16 import pytest
17
18 from vllm import LLM, SamplingParams
19 +from vllm.exceptions import VLLMValidationError
20 from vllm.outputs import RequestOutput
21
22 # Test configuration
@@ -479,13 +480,13 @@ def test_min_tokens_validation():
480
481 # Invalid cases
482 with pytest.raises(
482 - ValueError,
483 + VLLMValidationError,
484 match="min_tokens must be greater than or equal to 0",
485 ):
486 SamplingParams(min_tokens=-1, max_tokens=10)
487
488 with pytest.raises(
488 - ValueError,
489 + VLLMValidationError,
490 match="min_tokens must be less than or equal to max_tokens",
491 ):
492 SamplingParams(min_tokens=15, max_tokens=10)
tests/v1/e2e/general/test_streaming_input.py
+10 -3
@@ -20,6 +20,7 @@ import pytest_asyncio
20
21 from vllm import SamplingParams
22 from vllm.engine.protocol import StreamingInput
23 +from vllm.exceptions import VLLMValidationError
24 from vllm.outputs import RequestOutput
25 from vllm.platforms import current_platform
26 from vllm.sampling_params import RequestOutputKind
@@ -571,13 +572,17 @@ async def test_streaming_input_validation_errors(engine: AsyncLLM):
572 yield StreamingInput(prompt="test")
573
574 # Test n > 1 is rejected
574 - with pytest.raises(ValueError, match="Input streaming not currently supported"):
575 + with pytest.raises(
576 + VLLMValidationError, match="Input streaming not currently supported"
577 + ):
578 params_n2 = SamplingParams(max_tokens=10, n=2)
579 async for _ in engine.generate(dummy_generator(), params_n2, "test_n2"):
580 pass
581
582 # Test FINAL_ONLY is rejected
580 - with pytest.raises(ValueError, match="Input streaming not currently supported"):
583 + with pytest.raises(
584 + VLLMValidationError, match="Input streaming not currently supported"
585 + ):
586 params_final = SamplingParams(
587 max_tokens=10, output_kind=RequestOutputKind.FINAL_ONLY
588 )
@@ -585,7 +590,9 @@ async def test_streaming_input_validation_errors(engine: AsyncLLM):
590 pass
591
592 # Test stop strings are rejected
588 - with pytest.raises(ValueError, match="Input streaming not currently supported"):
593 + with pytest.raises(
594 + VLLMValidationError, match="Input streaming not currently supported"
595 + ):
596 params_stop = SamplingParams(max_tokens=10, stop=["stop"])
597 async for _ in engine.generate(dummy_generator(), params_stop, "test_stop"):
598 pass
tests/v1/engine/test_async_llm.py
+4 -3
@@ -19,6 +19,7 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
19 from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
20 from vllm.entrypoints.openai.models.protocol import BaseModelPath
21 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
22 +from vllm.exceptions import VLLMValidationError
23 from vllm.inputs import PromptType
24 from vllm.outputs import RequestOutput
25 from vllm.platforms import current_platform
@@ -485,7 +486,7 @@ async def test_dp_rank_argument():
486 pass
487
488 # Test with out-of-range DP rank.
488 - with pytest.raises(ValueError):
489 + with pytest.raises(VLLMValidationError):
490 async for _ in engine.generate(
491 request_id="request-35",
492 prompt=TEXT_PROMPT,
@@ -554,8 +555,8 @@ async def test_header_dp_rank_argument():
555 # Test 2: Out-of-range DP rank (1)
556 mock_raw_request.headers = {"X-data-parallel-rank": "1"}
557
557 - # should raise ValueError for out-of-range rank
558 - with pytest.raises(ValueError):
558 + # should raise VLLMValidationError for out-of-range rank
559 + with pytest.raises(VLLMValidationError):
560 await serving_chat.create_chat_completion(req, mock_raw_request)
561
562
tests/v1/logits_processors/utils.py
+2 -1
@@ -11,6 +11,7 @@ import torch
11
12 from tests.utils import requires_spawn_multiprocessing
13 from vllm.config import VllmConfig
14 +from vllm.exceptions import VLLMValidationError
15 from vllm.logger import init_logger
16 from vllm.sampling_params import SamplingParams
17 from vllm.v1.sample.logits_processor import (
@@ -61,7 +62,7 @@ class DummyLogitsProcessor(LogitsProcessor):
62 "target_token"
63 )
64 if target_token is not None and not isinstance(target_token, int):
64 - raise ValueError(
65 + raise VLLMValidationError(
66 f"target_token value {target_token} {type(target_token)} is not int"
67 )
68
tests/v1/sample/test_logprobs.py
+1 -1
@@ -405,7 +405,7 @@ def test_max_logprobs():
405 runner.generate(["Hello world"], sampling_params=vllm_sampling_params)
406
407 bad_sampling_params = SamplingParams(logprobs=2)
408 - with pytest.raises(ValueError):
408 + with pytest.raises(VLLMValidationError):
409 runner.generate(["Hello world"], sampling_params=bad_sampling_params)
410
411
tests/v1/sample/test_sampling_params_e2e.py
+4 -3
@@ -4,6 +4,7 @@
4 import pytest
5
6 from vllm import LLM, SamplingParams
7 +from vllm.exceptions import VLLMValidationError
8
9 MODEL = "hmellor/tiny-random-LlamaForCausalLM"
10 PROMPT = "Hello my name is Robert and I"
@@ -161,15 +162,15 @@ def test_allowed_token_ids(llm):
162 assert output[0].outputs[0].token_ids[-1] == token_id
163
164 # Reject empty allowed_token_ids.
164 - with pytest.raises(ValueError):
165 + with pytest.raises(VLLMValidationError):
166 _ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[]))
167
168 # Reject negative token id.
168 - with pytest.raises(ValueError):
169 + with pytest.raises(VLLMValidationError):
170 _ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[-1]))
171
172 # Reject out of vocabulary.
172 - with pytest.raises(ValueError):
173 + with pytest.raises(VLLMValidationError):
174 _ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[10000000]))
175
176
tests/v1/structured_output/test_validation.py
+3 -2
@@ -5,6 +5,7 @@
5 import pytest
6
7 from vllm.config import StructuredOutputsConfig
8 +from vllm.exceptions import VLLMValidationError
9 from vllm.sampling_params import SamplingParams, StructuredOutputsParams
10
11 pytestmark = pytest.mark.cpu_test
@@ -32,7 +33,7 @@ def test_structured_outputs_rejected_for_diffusion_models():
33 params = SamplingParams(
34 structured_outputs=StructuredOutputsParams(json=JSON_SCHEMA)
35 )
35 - with pytest.raises(ValueError, match="not yet supported for diffusion"):
36 + with pytest.raises(VLLMValidationError, match="not yet supported for diffusion"):
37 params._validate_structured_outputs(
38 _StubModelConfig(is_diffusion=True),
39 StructuredOutputsConfig(),
@@ -63,7 +64,7 @@ def test_degenerate_structured_outputs_rejected(structured_outputs, match):
64 rejected at request validation (-> 400) instead of reaching and crashing
65 the engine."""
66 params = SamplingParams(structured_outputs=structured_outputs)
66 - with pytest.raises(ValueError, match=match):
67 + with pytest.raises(VLLMValidationError, match=match):
68 params._validate_structured_outputs(
69 _StubModelConfig(is_diffusion=False),
70 StructuredOutputsConfig(),
vllm/entrypoints/openai/api_server.py
+16 -20
@@ -28,7 +28,6 @@ from vllm.engine.protocol import EngineClient
28 from vllm.entrypoints.chat_utils import load_chat_template
29 from vllm.entrypoints.launcher import serve_http
30 from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
31 -from vllm.entrypoints.openai.engine.protocol import GenerationError
31 from vllm.entrypoints.openai.models.protocol import BaseModelPath
32 from vllm.entrypoints.openai.models.serving import OpenAIServingModels
33 from vllm.entrypoints.serve.elastic_ep.middleware import ScalingMiddleware
@@ -42,20 +41,15 @@ from vllm.entrypoints.serve.utils.api_utils import (
41 )
42 from vllm.entrypoints.serve.utils.request_logger import RequestLogger
43 from vllm.entrypoints.serve.utils.server_utils import (
45 - engine_error_handler,
44 exception_handler,
47 - generation_error_handler,
45 get_uvicorn_log_config,
46 http_exception_handler,
47 lifespan,
48 log_response,
49 validation_exception_handler,
50 + vllm_error_handler,
51 )
54 -from vllm.exceptions import (
55 - VLLMNotFoundError,
56 - VLLMUnprocessableEntityError,
57 - VLLMValidationError,
58 -)
52 +from vllm.exceptions import VLLMError
53 from vllm.logger import init_logger
54 from vllm.reasoning import ReasoningParserManager
55 from vllm.renderers.online_derenderer import OnlineDerenderer
@@ -67,7 +61,6 @@ from vllm.usage.usage_lib import UsageContext
61 from vllm.utils.argparse_utils import FlexibleArgumentParser
62 from vllm.utils.network_utils import is_valid_ipv6_address
63 from vllm.utils.system_utils import decorate_logs, set_ulimit
70 -from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
64 from vllm.version import __version__ as VLLM_VERSION
65
66 prometheus_multiproc_dir: tempfile.TemporaryDirectory
@@ -291,23 +284,26 @@ def build_app(
284 allow_headers=args.allowed_headers,
285 )
286
287 + # Exception handlers are registered in four layers:
288 + # 1. framework errors raised by FastAPI/Starlette
289 + # 2. vLLM-specific errors dispatched via a single ``VLLMError`` handler
290 + # 3. fallback handlers for raw exceptions not yet migrated to ``VLLMError``
291 + # 4. the raw ``Exception`` handler as a safety net
292 + # Registering specific exception types (rather than only ``Exception``)
293 + # ensures they are handled by ``ExceptionMiddleware`` (inside the Prometheus
294 + # middleware) rather than ``ServerErrorMiddleware`` (outside it), so their
295 + # status codes are recorded correctly.
296 app.exception_handler(HTTPException)(http_exception_handler)
297 app.exception_handler(RequestValidationError)(validation_exception_handler)
296 - app.exception_handler(EngineGenerateError)(engine_error_handler)
297 - app.exception_handler(EngineDeadError)(engine_error_handler)
298 - app.exception_handler(GenerationError)(generation_error_handler)
299 - # Register specific exception types so they are handled by
300 - # ExceptionMiddleware (inside the Prometheus middleware) rather than
301 - # ServerErrorMiddleware (outside it). Without this, these exceptions
302 - # propagate through Prometheus as unhandled and get recorded as 5xx
303 - # even though they result in 4xx responses to the client.
304 - app.exception_handler(VLLMValidationError)(exception_handler)
305 - app.exception_handler(VLLMUnprocessableEntityError)(exception_handler)
306 - app.exception_handler(VLLMNotFoundError)(exception_handler)
298 +
299 + app.exception_handler(VLLMError)(vllm_error_handler)
300 +
301 + # TODO(zqzten): remove these fallback handlers after migration to VLLMError
302 app.exception_handler(ValueError)(exception_handler)
303 app.exception_handler(TypeError)(exception_handler)
304 app.exception_handler(OverflowError)(exception_handler)
305 app.exception_handler(NotImplementedError)(exception_handler)
306 +
307 app.exception_handler(Exception)(exception_handler)
308
309 # Ensure --api-key option from CLI takes precedence over VLLM_API_KEY
vllm/entrypoints/openai/engine/protocol.py
+2 -2
@@ -19,7 +19,7 @@ from pydantic import (
19
20 from vllm.config.utils import replace
21 from vllm.entrypoints.chat_utils import make_tool_call_id
22 -from vllm.exceptions import VLLMValidationError
22 +from vllm.exceptions import VLLMServerError, VLLMValidationError
23 from vllm.logger import init_logger
24 from vllm.sampling_params import StructuredOutputsParams
25 from vllm.utils import random_uuid
@@ -404,7 +404,7 @@ class DeltaMessage(OpenAIBaseModel):
404 return data
405
406
407 -class GenerationError(Exception):
407 +class GenerationError(VLLMServerError):
408 """raised when finish_reason indicates internal server error (500)"""
409
410 def __init__(self, message: str = "Internal server error"):
vllm/entrypoints/serve/utils/error_response.py
+18 -5
@@ -28,7 +28,9 @@ def create_error_response(
28 )
29
30 from vllm.exceptions import (
31 + VLLMClientError,
32 VLLMNotFoundError,
33 + VLLMServerError,
34 VLLMUnprocessableEntityError,
35 VLLMValidationError,
36 )
@@ -45,8 +47,23 @@ def create_error_response(
47 err_type = "NotFoundError"
48 status_code = HTTPStatus.NOT_FOUND
49 param = None
50 + elif isinstance(exc, VLLMClientError):
51 + # Any other client-caused error defaults to 400.
52 + err_type = "BadRequestError"
53 + status_code = HTTPStatus.BAD_REQUEST
54 + param = None
55 + elif isinstance(exc, GenerationError):
56 + err_type = "InternalServerError"
57 + status_code = exc.status_code
58 + param = None
59 + elif isinstance(exc, VLLMServerError):
60 + # Any other server-caused error defaults to 500.
61 + err_type = "InternalServerError"
62 + status_code = HTTPStatus.INTERNAL_SERVER_ERROR
63 + param = None
64 + # Fallback for raw exceptions not yet migrated to VLLMError.
65 + # TODO(zqzten): remove these fallback handlers after migration to VLLMError
66 elif isinstance(exc, (ValueError, TypeError, OverflowError)):
49 - # Common validation errors from user input
67 err_type = "BadRequestError"
68 status_code = HTTPStatus.BAD_REQUEST
69 param = None
@@ -54,10 +71,6 @@ def create_error_response(
71 err_type = "NotImplementedError"
72 status_code = HTTPStatus.NOT_IMPLEMENTED
73 param = None
57 - elif isinstance(exc, GenerationError):
58 - err_type = "InternalServerError"
59 - status_code = exc.status_code
60 - param = None
74 elif any(cls.__name__ == "TemplateError" for cls in type(exc).__mro__):
75 # jinja2.TemplateError and its subclasses (avoid importing jinja2)
76 err_type = "BadRequestError"
vllm/entrypoints/serve/utils/server_utils.py
+11 -1
@@ -31,7 +31,7 @@ from vllm.entrypoints.serve.utils.error_response import (
31 create_error_response,
32 sanitize_message,
33 )
34 -from vllm.exceptions import VLLMValidationError
34 +from vllm.exceptions import VLLMError, VLLMValidationError
35 from vllm.logger import init_logger
36 from vllm.utils.gc_utils import freeze_gc_heap
37 from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
@@ -325,6 +325,16 @@ async def log_response(request: Request, call_next):
325 return response
326
327
328 +async def vllm_error_handler(req: Request, exc: VLLMError):
329 + """Dispatch a vLLM-specific error to the appropriate handler."""
330 + if isinstance(exc, (EngineGenerateError, EngineDeadError)):
331 + return await engine_error_handler(req, exc)
332 + elif isinstance(exc, GenerationError):
333 + return await generation_error_handler(req, exc)
334 + else:
335 + return await exception_handler(req, exc)
336 +
337 +
338 async def engine_error_handler(
339 req: Request, exc: EngineDeadError | EngineGenerateError
340 ):
vllm/exceptions.py
+21 -3
@@ -6,7 +6,25 @@
6 from typing import Any
7
8
9 -class VLLMValidationError(ValueError):
9 +class VLLMError(Exception):
10 + """Base class for all vLLM-specific errors.
11 +
12 + Subclasses are split into `VLLMClientError` (caused by the request, mapped
13 + to 4xx) and `VLLMServerError` (caused by the server, mapped to 5xx).
14 + Dispatching on this hierarchy lets the entrypoints decide the HTTP status
15 + without relying on raw Python exception types such as `ValueError`.
16 + """
17 +
18 +
19 +class VLLMClientError(VLLMError):
20 + """Base class for errors caused by the client request (4xx)."""
21 +
22 +
23 +class VLLMServerError(VLLMError):
24 + """Base class for errors caused by the server (5xx)."""
25 +
26 +
27 +class VLLMValidationError(VLLMClientError):
28 """vLLM-specific validation error for request validation failures.
29
30 Args:
@@ -36,7 +54,7 @@ class VLLMValidationError(ValueError):
54 return f"{base} ({', '.join(extras)})" if extras else base
55
56
39 -class VLLMNotFoundError(Exception):
57 +class VLLMNotFoundError(VLLMClientError):
58 """vLLM-specific NotFoundError"""
59
60 pass
@@ -66,7 +84,7 @@ class LoRAAdapterNotFoundError(VLLMNotFoundError):
84 return self.message
85
86
69 -class VLLMUnprocessableEntityError(ValueError):
87 +class VLLMUnprocessableEntityError(VLLMClientError):
88 """vLLM-specific error for unprocessable entity requests.
89
90 This exception is raised when the request content is invalid or cannot be
vllm/inputs/engine.py
+4 -2
@@ -7,6 +7,8 @@ from typing import TYPE_CHECKING, Literal, TypeAlias
7
8 from typing_extensions import NotRequired, TypedDict, assert_never
9
10 +from vllm.exceptions import VLLMValidationError
11 +
12 if TYPE_CHECKING:
13 import torch
14
@@ -284,7 +286,7 @@ which can be passed to `LLMEngine.add_request` or `AsyncLLM.add_request`.
286
287 def _validate_enc_input(enc_input: SingletonInput) -> EncoderInput:
288 if enc_input["type"] == "embeds":
287 - raise ValueError(
289 + raise VLLMValidationError(
290 "Embedding inputs are not supported for encoder-decoder models"
291 )
292
@@ -302,7 +304,7 @@ def _validate_enc_input(enc_input: SingletonInput) -> EncoderInput:
304
305 def _validate_dec_input(dec_input: SingletonInput) -> DecoderEngineInput:
306 if dec_input["type"] == "embeds":
305 - raise ValueError(
307 + raise VLLMValidationError(
308 "Embedding inputs are not supported for encoder-decoder models"
309 )
310
vllm/pooling_params.py
+7 -6
@@ -7,6 +7,7 @@ from typing import Any
7 import msgspec
8
9 from vllm.config import ModelConfig, PoolerConfig
10 +from vllm.exceptions import VLLMValidationError
11 from vllm.logger import init_logger
12 from vllm.sampling_params import RequestOutputKind
13 from vllm.tasks import PoolingTask, check_removed_pooling_task
@@ -145,7 +146,7 @@ class PoolingParams(
146 invalid_parameters.append(k)
147
148 if invalid_parameters:
148 - raise ValueError(
149 + raise VLLMValidationError(
150 f"Task {self.task} only supports {valid_parameters} "
151 f"parameters, does not support "
152 f"{invalid_parameters} parameters"
@@ -170,21 +171,21 @@ class PoolingParams(
171 valid_range = f"[1, {embedding_size}]"
172 dimensions_in_range = 1 <= dimensions <= embedding_size
173 if not model_config.is_matryoshka:
173 - raise ValueError(
174 + raise VLLMValidationError(
175 f"Model {model_name!r} does not support Matryoshka "
176 f"embeddings; dimensions must be unset "
177 f"(received dimensions={dimensions})."
178 )
179
180 if not dimensions_in_range:
180 - raise ValueError(
181 + raise VLLMValidationError(
182 f"Model {model_name!r} only supports dimensions in "
183 f"range {valid_range}, got {dimensions}."
184 )
185
186 mds = model_config.matryoshka_dimensions
187 if mds is not None and dimensions not in mds:
187 - raise ValueError(
188 + raise VLLMValidationError(
189 f"Model {model_name!r} only supports Matryoshka "
190 f"dimensions {str(mds)}, got {dimensions}."
191 )
@@ -208,7 +209,7 @@ class PoolingParams(
209 invalid_parameters.append(k)
210
211 if invalid_parameters:
211 - raise ValueError(
212 + raise VLLMValidationError(
213 f"Task {self.task!r} only supports {valid_parameters} "
214 f"parameters, does not support "
215 f"{invalid_parameters} parameters"
@@ -231,7 +232,7 @@ class PoolingParams(
232 def __post_init__(self) -> None:
233 check_removed_pooling_task(self.task)
234 if self.output_kind != RequestOutputKind.FINAL_ONLY:
234 - raise ValueError(
235 + raise VLLMValidationError(
236 "For pooling output_kind has to be FINAL_ONLY, "
237 f"got {self.output_kind!r}"
238 )
vllm/sampling_params.py
+40 -32
@@ -100,12 +100,12 @@ class StructuredOutputsParams:
100 ]
101 )
102 if count > 1:
103 - raise ValueError(
103 + raise VLLMValidationError(
104 "You can only use one kind of structured outputs constraint "
105 f"but multiple are specified: {self.__dict__}"
106 )
107 if count < 1:
108 - raise ValueError(
108 + raise VLLMValidationError(
109 "You must use one kind of structured outputs constraint "
110 f"but none are specified: {self.__dict__}"
111 )
@@ -166,13 +166,13 @@ class RepetitionDetectionParams:
166 or self.min_pattern_size < 0
167 or self.min_pattern_size > self.max_pattern_size
168 ):
169 - raise ValueError(
169 + raise VLLMValidationError(
170 "max_pattern_size, min_pattern_size must be >=0, "
171 "with min_pattern_size <= max_pattern_size. "
172 "Set both to 0 to disable repetitive pattern detection."
173 )
174 if self.max_pattern_size > 0 and self.min_count < 2:
175 - raise ValueError(
175 + raise VLLMValidationError(
176 "min_count must be >= 2 to detect repetitive patterns "
177 "in engine output. If you do not wish to detect repetitive "
178 "patterns, set max_pattern_size to 0."
@@ -514,31 +514,33 @@ class SamplingParams(
514
515 def _verify_args(self) -> None:
516 if not isinstance(self.n, int):
517 - raise ValueError(f"n must be an int, but is of type {type(self.n)}")
517 + raise VLLMValidationError(
518 + f"n must be an int, but is of type {type(self.n)}"
519 + )
520 if self.n < 1:
519 - raise ValueError(f"n must be at least 1, got {self.n}.")
521 + raise VLLMValidationError(f"n must be at least 1, got {self.n}.")
522 max_n = envs.VLLM_MAX_N_SEQUENCES
523 if self.n > max_n:
522 - raise ValueError(
524 + raise VLLMValidationError(
525 f"n must be at most {max_n}, got {self.n}. "
526 "To increase this limit, set the VLLM_MAX_N_SEQUENCES "
527 "environment variable."
528 )
529 if not -2.0 <= self.presence_penalty <= 2.0:
528 - raise ValueError(
530 + raise VLLMValidationError(
531 f"presence_penalty must be in [-2, 2], got {self.presence_penalty}."
532 )
533 if not -2.0 <= self.frequency_penalty <= 2.0:
532 - raise ValueError(
534 + raise VLLMValidationError(
535 f"frequency_penalty must be in [-2, 2], got {self.frequency_penalty}."
536 )
537 if not math.isfinite(self.repetition_penalty):
536 - raise ValueError(
538 + raise VLLMValidationError(
539 "repetition_penalty must be a finite number, "
540 f"got {self.repetition_penalty}."
541 )
542 if self.repetition_penalty <= 0.0:
541 - raise ValueError(
543 + raise VLLMValidationError(
544 "repetition_penalty must be greater than zero, got "
545 f"{self.repetition_penalty}."
546 )
@@ -568,15 +570,15 @@ class SamplingParams(
570 )
571 # quietly accept -1 as disabled, but prefer 0
572 if self.top_k < -1:
571 - raise ValueError(
573 + raise VLLMValidationError(
574 f"top_k must be 0 (disable), or at least 1, got {self.top_k}."
575 )
576 if not isinstance(self.top_k, int):
575 - raise TypeError(
577 + raise VLLMValidationError(
578 f"top_k must be an integer, got {type(self.top_k).__name__}"
579 )
580 if not 0.0 <= self.min_p <= 1.0:
579 - raise ValueError(f"min_p must be in [0, 1], got {self.min_p}.")
581 + raise VLLMValidationError(f"min_p must be in [0, 1], got {self.min_p}.")
582 if self.max_tokens is not None and self.max_tokens < 1:
583 raise VLLMValidationError(
584 f"max_tokens must be at least 1, got {self.max_tokens}.",
@@ -584,11 +586,11 @@ class SamplingParams(
586 value=self.max_tokens,
587 )
588 if self.min_tokens < 0:
587 - raise ValueError(
589 + raise VLLMValidationError(
590 f"min_tokens must be greater than or equal to 0, got {self.min_tokens}."
591 )
592 if self.max_tokens is not None and self.min_tokens > self.max_tokens:
591 - raise ValueError(
593 + raise VLLMValidationError(
594 f"min_tokens must be less than or equal to "
595 f"max_tokens={self.max_tokens}, got {self.min_tokens}."
596 )
@@ -617,27 +619,29 @@ class SamplingParams(
619 )
620 assert isinstance(self.stop_token_ids, list)
621 if not all(isinstance(st_id, int) for st_id in self.stop_token_ids):
620 - raise ValueError(
622 + raise VLLMValidationError(
623 f"stop_token_ids must contain only integers, got {self.stop_token_ids}."
624 )
625 assert isinstance(self.stop, list)
626 if any(not stop_str for stop_str in self.stop):
625 - raise ValueError("stop cannot contain an empty string.")
627 + raise VLLMValidationError("stop cannot contain an empty string.")
628 if self.stop and not self.detokenize:
627 - raise ValueError(
629 + raise VLLMValidationError(
630 "stop strings are only supported when detokenize is True. "
631 "Set detokenize=True to use stop."
632 )
633 assert isinstance(self.bad_words, list)
634 if any(not bad_word for bad_word in self.bad_words):
633 - raise ValueError(
635 + raise VLLMValidationError(
636 f"bad_words cannot contain an empty string. "
637 f"Got bad_words={self.bad_words}"
638 )
639
640 def _verify_greedy_sampling(self) -> None:
641 if self.n > 1:
640 - raise ValueError(f"n must be 1 when using greedy sampling, got {self.n}.")
642 + raise VLLMValidationError(
643 + f"n must be 1 when using greedy sampling, got {self.n}."
644 + )
645
646 def update_from_generation_config(
647 self,
@@ -889,7 +893,7 @@ class SamplingParams(
893
894 # Some sampling parameters are not yet compatible with spec decoding.
895 if self.min_p > _SAMPLING_EPS or self.logit_bias:
892 - raise ValueError(
896 + raise VLLMValidationError(
897 "The min_p and logit_bias sampling parameters "
898 "are not yet supported with speculative decoding."
899 )
@@ -910,7 +914,7 @@ class SamplingParams(
914 or self.bad_words
915 or self.allowed_token_ids
916 ):
913 - raise ValueError(
917 + raise VLLMValidationError(
918 "The temperature, min_p, seed, min_tokens, logit_bias, "
919 "bad_words, and allowed_token_ids sampling parameters "
920 "are not yet supported with diffusion models."
@@ -930,7 +934,7 @@ class SamplingParams(
934 # rather than sampling left-to-right, which the grammar FSM
935 # requires. Without this check, requests fail mid-generation
936 # with an FSM rejection (HTTP 500). See issue #45436.
933 - raise ValueError(
937 + raise VLLMValidationError(
938 "Structured outputs are not yet supported for diffusion "
939 "language models. Remove the structured output constraint "
940 "(e.g. `response_format`, `structured_outputs`) from the "
@@ -938,7 +942,7 @@ class SamplingParams(
942 )
943
944 if tokenizer is None:
941 - raise ValueError(
945 + raise VLLMValidationError(
946 "Structured outputs requires a tokenizer so it can't be used with 'skip_tokenizer_init'" # noqa: E501
947 )
948
@@ -952,7 +956,7 @@ class SamplingParams(
956 if backend != _backend and not (
957 backend == "auto" and self.structured_outputs._backend_was_auto
958 ):
955 - raise ValueError(
959 + raise VLLMValidationError(
960 "Request-level structured output backend selection is not "
961 f"supported. The request specified '{_backend}', but vLLM "
962 f"was initialised with '{backend}'. This error can be "
@@ -967,7 +971,7 @@ class SamplingParams(
971 and not self.structured_outputs.choice
972 ):
973 # It is invalid for choice to be an empty list
970 - raise ValueError(
974 + raise VLLMValidationError(
975 f"Choice '{self.structured_outputs.choice}' cannot be an empty list" # noqa: E501
976 )
977 # Reject empty string grammar early to avoid engine-side crashes
@@ -975,16 +979,20 @@ class SamplingParams(
979 isinstance(self.structured_outputs.grammar, str)
980 and self.structured_outputs.grammar.strip() == ""
981 ):
978 - raise ValueError("structured_outputs.grammar cannot be an empty string")
982 + raise VLLMValidationError(
983 + "structured_outputs.grammar cannot be an empty string"
984 + )
985 # Reject empty string json schema early to avoid engine-side crashes
986 if (
987 isinstance(self.structured_outputs.json, str)
988 and self.structured_outputs.json.strip() == ""
989 ):
984 - raise ValueError("structured_outputs.json cannot be an empty string")
990 + raise VLLMValidationError(
991 + "structured_outputs.json cannot be an empty string"
992 + )
993 # Reject json_object=False early to avoid engine-side crashes
994 if self.structured_outputs.json_object is False:
987 - raise ValueError(
995 + raise VLLMValidationError(
996 "structured_outputs.json_object must be True if set; omit "
997 "structured_outputs to disable structured outputs"
998 )
@@ -1006,7 +1014,7 @@ class SamplingParams(
1014 validate_xgrammar_grammar(self)
1015 elif backend.startswith("guidance"):
1016 if _is_non_tekken_mistral(tokenizer=tokenizer):
1009 - raise ValueError(
1017 + raise VLLMValidationError(
1018 "Non-tekken Mistral tokenizers are not supported for the 'guidance'"
1019 " structured output backend. Please either use a more recent "
1020 "Mistral model, the ['xgrammar', 'outlines'] "
@@ -1026,7 +1034,7 @@ class SamplingParams(
1034 elif backend == "lm-format-enforcer":
1035 # lm format enforcer backend
1036 if is_mistral_tokenizer(tokenizer):
1029 - raise ValueError(
1037 + raise VLLMValidationError(
1038 "Mistral tokenizer is not supported for the 'lm-format-enforcer' "
1039 "structured output backend. Please use ['xgrammar', 'outlines'] "
1040 "backends or tokenizer_mode='hf' instead."
vllm/v1/engine/async_llm.py
+6 -5
@@ -21,6 +21,7 @@ from vllm.distributed.weight_transfer.base import (
21 from vllm.engine.arg_utils import AsyncEngineArgs
22 from vllm.engine.protocol import EngineClient, StreamingInput
23 from vllm.entrypoints.serve.elastic_ep.middleware import set_scaling_elastic_ep
24 +from vllm.exceptions import VLLMClientError, VLLMValidationError
25 from vllm.inputs import EngineInput, PromptType
26 from vllm.logger import init_logger
27 from vllm.lora.request import LoRARequest
@@ -309,7 +310,7 @@ class AsyncLLM(EngineClient):
310 and not is_pooling
311 and params.prompt_logprobs
312 ):
312 - raise ValueError(
313 + raise VLLMValidationError(
314 "--kv-sharing-fast-prefill produces incorrect logprobs for "
315 "prompt tokens, please disable it when the requests need "
316 "prompt logprobs"
@@ -476,7 +477,7 @@ class AsyncLLM(EngineClient):
477 )
478 req.external_req_id = request_id
479 if req.prompt_embeds is not None:
479 - raise ValueError(
480 + raise VLLMValidationError(
481 "prompt_embeds not supported for streaming inputs"
482 )
483 prompt_text, _, _ = extract_prompt_components(
@@ -512,7 +513,7 @@ class AsyncLLM(EngineClient):
513 or params.output_kind == RequestOutputKind.FINAL_ONLY
514 or params.stop
515 ):
515 - raise ValueError(
516 + raise VLLMValidationError(
517 "Input streaming not currently supported "
518 "for pooling models, n > 1, request_kind = FINAL_ONLY "
519 "or with stop strings."
@@ -604,7 +605,7 @@ class AsyncLLM(EngineClient):
605 raise
606
607 # Request validation error.
607 - except ValueError as e:
608 + except VLLMClientError as e:
609 if self.log_requests:
610 logger.info("Request %s failed (bad request): %s.", request_id, e)
611 raise
@@ -869,7 +870,7 @@ class AsyncLLM(EngineClient):
870 raise
871
872 # Request validation error.
872 - except ValueError:
873 + except VLLMClientError:
874 if self.log_requests:
875 logger.info("Request %s failed (bad request).", request_id)
876 raise
vllm/v1/engine/exceptions.py
+5 -2
@@ -1,12 +1,15 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 -class EngineGenerateError(Exception):
3 +from vllm.exceptions import VLLMServerError
4 +
5 +
6 +class EngineGenerateError(VLLMServerError):
7 """Raised when a AsyncLLM.generate() fails. Recoverable."""
8
9 pass
10
11
9 -class EngineDeadError(Exception):
12 +class EngineDeadError(VLLMServerError):
13 """Raised when the EngineCore dies. Unrecoverable."""
14
15 def __init__(self, *args, suppress_context: bool = False, **kwargs):
vllm/v1/engine/input_processor.py
+15 -12
@@ -7,6 +7,7 @@ from typing import Any, Literal
7
8 import vllm.envs as envs
9 from vllm.config import VllmConfig
10 +from vllm.exceptions import VLLMValidationError
11 from vllm.inputs import (
12 EngineInput,
13 PromptType,
@@ -90,7 +91,7 @@ class InputProcessor:
91 task for task in supported_tasks if task in GENERATION_TASKS
92 ]
93 if not supported_generation_tasks:
93 - raise ValueError("This model does not support generation")
94 + raise VLLMValidationError("This model does not support generation")
95
96 params.verify(
97 self.model_config,
@@ -104,13 +105,13 @@ class InputProcessor:
105 self.vllm_config.reasoning_config is None
106 or not self.vllm_config.reasoning_config.enabled
107 ):
107 - raise ValueError(
108 + raise VLLMValidationError(
109 "thinking_token_budget is set but reasoning_config is "
110 "not configured. Please set --reasoning-parser "
111 "and/or --reasoning-config to use thinking_token_budget."
112 )
113 if self.use_v2_model_runner:
113 - raise ValueError(
114 + raise VLLMValidationError(
115 "thinking_token_budget is not yet supported by the V2 "
116 "model runner. Run vLLM with VLLM_USE_V2_MODEL_RUNNER=0 "
117 "to use thinking_token_budget."
@@ -120,7 +121,7 @@ class InputProcessor:
121 task for task in supported_tasks if task in POOLING_TASKS
122 ]
123 if not supported_pooling_tasks:
123 - raise ValueError("This model does not support pooling")
124 + raise VLLMValidationError("This model does not support pooling")
125
126 if params.task is None:
127 if "token_embed" in supported_pooling_tasks:
@@ -131,7 +132,7 @@ class InputProcessor:
132 params.task = "plugin"
133
134 if params.task not in supported_pooling_tasks:
134 - raise ValueError(
135 + raise VLLMValidationError(
136 f"Unsupported task: {params.task!r} "
137 f"Supported tasks: {supported_pooling_tasks}"
138 )
@@ -149,7 +150,7 @@ class InputProcessor:
150
151 # LoRA request passed in while LoRA is not enabled
152 if not self.lora_config:
152 - raise ValueError(
153 + raise VLLMValidationError(
154 f"Got lora_request {lora_request} but LoRA is not enabled!"
155 )
156
@@ -261,7 +262,7 @@ class InputProcessor:
262 dp_local_size = parallel_config.data_parallel_size_local
263 num_ranks = dp_local_size if parallel_config.local_engines_only else dp_size
264 if data_parallel_rank is not None and not (0 <= data_parallel_rank < num_ranks):
264 - raise ValueError(
265 + raise VLLMValidationError(
266 f"data_parallel_rank {data_parallel_rank} "
267 f"is out of range [0, {num_ranks})."
268 )
@@ -393,7 +394,7 @@ class InputProcessor:
394 return
395
396 if prompt_len == 0 and prompt_type == "decoder":
396 - raise ValueError(f"The {prompt_type} prompt cannot be empty")
397 + raise VLLMValidationError(f"The {prompt_type} prompt cannot be empty")
398
399 model_config = self.model_config
400 max_prompt_len = (
@@ -415,7 +416,7 @@ class InputProcessor:
416 "number of text tokens."
417 )
418
418 - raise ValueError(
419 + raise VLLMValidationError(
420 f"The {prompt_type} prompt (length {prompt_len}) is "
421 f"longer than the maximum model length of {max_prompt_len}. "
422 f"{suggestion}"
@@ -425,7 +426,7 @@ class InputProcessor:
426 "Make sure that `max_model_len` is no smaller than the "
427 "number of text tokens (prompt + requested output tokens)."
428 )
428 - raise ValueError(
429 + raise VLLMValidationError(
430 f"The {prompt_type} prompt (length {prompt_len}) plus the number of "
431 f"requested output tokens (at least 1) is longer than the maximum "
432 f"model length of {max_prompt_len}. {suggestion}"
@@ -457,7 +458,7 @@ class InputProcessor:
458 for mm_position in mm_positions:
459 num_embeds = mm_position.get_num_embeds()
460 if num_embeds > self.mm_encoder_cache_size:
460 - raise ValueError(
461 + raise VLLMValidationError(
462 f"The {prompt_type} prompt contains a(n) {modality} item "
463 f"with {num_embeds} embedding tokens, which exceeds the "
464 f"pre-allocated encoder cache size "
@@ -481,7 +482,9 @@ class InputProcessor:
482 # truly out-of-vocabulary.
483 model_vocab_size = model_config.get_vocab_size()
484 if max_input_id > max(tokenizer.max_token_id, model_vocab_size - 1):
484 - raise ValueError(f"Token id {max_input_id} is out of vocabulary")
485 + raise VLLMValidationError(
486 + f"Token id {max_input_id} is out of vocabulary"
487 + )
488
489 def _validate_model_inputs(
490 self,
vllm/v1/sample/logits_processor/__init__.py
+7 -1
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING
10
11 import torch
12
13 +from vllm.exceptions import VLLMValidationError
14 from vllm.logger import init_logger
15 from vllm.logits_process import LogitsProcessor as RequestLogitsProcessor
16 from vllm.sampling_params import SamplingParams
@@ -228,7 +229,12 @@ def validate_logits_processors_parameters(
229 tuple(logits_processors) if logits_processors is not None else None
230 )
231 for logits_procs in cached_load_custom_logitsprocs(logits_processors):
231 - logits_procs.validate_params(sampling_params)
232 + try:
233 + logits_procs.validate_params(sampling_params)
234 + except ValueError as e:
235 + # Legacy custom logitsprocs may still raise ValueError from
236 + # validate_params; convert for backward compatibility.
237 + raise VLLMValidationError(str(e)) from e
238
239
240 class AdapterLogitsProcessor(LogitsProcessor):
vllm/v1/sample/logits_processor/interface.py
+3 -1
@@ -62,7 +62,9 @@ class LogitsProcessor(ABC):
62 def validate_params(cls, sampling_params: SamplingParams):
63 """Validate sampling params for this logits processor.
64
65 - Raise ValueError for invalid ones.
65 + Raise ``VLLMValidationError`` (preferred) / ``ValueError`` (backward compatible)
66 + for invalid params. Bare ``ValueError`` is converted to ``VLLMValidationError``
67 + at the engine boundary so online serving returns HTTP 400.
68 """
69 return None
70