From 48bada6ea49ad7f3ecbe03128aa76562089c8b00 Mon Sep 17 00:00:00 2001 From: Tarun Kumar Date: Tue, 11 Aug 2026 07:01:19 +0530 Subject: [PATCH] Fix chat completion 500 on non-object JSON bodies (#51654) Signed-off-by: Tarun Kumar --- .../test_non_object_body_validation.py | 38 +++++++++++++++++++ .../openai/chat_completion/protocol.py | 27 +++++++++---- 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 tests/entrypoints/openai/chat_completion/test_non_object_body_validation.py diff --git a/tests/entrypoints/openai/chat_completion/test_non_object_body_validation.py b/tests/entrypoints/openai/chat_completion/test_non_object_body_validation.py new file mode 100644 index 00000000000..7dba0018610 --- /dev/null +++ b/tests/entrypoints/openai/chat_completion/test_non_object_body_validation.py @@ -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": "", + } + ) diff --git a/vllm/entrypoints/openai/chat_completion/protocol.py b/vllm/entrypoints/openai/chat_completion/protocol.py index d25b3886ad5..eda0f5a3bae 100644 --- a/vllm/entrypoints/openai/chat_completion/protocol.py +++ b/vllm/entrypoints/openai/chat_completion/protocol.py @@ -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)