[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