Fix chat completion 500 on non-object JSON bodies (#51654)

Signed-off-by: Tarun Kumar <[email protected]>
This commit is contained in:
Tarun Kumar
2026-08-11 01:31:19 +00:00
committed by GitHub
parent f1e921b6d6
commit 48bada6ea4
2 changed files with 58 additions and 7 deletions
@@ -0,0 +1,38 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Non-object JSON bodies must fail validation cleanly (4xx), not AttributeError (500).
mode=before validators that call data.get(...) without an isinstance(data, dict)
guard raise AttributeError for string/list/scalar bodies and surface as HTTP 500.
"""
import pytest
from pydantic import ValidationError
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
@pytest.mark.parametrize(
"payload",
[
"this is not valid json{{{",
["not", "an", "object"],
42,
None,
],
)
def test_chat_completion_request_rejects_non_object_body(payload):
with pytest.raises(ValidationError):
ChatCompletionRequest.model_validate(payload)
def test_chat_completion_request_cache_salt_still_validated_on_dict():
with pytest.raises(ValidationError, match="cache_salt"):
ChatCompletionRequest.model_validate(
{
"model": "qwen",
"messages": [{"role": "user", "content": "hello"}],
"cache_salt": "",
}
)
@@ -745,6 +745,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
@model_validator(mode="before")
@classmethod
def validate_response_format(cls, data):
if not isinstance(data, dict):
return data
response_format = data.get("response_format")
if response_format is None:
return data
@@ -776,6 +778,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
@model_validator(mode="before")
@classmethod
def validate_stream_options(cls, data):
if not isinstance(data, dict):
return data
if data.get("stream_options") and not data.get("stream"):
raise VLLMValidationError(
"Stream options can only be defined when `stream=True`.",
@@ -787,6 +791,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
@model_validator(mode="before")
@classmethod
def check_logprobs(cls, data):
if not isinstance(data, dict):
return data
if data.get("logprob_token_ids") and data.get("use_beam_search"):
raise VLLMValidationError(
"`logprob_token_ids` is not supported with beam search.",
@@ -845,6 +851,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
def check_structured_outputs_count(cls, data):
if isinstance(data, ValueError):
raise data
if not isinstance(data, dict):
return data
if data.get("structured_outputs", None) is None:
return data
@@ -970,6 +978,8 @@ class ChatCompletionRequest(OpenAIBaseModel):
@model_validator(mode="before")
@classmethod
def check_generation_prompt(cls, data):
if not isinstance(data, dict):
return data
if data.get("continue_final_message") and data.get("add_generation_prompt"):
raise VLLMValidationError(
"Cannot set both `continue_final_message` and "
@@ -1105,6 +1115,8 @@ class BatchChatCompletionRequest(OpenAIBaseModel):
def check_batch_mode(cls, data: Any) -> Any:
if isinstance(data, BatchChatCompletionRequest):
data = data.model_dump(exclude_unset=True)
if not isinstance(data, dict):
return data
if data.get("use_beam_search"):
raise VLLMValidationError(
"Batch chat completions do not support beam search. "
@@ -1117,13 +1129,14 @@ class BatchChatCompletionRequest(OpenAIBaseModel):
parameter="logprob_token_ids",
)
response_format = data.get("response_format")
rf_type = (
response_format.get("type")
if isinstance(response_format, dict)
else getattr(response_format, "type", None)
)
if rf_type == "structural_tag":
validate_structural_tag_response_format(response_format)
if response_format is not None:
rf_type = (
response_format.get("type")
if isinstance(response_format, dict)
else getattr(response_format, "type", None)
)
if rf_type == "structural_tag":
validate_structural_tag_response_format(response_format)
if (structured_outputs := data.get("structured_outputs")) is not None:
validate_structured_outputs_structural_tag(structured_outputs)
n = data.get("n", 1)