| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | """Non-object JSON bodies must fail validation cleanly (4xx), not AttributeError (500). |
| 5 | |
| 6 | mode=before validators that call data.get(...) without an isinstance(data, dict) |
| 7 | guard raise AttributeError for string/list/scalar bodies and surface as HTTP 500. |
| 8 | |
| 9 | This extends the chat completion coverage added in #51654 to the remaining |
| 10 | request models whose before-validators were missing the same guard. |
| 11 | """ |
| 12 | |
| 13 | import pytest |
| 14 | from pydantic import ValidationError |
| 15 | |
| 16 | from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest |
| 17 | from vllm.entrypoints.openai.completion.protocol import CompletionRequest |
| 18 | from vllm.entrypoints.openai.responses.protocol import ResponsesRequest |
| 19 | from vllm.entrypoints.pooling.classify.protocol import ClassificationChatRequest |
| 20 | from vllm.entrypoints.pooling.embed.protocol import EmbeddingChatRequest |
| 21 | from vllm.entrypoints.pooling.pooling.protocol import PoolingChatRequest |
| 22 | from vllm.entrypoints.scale_out.token_in_token_out.protocol import GenerateRequest |
| 23 | from vllm.entrypoints.serve.tokenize.protocol import TokenizeChatRequest |
| 24 | from vllm.entrypoints.speech_to_text.transcription.protocol import TranscriptionRequest |
| 25 | from vllm.entrypoints.speech_to_text.translation.protocol import TranslationRequest |
| 26 | from vllm.exceptions import VLLMValidationError |
| 27 | |
| 28 | pytestmark = pytest.mark.skip_global_cleanup |
| 29 | |
| 30 | REQUEST_MODELS = [ |
| 31 | CompletionRequest, |
| 32 | ResponsesRequest, |
| 33 | EmbeddingChatRequest, |
| 34 | ClassificationChatRequest, |
| 35 | PoolingChatRequest, |
| 36 | TokenizeChatRequest, |
| 37 | TranscriptionRequest, |
| 38 | TranslationRequest, |
| 39 | ] |
| 40 | |
| 41 | |
| 42 | @pytest.mark.parametrize("request_model", REQUEST_MODELS, ids=lambda m: m.__name__) |
| 43 | @pytest.mark.parametrize( |
| 44 | "payload", |
| 45 | [ |
| 46 | "this is not valid json{{{", |
| 47 | ["not", "an", "object"], |
| 48 | 42, |
| 49 | None, |
| 50 | True, |
| 51 | ], |
| 52 | ) |
| 53 | def test_request_models_reject_non_object_body(request_model, payload): |
| 54 | with pytest.raises(ValidationError): |
| 55 | request_model.model_validate(payload) |
| 56 | |
| 57 | |
| 58 | def test_completion_request_still_validates_dict_bodies(): |
| 59 | """The guard must not swallow real field-level errors on object bodies.""" |
| 60 | with pytest.raises(VLLMValidationError, match="prompt"): |
| 61 | CompletionRequest.model_validate({"model": "qwen", "prompt": ""}) |
| 62 | |
| 63 | |
| 64 | def test_tokenize_chat_request_still_validates_dict_bodies(): |
| 65 | with pytest.raises(VLLMValidationError, match="add_generation_prompt"): |
| 66 | TokenizeChatRequest.model_validate( |
| 67 | { |
| 68 | "model": "qwen", |
| 69 | "messages": [{"role": "user", "content": "hello"}], |
| 70 | "continue_final_message": True, |
| 71 | "add_generation_prompt": True, |
| 72 | } |
| 73 | ) |
| 74 | |
| 75 | |
| 76 | CACHE_SALT_MODELS = [ |
| 77 | (CompletionRequest, {"prompt": "hello"}), |
| 78 | (ChatCompletionRequest, {"messages": [{"role": "user", "content": "hello"}]}), |
| 79 | (ResponsesRequest, {"input": "hello"}), |
| 80 | (GenerateRequest, {"token_ids": [1], "sampling_params": {}}), |
| 81 | ] |
| 82 | |
| 83 | |
| 84 | @pytest.mark.parametrize( |
| 85 | ("request_model", "payload"), |
| 86 | CACHE_SALT_MODELS, |
| 87 | ids=[m.__name__ for m, _ in CACHE_SALT_MODELS], |
| 88 | ) |
| 89 | def test_request_models_reject_forbidden_cache_salt(request_model, payload): |
| 90 | with pytest.raises(VLLMValidationError, match="cache_salt"): |
| 91 | request_model.model_validate({**payload, "cache_salt": "/"}) |
| 92 | |
| 93 | |
| 94 | def test_request_models_reject_overlong_cache_salt(): |
| 95 | with pytest.raises(VLLMValidationError, match="cache_salt"): |
| 96 | CompletionRequest.model_validate({"prompt": "hello", "cache_salt": "a" * 129}) |
| 97 | |
| 98 | |
| 99 | def test_request_models_accept_safe_cache_salt(): |
| 100 | request = CompletionRequest.model_validate( |
| 101 | {"prompt": "hello", "cache_salt": "safe-cache_salt-123"} |
| 102 | ) |
| 103 | assert request.cache_salt == "safe-cache_salt-123" |