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

Signed-off-by: Zach Zhu <zzqshu@126.com>
This commit is contained in:
Zach Zhu authored and GitHub committed 2026-07-29 04:20:40 +00:00
1 parent f37f03db4a
commit 58f9659397
46 files changed
+370 -248

No files matched your search

+5 -1
View File
@@ -3,6 +3,8 @@
import pytest
from vllm.exceptions import VLLMValidationError
from ..conftest import IMAGE_ASSETS
HF_IMAGE_PROMPTS = IMAGE_ASSETS.prompts(
@@ -19,7 +21,9 @@ models = ["llava-hf/llava-1.5-7b-hf"]
def test_context_length_too_short(vllm_runner, image_assets, model):
images = [asset.pil_image for asset in image_assets]
with pytest.raises(ValueError, match="longer than the maximum model length"):
with pytest.raises(
VLLMValidationError, match="longer than the maximum model length"
):
vllm_model = vllm_runner(
model,
# LLaVA has a feature size of 576
+2 -1
View File
@@ -6,6 +6,7 @@ import pytest
from vllm import LLM
from vllm.distributed import cleanup_dist_env_and_memory
from vllm.exceptions import VLLMValidationError
from vllm.sampling_params import SamplingParams
@@ -157,7 +158,7 @@ def test_chat_batch_failure_cleanup(llm_for_failure_test):
batch_2 = [valid_msg, valid_msg]
sampling_params = SamplingParams(temperature=0, max_tokens=10)
with pytest.raises(ValueError, match="maximum context length is"):
with pytest.raises(VLLMValidationError, match="maximum context length is"):
llm.chat(batch_1, sampling_params=sampling_params)
assert llm.llm_engine.get_num_unfinished_requests() == 0
@@ -5,17 +5,18 @@ import pytest
import torch
from vllm import LLM
from vllm.exceptions import VLLMValidationError
def test_empty_prompt():
llm = LLM(model="openai-community/gpt2", enforce_eager=True)
with pytest.raises(ValueError, match="decoder prompt cannot be empty"):
with pytest.raises(VLLMValidationError, match="decoder prompt cannot be empty"):
llm.generate([""])
def test_out_of_vocab_token():
llm = LLM(model="openai-community/gpt2", enforce_eager=True)
with pytest.raises(ValueError, match="out of vocabulary"):
with pytest.raises(VLLMValidationError, match="out of vocabulary"):
llm.generate({"prompt_token_ids": [999999]})
@@ -6,6 +6,7 @@ import pytest
from tests.entrypoints.multimodal.conftest import managed_llm
from vllm import LLM, SamplingParams
from vllm.assets.image import ImageAsset
from vllm.exceptions import VLLMValidationError
MODEL = "llava-hf/llava-1.5-7b-hf"
PROMPT = "USER: <image>\nDescribe this image briefly.\nASSISTANT:"
@@ -42,7 +43,7 @@ def test_generate_with_embedding(llm: LLM):
def test_raw_image_rejected(llm: LLM):
"""Raw image input is still rejected when limit=0."""
raw_image = ImageAsset("stop_sign").pil_image
with pytest.raises(ValueError, match=r"At most 0 image\(s\)"):
with pytest.raises(VLLMValidationError, match=r"At most 0 image\(s\)"):
llm.generate(
{"prompt": PROMPT, "multi_modal_data": {"image": raw_image}},
sampling_params=SamplingParams(max_tokens=16),
@@ -18,6 +18,7 @@ from tests.utils import RemoteOpenAIServer
from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionRequest,
)
from vllm.exceptions import VLLMValidationError
from vllm.sampling_params import SamplingParams
# any model with a chat template should work here
@@ -1074,7 +1075,7 @@ def test_chat_completion_request_n_parameter_exceeds_default_limit(
max_tokens=10,
)
with pytest.raises(ValueError, match="n must be at most"):
with pytest.raises(VLLMValidationError, match="n must be at most"):
request.to_sampling_params(
max_tokens=10,
default_sampling_params={},
@@ -1136,7 +1137,7 @@ def test_chat_completion_request_n_parameter_custom_limit(
max_tokens=10,
)
with pytest.raises(ValueError, match="n must be at most 128"):
with pytest.raises(VLLMValidationError, match="n must be at most 128"):
request_over.to_sampling_params(
max_tokens=10,
default_sampling_params={},
@@ -1160,7 +1161,7 @@ def test_chat_completion_request_n_parameter_massive_value(
max_tokens=1,
)
with pytest.raises(ValueError, match="n must be at most"):
with pytest.raises(VLLMValidationError, match="n must be at most"):
request.to_sampling_params(
max_tokens=1,
default_sampling_params={},
@@ -6,7 +6,6 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from pydantic import ValidationError
from vllm.config.multimodal import MultiModalConfig
from vllm.entrypoints.openai.chat_completion.protocol import (
@@ -18,6 +17,7 @@ from vllm.entrypoints.openai.engine.protocol import GenerationError
from vllm.entrypoints.openai.models.protocol import BaseModelPath
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.entrypoints.scale_out.render.serving import ServingRender
from vllm.exceptions import VLLMValidationError
from vllm.outputs import CompletionOutput, RequestOutput
from vllm.renderers.hf import HfRenderer
from vllm.renderers.online_renderer import OnlineRenderer
@@ -479,7 +479,7 @@ def test_json_schema_response_format_missing_schema():
def test_structural_tag_response_format_invalid(format_value):
"""Malformed structural tags should be rejected during request validation."""
with pytest.raises(
ValidationError,
VLLMValidationError,
match="Invalid response_format structural_tag",
):
ChatCompletionRequest(
@@ -493,7 +493,7 @@ def test_structural_tag_response_format_invalid(format_value):
def test_batch_structural_tag_response_format_invalid(format_value):
"""Batch chat should reject malformed structural tags at request parsing."""
with pytest.raises(
ValidationError,
VLLMValidationError,
match="Invalid response_format structural_tag",
):
BatchChatCompletionRequest(
@@ -507,7 +507,7 @@ def test_batch_structural_tag_response_format_invalid(format_value):
def test_structured_outputs_structural_tag_invalid(structural_tag):
"""Malformed direct structured_outputs structural tags should be rejected."""
with pytest.raises(
ValidationError,
VLLMValidationError,
match="Invalid structured_outputs structural_tag",
):
ChatCompletionRequest(
@@ -521,7 +521,7 @@ def test_structured_outputs_structural_tag_invalid(structural_tag):
def test_non_numeric_logprobs_rejected(field_name):
"""A non-numeric logprobs value must be a clean 400 validation error, not a
TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
with pytest.raises(VLLMValidationError, match=f"`{field_name}` must be an integer"):
ChatCompletionRequest(
model=MODEL_NAME,
messages=[{"role": "user", "content": "hello"}],
@@ -14,11 +14,11 @@ digit-token vocab id).
import math
import pytest
from pydantic import ValidationError
from tests.utils import RemoteOpenAIServer
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.completion.protocol import CompletionRequest
from vllm.exceptions import VLLMValidationError
MODEL_NAME = "Qwen/Qwen2.5-1.5B-Instruct"
@@ -87,7 +87,7 @@ def test_completion_request_decouples_top_k_from_explicit_token_ids():
def test_completion_rejects_explicit_token_ids_without_generated_tokens():
with pytest.raises(ValidationError, match="no output tokens are generated"):
with pytest.raises(VLLMValidationError, match="no output tokens are generated"):
CompletionRequest(
model=MODEL_NAME,
prompt="Hello",
@@ -99,7 +99,7 @@ def test_completion_rejects_explicit_token_ids_without_generated_tokens():
def test_requests_reject_explicit_token_ids_with_beam_search():
with pytest.raises(ValidationError, match="not supported with beam search"):
with pytest.raises(VLLMValidationError, match="not supported with beam search"):
ChatCompletionRequest(
model=MODEL_NAME,
messages=[{"role": "user", "content": "Hello"}],
@@ -108,7 +108,7 @@ def test_requests_reject_explicit_token_ids_with_beam_search():
use_beam_search=True,
)
with pytest.raises(ValidationError, match="not supported with beam search"):
with pytest.raises(VLLMValidationError, match="not supported with beam search"):
CompletionRequest(
model=MODEL_NAME,
prompt="Hello",
@@ -2,15 +2,15 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
from pydantic import ValidationError
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.completion.protocol import CompletionRequest
from vllm.exceptions import VLLMValidationError
@pytest.mark.parametrize("raw_value", [-2, 0.6, 10.5])
def test_chat_completion_request_rejects_invalid_thinking_token_budget(raw_value):
with pytest.raises(ValidationError, match="thinking_token_budget"):
with pytest.raises(VLLMValidationError, match="thinking_token_budget"):
ChatCompletionRequest.model_validate(
{
"model": "qwen",
@@ -44,7 +44,7 @@ def test_chat_completion_request_accepts_minus_one_as_unlimited():
@pytest.mark.parametrize("raw_value", [0.6, 3.14, -2])
def test_completion_request_rejects_invalid_thinking_token_budget(raw_value):
with pytest.raises(ValidationError, match="thinking_token_budget"):
with pytest.raises(VLLMValidationError, match="thinking_token_budget"):
CompletionRequest.model_validate(
{
"model": "qwen",
@@ -6,7 +6,6 @@ from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from pydantic import ValidationError
from vllm.config.multimodal import MultiModalConfig
from vllm.entrypoints.openai.completion.protocol import CompletionRequest
@@ -18,6 +17,7 @@ from vllm.entrypoints.openai.engine.protocol import (
from vllm.entrypoints.openai.models.protocol import BaseModelPath
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.entrypoints.scale_out.render.serving import ServingRender
from vllm.exceptions import VLLMValidationError
from vllm.outputs import CompletionOutput, RequestOutput
from vllm.renderers.hf import HfRenderer
from vllm.renderers.online_renderer import OnlineRenderer
@@ -430,7 +430,7 @@ def test_json_schema_response_format_missing_schema():
def test_structural_tag_response_format_invalid(format_value):
"""Malformed structural tags should be rejected during request validation."""
with pytest.raises(
ValidationError,
VLLMValidationError,
match="Invalid response_format structural_tag",
):
CompletionRequest(
@@ -445,7 +445,7 @@ def test_structural_tag_response_format_invalid(format_value):
def test_structured_outputs_structural_tag_invalid(structural_tag):
"""Malformed direct structured_outputs structural tags should be rejected."""
with pytest.raises(
ValidationError,
VLLMValidationError,
match="Invalid structured_outputs structural_tag",
):
CompletionRequest(
@@ -616,7 +616,7 @@ class TestCompletionPromptListLimit:
def test_non_numeric_logprobs_rejected(field_name):
"""A non-numeric logprobs value must be a clean 400 validation error, not a
TypeError from the mode='before' comparison (which surfaces as HTTP 500)."""
with pytest.raises(ValidationError, match=f"`{field_name}` must be an integer"):
with pytest.raises(VLLMValidationError, match=f"`{field_name}` must be an integer"):
CompletionRequest(
model=MODEL_NAME,
prompt="Test prompt",
@@ -13,6 +13,7 @@ import torch
from tests.utils import RemoteOpenAIServer
from vllm.config import ModelConfig
from vllm.exceptions import VLLMValidationError
from vllm.renderers.embed_utils import safe_load_prompt_embeds
@@ -111,5 +112,5 @@ def test_disable_prompt_embeds(dtype: torch.dtype, seq_len: int, hidden_size: in
buffer.seek(0)
encoded_tensor = pybase64.b64encode(buffer.getvalue())
with pytest.raises(ValueError, match="--enable-prompt-embeds"):
with pytest.raises(VLLMValidationError, match="--enable-prompt-embeds"):
safe_load_prompt_embeds(model_config, encoded_tensor)
@@ -14,6 +14,7 @@ from vllm.entrypoints.openai.responses.protocol import (
ResponsesRequest,
ResponseTextConfig,
)
from vllm.exceptions import VLLMValidationError
from vllm.sampling_params import StructuredOutputsParams
@@ -163,7 +164,7 @@ class TestResponsesRequestSamplingParams:
text=text_config,
)
with pytest.raises(ValueError) as exc_info:
with pytest.raises(VLLMValidationError) as exc_info:
request.to_sampling_params(default_max_tokens=1000)
assert "Cannot specify both structured_outputs and text.format" in str(
@@ -8,54 +8,95 @@ PrometheusInstrumentatorMiddleware before being caught by ServerErrorMiddleware.
"""
from argparse import Namespace
from http import HTTPStatus
import httpx
import pytest
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi import HTTPException
from prometheus_client import CollectorRegistry
from prometheus_fastapi_instrumentator import Instrumentator
from vllm.entrypoints.serve.utils.server_utils import exception_handler
from vllm.exceptions import VLLMNotFoundError, VLLMValidationError
from vllm.entrypoints.openai.api_server import build_app
from vllm.exceptions import (
VLLMNotFoundError,
VLLMServerError,
VLLMValidationError,
)
@pytest.fixture
@pytest.fixture(scope="module")
def should_do_global_cleanup_after_test() -> bool:
# This suite never initializes distributed/accelerator state.
return False
def _build_args() -> Namespace:
"""Minimal args for ``build_app``; avoids ``make_arg_parser`` device probing."""
return Namespace(
disable_fastapi_docs=True,
enable_offline_docs=False,
root_path=None,
allowed_origins=["*"],
allow_credentials=False,
allowed_methods=["*"],
allowed_headers=["*"],
api_key=None,
enable_request_id_headers=False,
enable_fault_tolerance=False,
middleware=[],
log_error_stack=False,
)
@pytest.fixture(scope="module")
def registry():
"""Create a fresh Prometheus registry for each test."""
"""Shared Prometheus registry for the module-scoped app."""
return CollectorRegistry()
@pytest.fixture
@pytest.fixture(scope="module")
def app(registry):
"""Create a minimal FastAPI app that mirrors vLLM's exception handler
and Prometheus middleware setup."""
"""Build the real vLLM FastAPI app once and attach probe routes that raise.
app = FastAPI()
Patch the name used by ``attach_router`` (imported into the instrumentator
metrics module), not ``vllm.v1.metrics.prometheus`` alone — that binding is
captured at import time.
"""
import vllm.entrypoints.serve.instrumentator.metrics as metrics_mod
# Mock app state that exception_handler needs
app.state.args = Namespace(log_error_stack=False)
original = metrics_mod.get_prometheus_registry
metrics_mod.get_prometheus_registry = lambda: registry
try:
app = build_app(_build_args(), supported_tasks=())
finally:
metrics_mod.get_prometheus_registry = original
# Register exception handlers exactly as vLLM does in build_app()
app.exception_handler(HTTPException)(_http_exception_handler)
app.exception_handler(RequestValidationError)(_validation_exception_handler)
app.exception_handler(ValueError)(exception_handler)
app.exception_handler(TypeError)(exception_handler)
app.exception_handler(OverflowError)(exception_handler)
app.exception_handler(NotImplementedError)(exception_handler)
app.exception_handler(VLLMValidationError)(exception_handler)
app.exception_handler(VLLMNotFoundError)(exception_handler)
app.exception_handler(Exception)(exception_handler)
@app.get("/raise_http_exception_400")
async def raise_http_exception_400():
raise HTTPException(status_code=400, detail="bad request")
# Instrument with Prometheus (same as vLLM's attach_router)
Instrumentator(
excluded_handlers=["/metrics"],
registry=registry,
).add().instrument(app)
@app.get("/raise_http_exception_404")
async def raise_http_exception_404():
raise HTTPException(status_code=404, detail="not found")
@app.get("/raise_request_validation_error")
async def raise_request_validation_error(n: int):
# Invalid ``n`` triggers FastAPI's RequestValidationError.
return {"n": n}
@app.get("/raise_vllm_validation_error")
async def raise_vllm_validation_error():
raise VLLMValidationError("bad parameter", parameter="temperature")
@app.get("/raise_vllm_not_found_error")
async def raise_vllm_not_found_error():
raise VLLMNotFoundError("model not found")
@app.get("/raise_vllm_server_error")
async def raise_vllm_server_error():
# Bare VLLMServerError goes through vllm_error_handler → 500.
# EngineGenerateError / EngineDeadError are not used here: they call
# terminate_if_errored and need engine/server state.
raise VLLMServerError("internal server failure")
# Test routes that raise different exception types
@app.get("/raise_value_error")
async def raise_value_error():
raise ValueError("invalid input value")
@@ -72,22 +113,6 @@ def app(registry):
async def raise_not_implemented_error():
raise NotImplementedError("feature not supported")
@app.get("/raise_vllm_validation_error")
async def raise_vllm_validation_error():
raise VLLMValidationError("bad parameter", parameter="temperature")
@app.get("/raise_vllm_not_found_error")
async def raise_vllm_not_found_error():
raise VLLMNotFoundError("model not found")
@app.get("/raise_http_exception_400")
async def raise_http_exception_400():
raise HTTPException(status_code=400, detail="bad request")
@app.get("/raise_http_exception_404")
async def raise_http_exception_404():
raise HTTPException(status_code=404, detail="not found")
@app.get("/raise_runtime_error")
async def raise_runtime_error():
raise RuntimeError("unexpected server error")
@@ -99,14 +124,6 @@ def app(registry):
return app
async def _http_exception_handler(req: Request, exc: HTTPException):
return JSONResponse({"error": exc.detail}, status_code=exc.status_code)
async def _validation_exception_handler(req: Request, exc: RequestValidationError):
return JSONResponse({"error": str(exc)}, status_code=HTTPStatus.BAD_REQUEST)
def _get_http_requests_total(registry, method: str, handler: str):
"""Extract the http_requests_total metric values grouped by status.
@@ -128,31 +145,31 @@ def _get_http_requests_total(registry, method: str, handler: str):
@pytest.mark.asyncio
@pytest.mark.parametrize(
"endpoint,expected_status_group,expected_http_code",
"endpoint,expected_status_group,expected_http_code,request_kwargs",
[
# These should record as 4xx in Prometheus
("/raise_value_error", "4xx", 400),
("/raise_type_error", "4xx", 400),
("/raise_overflow_error", "4xx", 400),
("/raise_vllm_validation_error", "4xx", 400),
("/raise_vllm_not_found_error", "4xx", 404),
("/raise_http_exception_400", "4xx", 400),
("/raise_http_exception_404", "4xx", 404),
# NotImplementedError returns 501 which is still 5xx group
("/raise_not_implemented_error", "5xx", 501),
# These should record as 5xx in Prometheus (genuine server errors)
("/raise_runtime_error", "5xx", 500),
# Successful requests should record as 2xx
("/success", "2xx", 200),
("/raise_http_exception_400", "4xx", 400, {}),
("/raise_http_exception_404", "4xx", 404, {}),
("/raise_request_validation_error", "4xx", 400, {"params": {"n": "x"}}),
("/raise_vllm_validation_error", "4xx", 400, {}),
("/raise_vllm_not_found_error", "4xx", 404, {}),
("/raise_vllm_server_error", "5xx", 500, {}),
("/raise_value_error", "4xx", 400, {}),
("/raise_type_error", "4xx", 400, {}),
("/raise_overflow_error", "4xx", 400, {}),
("/raise_not_implemented_error", "5xx", 501, {}),
("/raise_runtime_error", "5xx", 500, {}),
("/success", "2xx", 200, {}),
],
ids=[
"HTTPException(400)->4xx",
"HTTPException(404)->4xx",
"RequestValidationError->4xx",
"VLLMValidationError->4xx",
"VLLMNotFoundError->4xx",
"VLLMServerError->5xx",
"ValueError->4xx",
"TypeError->4xx",
"OverflowError->4xx",
"VLLMValidationError->4xx",
"VLLMNotFoundError->4xx",
"HTTPException(400)->4xx",
"HTTPException(404)->4xx",
"NotImplementedError->5xx",
"RuntimeError->5xx",
"success->2xx",
@@ -164,6 +181,7 @@ async def test_http_requests_total_records_correct_status(
endpoint,
expected_status_group,
expected_http_code,
request_kwargs,
):
"""Verify that http_requests_total records the correct status group.
@@ -177,7 +195,7 @@ async def test_http_requests_total_records_correct_status(
async with httpx.AsyncClient(
transport=transport, base_url="http://testserver"
) as client:
response = await client.get(endpoint)
response = await client.get(endpoint, **request_kwargs)
# Verify the HTTP response code returned to the client is correct
assert response.status_code == expected_http_code, (
@@ -21,6 +21,7 @@ from vllm.entrypoints.chat_utils import (
parse_chat_messages,
parse_chat_messages_async,
)
from vllm.exceptions import VLLMValidationError
from vllm.inputs import MultiModalDataDict, MultiModalUUIDDict
from vllm.multimodal.utils import (
encode_audio_url,
@@ -1504,7 +1505,7 @@ def test_parse_chat_messages_rejects_too_many_images_in_one_message(
"ignore",
message="coroutine 'async_get_and_parse_image' was never awaited",
)
with pytest.raises(ValueError, match="At most"):
with pytest.raises(VLLMValidationError, match="At most"):
parse_chat_messages(
[
{
@@ -1540,7 +1541,7 @@ def test_parse_chat_messages_rejects_too_many_images_across_messages(
"ignore",
message="coroutine 'async_get_and_parse_image' was never awaited",
)
with pytest.raises(ValueError, match="At most"):
with pytest.raises(VLLMValidationError, match="At most"):
parse_chat_messages(
[
{
@@ -7,6 +7,7 @@ import pytest
import torch
from PIL import Image as PILImage
from vllm.exceptions import VLLMValidationError
from vllm.model_executor.models.gemma4_mm import (
Gemma4ForConditionalGeneration,
Gemma4ImagePixelInputs,
@@ -222,7 +223,7 @@ def test_limit_mm_per_prompt(
mm_data = {"image": images}
# Expect ValueError when exceeding limit
with pytest.raises(ValueError, match="At most 1 image"):
with pytest.raises(VLLMValidationError, match="At most 1 image"):
processor(
prompt,
mm_items=processor.info.parse_mm_data(mm_data),
@@ -7,6 +7,7 @@ import pytest
import torch
from PIL import Image as PILImage
from vllm.exceptions import VLLMValidationError
from vllm.model_executor.models.gemma4_mm import Gemma4ImagePixelInputs
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import MultiModalFieldConfig
@@ -197,7 +198,7 @@ def test_limit_mm_per_prompt(
mm_data = {"image": images}
with pytest.raises(ValueError, match="At most 1 image"):
with pytest.raises(VLLMValidationError, match="At most 1 image"):
processor(
prompt,
mm_items=processor.info.parse_mm_data(mm_data),
@@ -14,7 +14,7 @@ import aiohttp
import pytest
from vllm.entrypoints.serve.utils.error_response import create_error_response
from vllm.exceptions import VLLMUnprocessableEntityError
from vllm.exceptions import VLLMClientError, VLLMUnprocessableEntityError
from vllm.multimodal.media import MediaConnector
@@ -35,9 +35,9 @@ class TestVLLMUnprocessableEntityError:
assert "parameter=image_url" in str(exc)
assert "value=https://example.com/image.jpg" in str(exc)
def test_is_value_error_subclass(self):
def test_is_client_error_subclass(self):
exc = VLLMUnprocessableEntityError("Test")
assert isinstance(exc, ValueError)
assert isinstance(exc, VLLMClientError)
class TestMediaConnectorErrorHandling:
+6 -1
View File
@@ -8,6 +8,7 @@ import numpy as np
import pytest
from vllm.config import ModelConfig
from vllm.exceptions import VLLMValidationError
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.processing.context import InputProcessingContext
from vllm.multimodal.processing.processor import (
@@ -931,7 +932,11 @@ def test_limit_mm_per_prompt_apply(model_id, num_images, limit, is_valid):
else:
mm_data = {"image": [image] * num_images}
exc_ctx = nullcontext() if is_valid else pytest.raises(ValueError, match="At most")
exc_ctx = (
nullcontext()
if is_valid
else pytest.raises(VLLMValidationError, match="At most")
)
with exc_ctx:
processor(
@@ -26,6 +26,7 @@ from vllm.entrypoints.chat_utils import (
parse_chat_messages,
parse_chat_messages_async,
)
from vllm.exceptions import VLLMValidationError
from vllm.renderers.hf import (
_PROMPT_EMBEDS_PLACEHOLDER_SPAN_MISMATCH_ERROR,
_build_mixed_prompt_embeds,
@@ -264,7 +265,7 @@ def test_parse_chat_messages_requires_flag():
"content": [{"type": "prompt_embeds", "data": b64}],
}
]
with pytest.raises(ValueError, match=_ENABLE_PROMPT_EMBEDS_ERROR):
with pytest.raises(VLLMValidationError, match=_ENABLE_PROMPT_EMBEDS_ERROR):
parse_chat_messages(
messages,
mc,
@@ -283,7 +284,7 @@ def test_parse_chat_messages_rejects_missing_data():
"content": [{"type": "prompt_embeds"}], # no `data`
}
]
with pytest.raises(ValueError, match=_PROMPT_EMBEDS_MISSING_DATA_ERROR):
with pytest.raises(VLLMValidationError, match=_PROMPT_EMBEDS_MISSING_DATA_ERROR):
parse_chat_messages(
messages,
mc,
+4 -3
View File
@@ -11,6 +11,7 @@ import pytest
import torch
from vllm.config import ModelConfig
from vllm.exceptions import VLLMValidationError
from vllm.inputs import SingletonPrompt
from vllm.renderers import TokenizeParams
from vllm.renderers.hf import HfRenderer
@@ -286,7 +287,7 @@ class TestRenderPrompt:
)
with pytest.raises(
ValueError,
VLLMValidationError,
match="maximum context length is",
):
renderer.tokenize_prompts(
@@ -307,7 +308,7 @@ class TestRenderPrompt:
)
with pytest.raises(
ValueError,
VLLMValidationError,
match="maximum context length is",
):
renderer.tokenize_prompts(
@@ -328,7 +329,7 @@ class TestRenderPrompt:
)
with pytest.raises(
ValueError,
VLLMValidationError,
match="maximum context length is",
):
renderer.tokenize_prompts(
+1 -1
View File
@@ -42,7 +42,7 @@ class TestNonFiniteRepetitionPenalty:
ids=["nan", "inf", "-inf", "math.nan", "math.inf"],
)
def test_non_finite_repetition_penalty_rejected(self, value: float):
with pytest.raises(ValueError, match="repetition_penalty"):
with pytest.raises(VLLMValidationError, match="repetition_penalty"):
SamplingParams(repetition_penalty=value)
def test_finite_repetition_penalty_accepted(self):
+3 -2
View File
@@ -15,6 +15,7 @@ from vllm.envs import (
env_with_choices,
environment_variables,
)
from vllm.exceptions import VLLMValidationError
def test_getattr_without_cache(monkeypatch: pytest.MonkeyPatch):
@@ -547,7 +548,7 @@ class TestVllmMaxNSequences:
max_n = envs.VLLM_MAX_N_SEQUENCES
SamplingParams(n=max_n)
with pytest.raises(ValueError, match="n must be at most"):
with pytest.raises(VLLMValidationError, match="n must be at most"):
SamplingParams(n=max_n + 1)
def test_sampling_params_respects_custom_limit(
@@ -563,5 +564,5 @@ class TestVllmMaxNSequences:
SamplingParams(n=128)
with pytest.raises(ValueError, match="n must be at most 128"):
with pytest.raises(VLLMValidationError, match="n must be at most 128"):
SamplingParams(n=129)
+17 -11
View File
@@ -52,13 +52,19 @@ class MockModelConfig:
def test_removed_pooling_parameters(parameter: str, value: Any, message: str):
data = {"input": "hello", parameter: value}
for request_type in (EmbeddingRequest, ClassificationRequest, PoolingRequest):
with pytest.raises(ValidationError, match=message) as exc_info:
with pytest.raises(VLLMValidationError, match=message):
TypeAdapter(request_type).validate_python(data)
assert len(exc_info.value.errors()) == 1
with pytest.raises(ValidationError, match=message) as exc_info:
TypeAdapter(PoolerConfig).validate_python({parameter: value})
assert len(exc_info.value.errors()) == 1
# PoolerConfig still raises bare ValueError for `normalize`
# (wrapped to ValidationError by Pydantic), but `check_removed_pooling_task`
# raises VLLMValidationError for removed tasks.
if parameter == "normalize":
with pytest.raises(ValidationError, match=message) as exc_info:
TypeAdapter(PoolerConfig).validate_python({parameter: value})
assert len(exc_info.value.errors()) == 1
else:
with pytest.raises(VLLMValidationError, match=message):
TypeAdapter(PoolerConfig).validate_python({parameter: value})
if parameter == "task":
with pytest.raises(VLLMValidationError, match=message):
@@ -80,7 +86,7 @@ def test_embed():
invalid_parameters = classify_parameters + step_pooling_parameters
for p in set(invalid_parameters) - set(embed_parameters):
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
pooling_params = PoolingParams(task=task, **{p: True})
pooling_params.verify(model_config)
@@ -100,7 +106,7 @@ def test_embed_dimensions(model_info: EmbedModelInfo):
pooling_params = PoolingParams(task=task, dimensions=None)
pooling_params.verify(model_config)
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
pooling_params = PoolingParams(task=task, dimensions=1)
pooling_params.verify(model_config)
@@ -131,7 +137,7 @@ def test_embed_dimensions_matryoshka_without_list_upper_bound():
PoolingParams(task=task, dimensions=16).verify(model_config)
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
PoolingParams(task=task, dimensions=64).verify(model_config)
@@ -150,7 +156,7 @@ def test_classify(task):
invalid_parameters = embed_parameters + step_pooling_parameters
for p in set(invalid_parameters) - set(classify_parameters):
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
pooling_params = PoolingParams(task=task, **{p: True})
pooling_params.verify(model_config)
@@ -176,7 +182,7 @@ def test_token_embed(pooling_type: str):
invalid_parameters = classify_parameters + step_pooling_parameters
for p in set(invalid_parameters) - set(embed_parameters):
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
pooling_params = PoolingParams(task=task, **{p: True})
pooling_params.verify(model_config)
@@ -202,6 +208,6 @@ def test_token_classify(pooling_type: str):
invalid_parameters = embed_parameters + step_pooling_parameters
for p in set(invalid_parameters) - set(classify_parameters):
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
pooling_params = PoolingParams(task=task, **{p: True})
pooling_params.verify(model_config)
+2 -1
View File
@@ -5,6 +5,7 @@ from dataclasses import dataclass
import pytest
from vllm import SamplingParams
from vllm.exceptions import VLLMValidationError
@dataclass
@@ -32,7 +33,7 @@ class MockModelConfig:
)
def test_diffusion_rejects_unsupported_params(kwargs: dict):
params = SamplingParams(**kwargs)
with pytest.raises(ValueError, match="not yet supported with diffusion"):
with pytest.raises(VLLMValidationError, match="not yet supported with diffusion"):
params.verify(MockModelConfig(is_diffusion=True), None, None, None)
@@ -4,6 +4,7 @@
import pytest
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.exceptions import VLLMValidationError
def test_chat_completion_request_with_no_tools():
@@ -27,7 +28,7 @@ def test_chat_completion_request_with_no_tools():
assert request.tool_choice == "none"
# tools key present but empty -- should be rejected
with pytest.raises(ValueError, match="must not be an empty array"):
with pytest.raises(VLLMValidationError, match="must not be an empty array"):
ChatCompletionRequest.model_validate(
{
"messages": [{"role": "user", "content": "Hello"}],
@@ -40,7 +41,7 @@ def test_chat_completion_request_with_no_tools():
@pytest.mark.parametrize("tool_choice", ["auto", "required"])
def test_chat_completion_request_with_tool_choice_but_no_tools(tool_choice):
with pytest.raises(
ValueError, match="When using `tool_choice`, `tools` must be set."
VLLMValidationError, match="When using `tool_choice`, `tools` must be set."
):
ChatCompletionRequest.model_validate(
{
@@ -51,7 +52,7 @@ def test_chat_completion_request_with_tool_choice_but_no_tools(tool_choice):
)
with pytest.raises(
ValueError, match="When using `tool_choice`, `tools` must be set."
VLLMValidationError, match="When using `tool_choice`, `tools` must be set."
):
ChatCompletionRequest.model_validate(
{
@@ -134,7 +135,7 @@ SAMPLE_TOOL = {
def test_structured_outputs_with_named_tool_choice_rejected():
"""structured_outputs cannot be combined with a named tool_choice."""
with pytest.raises(
ValueError,
VLLMValidationError,
match="structured outputs or tools, not both",
):
ChatCompletionRequest.model_validate(
@@ -168,7 +169,7 @@ def test_structured_outputs_with_auto_tool_choice_allowed():
def test_multiple_structured_outputs_rejected():
"""Only one kind of structured output constraint is allowed."""
with pytest.raises(
ValueError,
VLLMValidationError,
match="You can only use one kind of constraints",
):
ChatCompletionRequest.model_validate(
@@ -2,12 +2,12 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
from pydantic import ValidationError
from vllm.entrypoints.openai.responses.protocol import (
ResponsesRequest,
ResponsesResponse,
)
from vllm.exceptions import VLLMValidationError
SAMPLE_TOOL = {
"type": "function",
@@ -58,13 +58,13 @@ def test_responses_request_required_without_tools(tools):
if tools is not None:
kwargs["tools"] = tools
with pytest.raises(
ValidationError, match="Tool choice 'required' must be specified"
VLLMValidationError, match="Tool choice 'required' must be specified"
):
ResponsesRequest.model_validate(kwargs)
def test_responses_request_named_tool_choice_without_tools():
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
@@ -107,7 +107,7 @@ def test_responses_request_named_tool_choice_matching():
def test_responses_request_named_tool_choice_not_matching():
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
@@ -164,7 +164,7 @@ def test_responses_request_empty_tools_tool_choice_auto():
],
)
def test_responses_request_named_tool_choice_missing_name(tool_choice):
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
@@ -176,7 +176,7 @@ def test_responses_request_named_tool_choice_missing_name(tool_choice):
def test_responses_request_empty_tools_named_tool_choice():
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
with pytest.raises(VLLMValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
+1 -1
View File
@@ -59,7 +59,7 @@ def test_decoder_max_context_length_validation(
"Make sure that `max_model_len` is no smaller than the number of "
"text tokens (prompt + requested output tokens)."
)
with pytest.raises(ValueError) as excinfo:
with pytest.raises(VLLMValidationError) as excinfo:
vllm_model.generate_greedy(prompt_ids, max_tokens)
assert expected_msg in str(excinfo.value)
+3 -2
View File
@@ -16,6 +16,7 @@ Covers:
import pytest
from vllm import LLM, SamplingParams
from vllm.exceptions import VLLMValidationError
from vllm.outputs import RequestOutput
# Test configuration
@@ -479,13 +480,13 @@ def test_min_tokens_validation():
# Invalid cases
with pytest.raises(
ValueError,
VLLMValidationError,
match="min_tokens must be greater than or equal to 0",
):
SamplingParams(min_tokens=-1, max_tokens=10)
with pytest.raises(
ValueError,
VLLMValidationError,
match="min_tokens must be less than or equal to max_tokens",
):
SamplingParams(min_tokens=15, max_tokens=10)
+10 -3
View File
@@ -20,6 +20,7 @@ import pytest_asyncio
from vllm import SamplingParams
from vllm.engine.protocol import StreamingInput
from vllm.exceptions import VLLMValidationError
from vllm.outputs import RequestOutput
from vllm.platforms import current_platform
from vllm.sampling_params import RequestOutputKind
@@ -571,13 +572,17 @@ async def test_streaming_input_validation_errors(engine: AsyncLLM):
yield StreamingInput(prompt="test")
# Test n > 1 is rejected
with pytest.raises(ValueError, match="Input streaming not currently supported"):
with pytest.raises(
VLLMValidationError, match="Input streaming not currently supported"
):
params_n2 = SamplingParams(max_tokens=10, n=2)
async for _ in engine.generate(dummy_generator(), params_n2, "test_n2"):
pass
# Test FINAL_ONLY is rejected
with pytest.raises(ValueError, match="Input streaming not currently supported"):
with pytest.raises(
VLLMValidationError, match="Input streaming not currently supported"
):
params_final = SamplingParams(
max_tokens=10, output_kind=RequestOutputKind.FINAL_ONLY
)
@@ -585,7 +590,9 @@ async def test_streaming_input_validation_errors(engine: AsyncLLM):
pass
# Test stop strings are rejected
with pytest.raises(ValueError, match="Input streaming not currently supported"):
with pytest.raises(
VLLMValidationError, match="Input streaming not currently supported"
):
params_stop = SamplingParams(max_tokens=10, stop=["stop"])
async for _ in engine.generate(dummy_generator(), params_stop, "test_stop"):
pass
+4 -3
View File
@@ -19,6 +19,7 @@ from vllm.entrypoints.openai.chat_completion.protocol import (
from vllm.entrypoints.openai.chat_completion.serving import OpenAIServingChat
from vllm.entrypoints.openai.models.protocol import BaseModelPath
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.exceptions import VLLMValidationError
from vllm.inputs import PromptType
from vllm.outputs import RequestOutput
from vllm.platforms import current_platform
@@ -485,7 +486,7 @@ async def test_dp_rank_argument():
pass
# Test with out-of-range DP rank.
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
async for _ in engine.generate(
request_id="request-35",
prompt=TEXT_PROMPT,
@@ -554,8 +555,8 @@ async def test_header_dp_rank_argument():
# Test 2: Out-of-range DP rank (1)
mock_raw_request.headers = {"X-data-parallel-rank": "1"}
# should raise ValueError for out-of-range rank
with pytest.raises(ValueError):
# should raise VLLMValidationError for out-of-range rank
with pytest.raises(VLLMValidationError):
await serving_chat.create_chat_completion(req, mock_raw_request)
+2 -1
View File
@@ -11,6 +11,7 @@ import torch
from tests.utils import requires_spawn_multiprocessing
from vllm.config import VllmConfig
from vllm.exceptions import VLLMValidationError
from vllm.logger import init_logger
from vllm.sampling_params import SamplingParams
from vllm.v1.sample.logits_processor import (
@@ -61,7 +62,7 @@ class DummyLogitsProcessor(LogitsProcessor):
"target_token"
)
if target_token is not None and not isinstance(target_token, int):
raise ValueError(
raise VLLMValidationError(
f"target_token value {target_token} {type(target_token)} is not int"
)
+1 -1
View File
@@ -405,7 +405,7 @@ def test_max_logprobs():
runner.generate(["Hello world"], sampling_params=vllm_sampling_params)
bad_sampling_params = SamplingParams(logprobs=2)
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
runner.generate(["Hello world"], sampling_params=bad_sampling_params)
+4 -3
View File
@@ -4,6 +4,7 @@
import pytest
from vllm import LLM, SamplingParams
from vllm.exceptions import VLLMValidationError
MODEL = "hmellor/tiny-random-LlamaForCausalLM"
PROMPT = "Hello my name is Robert and I"
@@ -161,15 +162,15 @@ def test_allowed_token_ids(llm):
assert output[0].outputs[0].token_ids[-1] == token_id
# Reject empty allowed_token_ids.
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
_ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[]))
# Reject negative token id.
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
_ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[-1]))
# Reject out of vocabulary.
with pytest.raises(ValueError):
with pytest.raises(VLLMValidationError):
_ = llm.generate(PROMPT, SamplingParams(allowed_token_ids=[10000000]))
@@ -5,6 +5,7 @@
import pytest
from vllm.config import StructuredOutputsConfig
from vllm.exceptions import VLLMValidationError
from vllm.sampling_params import SamplingParams, StructuredOutputsParams
pytestmark = pytest.mark.cpu_test
@@ -32,7 +33,7 @@ def test_structured_outputs_rejected_for_diffusion_models():
params = SamplingParams(
structured_outputs=StructuredOutputsParams(json=JSON_SCHEMA)
)
with pytest.raises(ValueError, match="not yet supported for diffusion"):
with pytest.raises(VLLMValidationError, match="not yet supported for diffusion"):
params._validate_structured_outputs(
_StubModelConfig(is_diffusion=True),
StructuredOutputsConfig(),
@@ -63,7 +64,7 @@ def test_degenerate_structured_outputs_rejected(structured_outputs, match):
rejected at request validation (-> 400) instead of reaching and crashing
the engine."""
params = SamplingParams(structured_outputs=structured_outputs)
with pytest.raises(ValueError, match=match):
with pytest.raises(VLLMValidationError, match=match):
params._validate_structured_outputs(
_StubModelConfig(is_diffusion=False),
StructuredOutputsConfig(),
+16 -20
View File
@@ -28,7 +28,6 @@ from vllm.engine.protocol import EngineClient
from vllm.entrypoints.chat_utils import load_chat_template
from vllm.entrypoints.launcher import serve_http
from vllm.entrypoints.openai.cli_args import make_arg_parser, validate_parsed_serve_args
from vllm.entrypoints.openai.engine.protocol import GenerationError
from vllm.entrypoints.openai.models.protocol import BaseModelPath
from vllm.entrypoints.openai.models.serving import OpenAIServingModels
from vllm.entrypoints.serve.elastic_ep.middleware import ScalingMiddleware
@@ -42,20 +41,15 @@ from vllm.entrypoints.serve.utils.api_utils import (
)
from vllm.entrypoints.serve.utils.request_logger import RequestLogger
from vllm.entrypoints.serve.utils.server_utils import (
engine_error_handler,
exception_handler,
generation_error_handler,
get_uvicorn_log_config,
http_exception_handler,
lifespan,
log_response,
validation_exception_handler,
vllm_error_handler,
)
from vllm.exceptions import (
VLLMNotFoundError,
VLLMUnprocessableEntityError,
VLLMValidationError,
)
from vllm.exceptions import VLLMError
from vllm.logger import init_logger
from vllm.reasoning import ReasoningParserManager
from vllm.renderers.online_derenderer import OnlineDerenderer
@@ -67,7 +61,6 @@ from vllm.usage.usage_lib import UsageContext
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm.utils.network_utils import is_valid_ipv6_address
from vllm.utils.system_utils import decorate_logs, set_ulimit
from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
from vllm.version import __version__ as VLLM_VERSION
prometheus_multiproc_dir: tempfile.TemporaryDirectory
@@ -291,23 +284,26 @@ def build_app(
allow_headers=args.allowed_headers,
)
# Exception handlers are registered in four layers:
# 1. framework errors raised by FastAPI/Starlette
# 2. vLLM-specific errors dispatched via a single ``VLLMError`` handler
# 3. fallback handlers for raw exceptions not yet migrated to ``VLLMError``
# 4. the raw ``Exception`` handler as a safety net
# Registering specific exception types (rather than only ``Exception``)
# ensures they are handled by ``ExceptionMiddleware`` (inside the Prometheus
# middleware) rather than ``ServerErrorMiddleware`` (outside it), so their
# status codes are recorded correctly.
app.exception_handler(HTTPException)(http_exception_handler)
app.exception_handler(RequestValidationError)(validation_exception_handler)
app.exception_handler(EngineGenerateError)(engine_error_handler)
app.exception_handler(EngineDeadError)(engine_error_handler)
app.exception_handler(GenerationError)(generation_error_handler)
# Register specific exception types so they are handled by
# ExceptionMiddleware (inside the Prometheus middleware) rather than
# ServerErrorMiddleware (outside it). Without this, these exceptions
# propagate through Prometheus as unhandled and get recorded as 5xx
# even though they result in 4xx responses to the client.
app.exception_handler(VLLMValidationError)(exception_handler)
app.exception_handler(VLLMUnprocessableEntityError)(exception_handler)
app.exception_handler(VLLMNotFoundError)(exception_handler)
app.exception_handler(VLLMError)(vllm_error_handler)
# TODO(zqzten): remove these fallback handlers after migration to VLLMError
app.exception_handler(ValueError)(exception_handler)
app.exception_handler(TypeError)(exception_handler)
app.exception_handler(OverflowError)(exception_handler)
app.exception_handler(NotImplementedError)(exception_handler)
app.exception_handler(Exception)(exception_handler)
# Ensure --api-key option from CLI takes precedence over VLLM_API_KEY
+2 -2
View File
@@ -19,7 +19,7 @@ from pydantic import (
from vllm.config.utils import replace
from vllm.entrypoints.chat_utils import make_tool_call_id
from vllm.exceptions import VLLMValidationError
from vllm.exceptions import VLLMServerError, VLLMValidationError
from vllm.logger import init_logger
from vllm.sampling_params import StructuredOutputsParams
from vllm.utils import random_uuid
@@ -404,7 +404,7 @@ class DeltaMessage(OpenAIBaseModel):
return data
class GenerationError(Exception):
class GenerationError(VLLMServerError):
"""raised when finish_reason indicates internal server error (500)"""
def __init__(self, message: str = "Internal server error"):
+18 -5
View File
@@ -28,7 +28,9 @@ def create_error_response(
)
from vllm.exceptions import (
VLLMClientError,
VLLMNotFoundError,
VLLMServerError,
VLLMUnprocessableEntityError,
VLLMValidationError,
)
@@ -45,8 +47,23 @@ def create_error_response(
err_type = "NotFoundError"
status_code = HTTPStatus.NOT_FOUND
param = None
elif isinstance(exc, VLLMClientError):
# Any other client-caused error defaults to 400.
err_type = "BadRequestError"
status_code = HTTPStatus.BAD_REQUEST
param = None
elif isinstance(exc, GenerationError):
err_type = "InternalServerError"
status_code = exc.status_code
param = None
elif isinstance(exc, VLLMServerError):
# Any other server-caused error defaults to 500.
err_type = "InternalServerError"
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
param = None
# Fallback for raw exceptions not yet migrated to VLLMError.
# TODO(zqzten): remove these fallback handlers after migration to VLLMError
elif isinstance(exc, (ValueError, TypeError, OverflowError)):
# Common validation errors from user input
err_type = "BadRequestError"
status_code = HTTPStatus.BAD_REQUEST
param = None
@@ -54,10 +71,6 @@ def create_error_response(
err_type = "NotImplementedError"
status_code = HTTPStatus.NOT_IMPLEMENTED
param = None
elif isinstance(exc, GenerationError):
err_type = "InternalServerError"
status_code = exc.status_code
param = None
elif any(cls.__name__ == "TemplateError" for cls in type(exc).__mro__):
# jinja2.TemplateError and its subclasses (avoid importing jinja2)
err_type = "BadRequestError"
+11 -1
View File
@@ -31,7 +31,7 @@ from vllm.entrypoints.serve.utils.error_response import (
create_error_response,
sanitize_message,
)
from vllm.exceptions import VLLMValidationError
from vllm.exceptions import VLLMError, VLLMValidationError
from vllm.logger import init_logger
from vllm.utils.gc_utils import freeze_gc_heap
from vllm.v1.engine.exceptions import EngineDeadError, EngineGenerateError
@@ -325,6 +325,16 @@ async def log_response(request: Request, call_next):
return response
async def vllm_error_handler(req: Request, exc: VLLMError):
"""Dispatch a vLLM-specific error to the appropriate handler."""
if isinstance(exc, (EngineGenerateError, EngineDeadError)):
return await engine_error_handler(req, exc)
elif isinstance(exc, GenerationError):
return await generation_error_handler(req, exc)
else:
return await exception_handler(req, exc)
async def engine_error_handler(
req: Request, exc: EngineDeadError | EngineGenerateError
):
+21 -3
View File
@@ -6,7 +6,25 @@
from typing import Any
class VLLMValidationError(ValueError):
class VLLMError(Exception):
"""Base class for all vLLM-specific errors.
Subclasses are split into `VLLMClientError` (caused by the request, mapped
to 4xx) and `VLLMServerError` (caused by the server, mapped to 5xx).
Dispatching on this hierarchy lets the entrypoints decide the HTTP status
without relying on raw Python exception types such as `ValueError`.
"""
class VLLMClientError(VLLMError):
"""Base class for errors caused by the client request (4xx)."""
class VLLMServerError(VLLMError):
"""Base class for errors caused by the server (5xx)."""
class VLLMValidationError(VLLMClientError):
"""vLLM-specific validation error for request validation failures.
Args:
@@ -36,7 +54,7 @@ class VLLMValidationError(ValueError):
return f"{base} ({', '.join(extras)})" if extras else base
class VLLMNotFoundError(Exception):
class VLLMNotFoundError(VLLMClientError):
"""vLLM-specific NotFoundError"""
pass
@@ -66,7 +84,7 @@ class LoRAAdapterNotFoundError(VLLMNotFoundError):
return self.message
class VLLMUnprocessableEntityError(ValueError):
class VLLMUnprocessableEntityError(VLLMClientError):
"""vLLM-specific error for unprocessable entity requests.
This exception is raised when the request content is invalid or cannot be
+4 -2
View File
@@ -7,6 +7,8 @@ from typing import TYPE_CHECKING, Literal, TypeAlias
from typing_extensions import NotRequired, TypedDict, assert_never
from vllm.exceptions import VLLMValidationError
if TYPE_CHECKING:
import torch
@@ -284,7 +286,7 @@ which can be passed to `LLMEngine.add_request` or `AsyncLLM.add_request`.
def _validate_enc_input(enc_input: SingletonInput) -> EncoderInput:
if enc_input["type"] == "embeds":
raise ValueError(
raise VLLMValidationError(
"Embedding inputs are not supported for encoder-decoder models"
)
@@ -302,7 +304,7 @@ def _validate_enc_input(enc_input: SingletonInput) -> EncoderInput:
def _validate_dec_input(dec_input: SingletonInput) -> DecoderEngineInput:
if dec_input["type"] == "embeds":
raise ValueError(
raise VLLMValidationError(
"Embedding inputs are not supported for encoder-decoder models"
)
+7 -6
View File
@@ -7,6 +7,7 @@ from typing import Any
import msgspec
from vllm.config import ModelConfig, PoolerConfig
from vllm.exceptions import VLLMValidationError
from vllm.logger import init_logger
from vllm.sampling_params import RequestOutputKind
from vllm.tasks import PoolingTask, check_removed_pooling_task
@@ -145,7 +146,7 @@ class PoolingParams(
invalid_parameters.append(k)
if invalid_parameters:
raise ValueError(
raise VLLMValidationError(
f"Task {self.task} only supports {valid_parameters} "
f"parameters, does not support "
f"{invalid_parameters} parameters"
@@ -170,21 +171,21 @@ class PoolingParams(
valid_range = f"[1, {embedding_size}]"
dimensions_in_range = 1 <= dimensions <= embedding_size
if not model_config.is_matryoshka:
raise ValueError(
raise VLLMValidationError(
f"Model {model_name!r} does not support Matryoshka "
f"embeddings; dimensions must be unset "
f"(received dimensions={dimensions})."
)
if not dimensions_in_range:
raise ValueError(
raise VLLMValidationError(
f"Model {model_name!r} only supports dimensions in "
f"range {valid_range}, got {dimensions}."
)
mds = model_config.matryoshka_dimensions
if mds is not None and dimensions not in mds:
raise ValueError(
raise VLLMValidationError(
f"Model {model_name!r} only supports Matryoshka "
f"dimensions {str(mds)}, got {dimensions}."
)
@@ -208,7 +209,7 @@ class PoolingParams(
invalid_parameters.append(k)
if invalid_parameters:
raise ValueError(
raise VLLMValidationError(
f"Task {self.task!r} only supports {valid_parameters} "
f"parameters, does not support "
f"{invalid_parameters} parameters"
@@ -231,7 +232,7 @@ class PoolingParams(
def __post_init__(self) -> None:
check_removed_pooling_task(self.task)
if self.output_kind != RequestOutputKind.FINAL_ONLY:
raise ValueError(
raise VLLMValidationError(
"For pooling output_kind has to be FINAL_ONLY, "
f"got {self.output_kind!r}"
)
+40 -32
View File
@@ -100,12 +100,12 @@ class StructuredOutputsParams:
]
)
if count > 1:
raise ValueError(
raise VLLMValidationError(
"You can only use one kind of structured outputs constraint "
f"but multiple are specified: {self.__dict__}"
)
if count < 1:
raise ValueError(
raise VLLMValidationError(
"You must use one kind of structured outputs constraint "
f"but none are specified: {self.__dict__}"
)
@@ -166,13 +166,13 @@ class RepetitionDetectionParams:
or self.min_pattern_size < 0
or self.min_pattern_size > self.max_pattern_size
):
raise ValueError(
raise VLLMValidationError(
"max_pattern_size, min_pattern_size must be >=0, "
"with min_pattern_size <= max_pattern_size. "
"Set both to 0 to disable repetitive pattern detection."
)
if self.max_pattern_size > 0 and self.min_count < 2:
raise ValueError(
raise VLLMValidationError(
"min_count must be >= 2 to detect repetitive patterns "
"in engine output. If you do not wish to detect repetitive "
"patterns, set max_pattern_size to 0."
@@ -514,31 +514,33 @@ class SamplingParams(
def _verify_args(self) -> None:
if not isinstance(self.n, int):
raise ValueError(f"n must be an int, but is of type {type(self.n)}")
raise VLLMValidationError(
f"n must be an int, but is of type {type(self.n)}"
)
if self.n < 1:
raise ValueError(f"n must be at least 1, got {self.n}.")
raise VLLMValidationError(f"n must be at least 1, got {self.n}.")
max_n = envs.VLLM_MAX_N_SEQUENCES
if self.n > max_n:
raise ValueError(
raise VLLMValidationError(
f"n must be at most {max_n}, got {self.n}. "
"To increase this limit, set the VLLM_MAX_N_SEQUENCES "
"environment variable."
)
if not -2.0 <= self.presence_penalty <= 2.0:
raise ValueError(
raise VLLMValidationError(
f"presence_penalty must be in [-2, 2], got {self.presence_penalty}."
)
if not -2.0 <= self.frequency_penalty <= 2.0:
raise ValueError(
raise VLLMValidationError(
f"frequency_penalty must be in [-2, 2], got {self.frequency_penalty}."
)
if not math.isfinite(self.repetition_penalty):
raise ValueError(
raise VLLMValidationError(
"repetition_penalty must be a finite number, "
f"got {self.repetition_penalty}."
)
if self.repetition_penalty <= 0.0:
raise ValueError(
raise VLLMValidationError(
"repetition_penalty must be greater than zero, got "
f"{self.repetition_penalty}."
)
@@ -568,15 +570,15 @@ class SamplingParams(
)
# quietly accept -1 as disabled, but prefer 0
if self.top_k < -1:
raise ValueError(
raise VLLMValidationError(
f"top_k must be 0 (disable), or at least 1, got {self.top_k}."
)
if not isinstance(self.top_k, int):
raise TypeError(
raise VLLMValidationError(
f"top_k must be an integer, got {type(self.top_k).__name__}"
)
if not 0.0 <= self.min_p <= 1.0:
raise ValueError(f"min_p must be in [0, 1], got {self.min_p}.")
raise VLLMValidationError(f"min_p must be in [0, 1], got {self.min_p}.")
if self.max_tokens is not None and self.max_tokens < 1:
raise VLLMValidationError(
f"max_tokens must be at least 1, got {self.max_tokens}.",
@@ -584,11 +586,11 @@ class SamplingParams(
value=self.max_tokens,
)
if self.min_tokens < 0:
raise ValueError(
raise VLLMValidationError(
f"min_tokens must be greater than or equal to 0, got {self.min_tokens}."
)
if self.max_tokens is not None and self.min_tokens > self.max_tokens:
raise ValueError(
raise VLLMValidationError(
f"min_tokens must be less than or equal to "
f"max_tokens={self.max_tokens}, got {self.min_tokens}."
)
@@ -617,27 +619,29 @@ class SamplingParams(
)
assert isinstance(self.stop_token_ids, list)
if not all(isinstance(st_id, int) for st_id in self.stop_token_ids):
raise ValueError(
raise VLLMValidationError(
f"stop_token_ids must contain only integers, got {self.stop_token_ids}."
)
assert isinstance(self.stop, list)
if any(not stop_str for stop_str in self.stop):
raise ValueError("stop cannot contain an empty string.")
raise VLLMValidationError("stop cannot contain an empty string.")
if self.stop and not self.detokenize:
raise ValueError(
raise VLLMValidationError(
"stop strings are only supported when detokenize is True. "
"Set detokenize=True to use stop."
)
assert isinstance(self.bad_words, list)
if any(not bad_word for bad_word in self.bad_words):
raise ValueError(
raise VLLMValidationError(
f"bad_words cannot contain an empty string. "
f"Got bad_words={self.bad_words}"
)
def _verify_greedy_sampling(self) -> None:
if self.n > 1:
raise ValueError(f"n must be 1 when using greedy sampling, got {self.n}.")
raise VLLMValidationError(
f"n must be 1 when using greedy sampling, got {self.n}."
)
def update_from_generation_config(
self,
@@ -889,7 +893,7 @@ class SamplingParams(
# Some sampling parameters are not yet compatible with spec decoding.
if self.min_p > _SAMPLING_EPS or self.logit_bias:
raise ValueError(
raise VLLMValidationError(
"The min_p and logit_bias sampling parameters "
"are not yet supported with speculative decoding."
)
@@ -910,7 +914,7 @@ class SamplingParams(
or self.bad_words
or self.allowed_token_ids
):
raise ValueError(
raise VLLMValidationError(
"The temperature, min_p, seed, min_tokens, logit_bias, "
"bad_words, and allowed_token_ids sampling parameters "
"are not yet supported with diffusion models."
@@ -930,7 +934,7 @@ class SamplingParams(
# rather than sampling left-to-right, which the grammar FSM
# requires. Without this check, requests fail mid-generation
# with an FSM rejection (HTTP 500). See issue #45436.
raise ValueError(
raise VLLMValidationError(
"Structured outputs are not yet supported for diffusion "
"language models. Remove the structured output constraint "
"(e.g. `response_format`, `structured_outputs`) from the "
@@ -938,7 +942,7 @@ class SamplingParams(
)
if tokenizer is None:
raise ValueError(
raise VLLMValidationError(
"Structured outputs requires a tokenizer so it can't be used with 'skip_tokenizer_init'" # noqa: E501
)
@@ -952,7 +956,7 @@ class SamplingParams(
if backend != _backend and not (
backend == "auto" and self.structured_outputs._backend_was_auto
):
raise ValueError(
raise VLLMValidationError(
"Request-level structured output backend selection is not "
f"supported. The request specified '{_backend}', but vLLM "
f"was initialised with '{backend}'. This error can be "
@@ -967,7 +971,7 @@ class SamplingParams(
and not self.structured_outputs.choice
):
# It is invalid for choice to be an empty list
raise ValueError(
raise VLLMValidationError(
f"Choice '{self.structured_outputs.choice}' cannot be an empty list" # noqa: E501
)
# Reject empty string grammar early to avoid engine-side crashes
@@ -975,16 +979,20 @@ class SamplingParams(
isinstance(self.structured_outputs.grammar, str)
and self.structured_outputs.grammar.strip() == ""
):
raise ValueError("structured_outputs.grammar cannot be an empty string")
raise VLLMValidationError(
"structured_outputs.grammar cannot be an empty string"
)
# Reject empty string json schema early to avoid engine-side crashes
if (
isinstance(self.structured_outputs.json, str)
and self.structured_outputs.json.strip() == ""
):
raise ValueError("structured_outputs.json cannot be an empty string")
raise VLLMValidationError(
"structured_outputs.json cannot be an empty string"
)
# Reject json_object=False early to avoid engine-side crashes
if self.structured_outputs.json_object is False:
raise ValueError(
raise VLLMValidationError(
"structured_outputs.json_object must be True if set; omit "
"structured_outputs to disable structured outputs"
)
@@ -1006,7 +1014,7 @@ class SamplingParams(
validate_xgrammar_grammar(self)
elif backend.startswith("guidance"):
if _is_non_tekken_mistral(tokenizer=tokenizer):
raise ValueError(
raise VLLMValidationError(
"Non-tekken Mistral tokenizers are not supported for the 'guidance'"
" structured output backend. Please either use a more recent "
"Mistral model, the ['xgrammar', 'outlines'] "
@@ -1026,7 +1034,7 @@ class SamplingParams(
elif backend == "lm-format-enforcer":
# lm format enforcer backend
if is_mistral_tokenizer(tokenizer):
raise ValueError(
raise VLLMValidationError(
"Mistral tokenizer is not supported for the 'lm-format-enforcer' "
"structured output backend. Please use ['xgrammar', 'outlines'] "
"backends or tokenizer_mode='hf' instead."
+6 -5
View File
@@ -21,6 +21,7 @@ from vllm.distributed.weight_transfer.base import (
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.protocol import EngineClient, StreamingInput
from vllm.entrypoints.serve.elastic_ep.middleware import set_scaling_elastic_ep
from vllm.exceptions import VLLMClientError, VLLMValidationError
from vllm.inputs import EngineInput, PromptType
from vllm.logger import init_logger
from vllm.lora.request import LoRARequest
@@ -309,7 +310,7 @@ class AsyncLLM(EngineClient):
and not is_pooling
and params.prompt_logprobs
):
raise ValueError(
raise VLLMValidationError(
"--kv-sharing-fast-prefill produces incorrect logprobs for "
"prompt tokens, please disable it when the requests need "
"prompt logprobs"
@@ -476,7 +477,7 @@ class AsyncLLM(EngineClient):
)
req.external_req_id = request_id
if req.prompt_embeds is not None:
raise ValueError(
raise VLLMValidationError(
"prompt_embeds not supported for streaming inputs"
)
prompt_text, _, _ = extract_prompt_components(
@@ -512,7 +513,7 @@ class AsyncLLM(EngineClient):
or params.output_kind == RequestOutputKind.FINAL_ONLY
or params.stop
):
raise ValueError(
raise VLLMValidationError(
"Input streaming not currently supported "
"for pooling models, n > 1, request_kind = FINAL_ONLY "
"or with stop strings."
@@ -604,7 +605,7 @@ class AsyncLLM(EngineClient):
raise
# Request validation error.
except ValueError as e:
except VLLMClientError as e:
if self.log_requests:
logger.info("Request %s failed (bad request): %s.", request_id, e)
raise
@@ -869,7 +870,7 @@ class AsyncLLM(EngineClient):
raise
# Request validation error.
except ValueError:
except VLLMClientError:
if self.log_requests:
logger.info("Request %s failed (bad request).", request_id)
raise
+5 -2
View File
@@ -1,12 +1,15 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
class EngineGenerateError(Exception):
from vllm.exceptions import VLLMServerError
class EngineGenerateError(VLLMServerError):
"""Raised when a AsyncLLM.generate() fails. Recoverable."""
pass
class EngineDeadError(Exception):
class EngineDeadError(VLLMServerError):
"""Raised when the EngineCore dies. Unrecoverable."""
def __init__(self, *args, suppress_context: bool = False, **kwargs):
+15 -12
View File
@@ -7,6 +7,7 @@ from typing import Any, Literal
import vllm.envs as envs
from vllm.config import VllmConfig
from vllm.exceptions import VLLMValidationError
from vllm.inputs import (
EngineInput,
PromptType,
@@ -90,7 +91,7 @@ class InputProcessor:
task for task in supported_tasks if task in GENERATION_TASKS
]
if not supported_generation_tasks:
raise ValueError("This model does not support generation")
raise VLLMValidationError("This model does not support generation")
params.verify(
self.model_config,
@@ -104,13 +105,13 @@ class InputProcessor:
self.vllm_config.reasoning_config is None
or not self.vllm_config.reasoning_config.enabled
):
raise ValueError(
raise VLLMValidationError(
"thinking_token_budget is set but reasoning_config is "
"not configured. Please set --reasoning-parser "
"and/or --reasoning-config to use thinking_token_budget."
)
if self.use_v2_model_runner:
raise ValueError(
raise VLLMValidationError(
"thinking_token_budget is not yet supported by the V2 "
"model runner. Run vLLM with VLLM_USE_V2_MODEL_RUNNER=0 "
"to use thinking_token_budget."
@@ -120,7 +121,7 @@ class InputProcessor:
task for task in supported_tasks if task in POOLING_TASKS
]
if not supported_pooling_tasks:
raise ValueError("This model does not support pooling")
raise VLLMValidationError("This model does not support pooling")
if params.task is None:
if "token_embed" in supported_pooling_tasks:
@@ -131,7 +132,7 @@ class InputProcessor:
params.task = "plugin"
if params.task not in supported_pooling_tasks:
raise ValueError(
raise VLLMValidationError(
f"Unsupported task: {params.task!r} "
f"Supported tasks: {supported_pooling_tasks}"
)
@@ -149,7 +150,7 @@ class InputProcessor:
# LoRA request passed in while LoRA is not enabled
if not self.lora_config:
raise ValueError(
raise VLLMValidationError(
f"Got lora_request {lora_request} but LoRA is not enabled!"
)
@@ -261,7 +262,7 @@ class InputProcessor:
dp_local_size = parallel_config.data_parallel_size_local
num_ranks = dp_local_size if parallel_config.local_engines_only else dp_size
if data_parallel_rank is not None and not (0 <= data_parallel_rank < num_ranks):
raise ValueError(
raise VLLMValidationError(
f"data_parallel_rank {data_parallel_rank} "
f"is out of range [0, {num_ranks})."
)
@@ -393,7 +394,7 @@ class InputProcessor:
return
if prompt_len == 0 and prompt_type == "decoder":
raise ValueError(f"The {prompt_type} prompt cannot be empty")
raise VLLMValidationError(f"The {prompt_type} prompt cannot be empty")
model_config = self.model_config
max_prompt_len = (
@@ -415,7 +416,7 @@ class InputProcessor:
"number of text tokens."
)
raise ValueError(
raise VLLMValidationError(
f"The {prompt_type} prompt (length {prompt_len}) is "
f"longer than the maximum model length of {max_prompt_len}. "
f"{suggestion}"
@@ -425,7 +426,7 @@ class InputProcessor:
"Make sure that `max_model_len` is no smaller than the "
"number of text tokens (prompt + requested output tokens)."
)
raise ValueError(
raise VLLMValidationError(
f"The {prompt_type} prompt (length {prompt_len}) plus the number of "
f"requested output tokens (at least 1) is longer than the maximum "
f"model length of {max_prompt_len}. {suggestion}"
@@ -457,7 +458,7 @@ class InputProcessor:
for mm_position in mm_positions:
num_embeds = mm_position.get_num_embeds()
if num_embeds > self.mm_encoder_cache_size:
raise ValueError(
raise VLLMValidationError(
f"The {prompt_type} prompt contains a(n) {modality} item "
f"with {num_embeds} embedding tokens, which exceeds the "
f"pre-allocated encoder cache size "
@@ -481,7 +482,9 @@ class InputProcessor:
# truly out-of-vocabulary.
model_vocab_size = model_config.get_vocab_size()
if max_input_id > max(tokenizer.max_token_id, model_vocab_size - 1):
raise ValueError(f"Token id {max_input_id} is out of vocabulary")
raise VLLMValidationError(
f"Token id {max_input_id} is out of vocabulary"
)
def _validate_model_inputs(
self,
+7 -1
View File
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING
import torch
from vllm.exceptions import VLLMValidationError
from vllm.logger import init_logger
from vllm.logits_process import LogitsProcessor as RequestLogitsProcessor
from vllm.sampling_params import SamplingParams
@@ -228,7 +229,12 @@ def validate_logits_processors_parameters(
tuple(logits_processors) if logits_processors is not None else None
)
for logits_procs in cached_load_custom_logitsprocs(logits_processors):
logits_procs.validate_params(sampling_params)
try:
logits_procs.validate_params(sampling_params)
except ValueError as e:
# Legacy custom logitsprocs may still raise ValueError from
# validate_params; convert for backward compatibility.
raise VLLMValidationError(str(e)) from e
class AdapterLogitsProcessor(LogitsProcessor):
+3 -1
View File
@@ -62,7 +62,9 @@ class LogitsProcessor(ABC):
def validate_params(cls, sampling_params: SamplingParams):
"""Validate sampling params for this logits processor.
Raise ValueError for invalid ones.
Raise ``VLLMValidationError`` (preferred) / ``ValueError`` (backward compatible)
for invalid params. Bare ``ValueError`` is converted to ``VLLMValidationError``
at the engine boundary so online serving returns HTTP 400.
"""
return None