| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | """Unit tests for ResponsesRequest.to_sampling_params() parameter mapping.""" |
| 5 | |
| 6 | import pytest |
| 7 | import torch |
| 8 | from openai.types.responses.response_format_text_json_schema_config import ( |
| 9 | ResponseFormatTextJSONSchemaConfig, |
| 10 | ) |
| 11 | from pydantic import ValidationError |
| 12 | |
| 13 | 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 | |
| 21 | class TestResponsesRequestSamplingParams: |
| 22 | """Test that ResponsesRequest correctly maps parameters to SamplingParams.""" |
| 23 | |
| 24 | def test_basic_sampling_params(self): |
| 25 | """Test basic sampling parameters are correctly mapped.""" |
| 26 | request = ResponsesRequest( |
| 27 | model="test-model", |
| 28 | input="test input", |
| 29 | temperature=0.8, |
| 30 | top_p=0.95, |
| 31 | top_k=50, |
| 32 | max_output_tokens=100, |
| 33 | ) |
| 34 | |
| 35 | sampling_params = request.to_sampling_params(default_max_tokens=1000) |
| 36 | |
| 37 | assert sampling_params.temperature == 0.8 |
| 38 | assert sampling_params.top_p == 0.95 |
| 39 | assert sampling_params.top_k == 50 |
| 40 | assert sampling_params.max_tokens == 100 |
| 41 | |
| 42 | def test_extra_sampling_params(self): |
| 43 | """Test extra sampling parameters are correctly mapped.""" |
| 44 | request = ResponsesRequest( |
| 45 | model="test-model", |
| 46 | input="test input", |
| 47 | repetition_penalty=1.2, |
| 48 | seed=42, |
| 49 | stop=["END", "STOP"], |
| 50 | ignore_eos=True, |
| 51 | vllm_xargs={"custom": "value"}, |
| 52 | ) |
| 53 | |
| 54 | sampling_params = request.to_sampling_params(default_max_tokens=1000) |
| 55 | |
| 56 | assert sampling_params.repetition_penalty == 1.2 |
| 57 | assert sampling_params.seed == 42 |
| 58 | assert sampling_params.stop == ["END", "STOP"] |
| 59 | assert sampling_params.ignore_eos is True |
| 60 | assert sampling_params.extra_args == {"custom": "value"} |
| 61 | |
| 62 | def test_stop_string_conversion(self): |
| 63 | """Test that single stop string is converted to list.""" |
| 64 | request = ResponsesRequest( |
| 65 | model="test-model", |
| 66 | input="test input", |
| 67 | stop="STOP", |
| 68 | ) |
| 69 | |
| 70 | sampling_params = request.to_sampling_params(default_max_tokens=1000) |
| 71 | |
| 72 | assert sampling_params.stop == ["STOP"] |
| 73 | |
| 74 | def test_default_values(self): |
| 75 | """Test default values for optional parameters.""" |
| 76 | request = ResponsesRequest( |
| 77 | model="test-model", |
| 78 | input="test input", |
| 79 | ) |
| 80 | |
| 81 | sampling_params = request.to_sampling_params(default_max_tokens=1000) |
| 82 | |
| 83 | assert sampling_params.repetition_penalty == 1.0 # None → 1.0 |
| 84 | assert sampling_params.stop == [] # Empty list |
| 85 | assert sampling_params.extra_args == {} # Empty dict |
| 86 | |
| 87 | def test_seed_bounds_validation(self): |
| 88 | """Test that seed values outside torch.long bounds are rejected.""" |
| 89 | # Test seed below minimum |
| 90 | with pytest.raises(ValidationError) as exc_info: |
| 91 | ResponsesRequest( |
| 92 | model="test-model", |
| 93 | input="test input", |
| 94 | seed=torch.iinfo(torch.long).min - 1, |
| 95 | ) |
| 96 | assert "greater_than_equal" in str(exc_info.value).lower() |
| 97 | |
| 98 | # Test seed above maximum |
| 99 | with pytest.raises(ValidationError) as exc_info: |
| 100 | ResponsesRequest( |
| 101 | model="test-model", |
| 102 | input="test input", |
| 103 | seed=torch.iinfo(torch.long).max + 1, |
| 104 | ) |
| 105 | assert "less_than_equal" in str(exc_info.value).lower() |
| 106 | |
| 107 | # Test valid seed at boundaries |
| 108 | request_min = ResponsesRequest( |
| 109 | model="test-model", |
| 110 | input="test input", |
| 111 | seed=torch.iinfo(torch.long).min, |
| 112 | ) |
| 113 | assert request_min.seed == torch.iinfo(torch.long).min |
| 114 | |
| 115 | request_max = ResponsesRequest( |
| 116 | model="test-model", |
| 117 | input="test input", |
| 118 | seed=torch.iinfo(torch.long).max, |
| 119 | ) |
| 120 | assert request_max.seed == torch.iinfo(torch.long).max |
| 121 | |
| 122 | def test_structured_outputs_passed_through(self): |
| 123 | """Test that structured_outputs field is passed to SamplingParams.""" |
| 124 | structured_outputs = StructuredOutputsParams(grammar="root ::= 'hello'") |
| 125 | request = ResponsesRequest( |
| 126 | model="test-model", |
| 127 | input="test input", |
| 128 | structured_outputs=structured_outputs, |
| 129 | ) |
| 130 | |
| 131 | sampling_params = request.to_sampling_params(default_max_tokens=1000) |
| 132 | |
| 133 | assert sampling_params.structured_outputs is not None |
| 134 | assert sampling_params.structured_outputs.grammar == "root ::= 'hello'" |
| 135 | |
| 136 | def test_text_format_json_object_enables_structured_outputs(self): |
| 137 | """text.format json_object enables structured outputs for sampling.""" |
| 138 | request = ResponsesRequest( |
| 139 | model="test-model", |
| 140 | input="test input", |
| 141 | text=ResponseTextConfig.model_validate({"format": {"type": "json_object"}}), |
| 142 | ) |
| 143 | |
| 144 | sampling_params = request.to_sampling_params(default_max_tokens=1000) |
| 145 | |
| 146 | assert sampling_params.structured_outputs is not None |
| 147 | assert sampling_params.structured_outputs.json_object is True |
| 148 | assert sampling_params.structured_outputs.json is None |
| 149 | assert request.structured_outputs is None |
| 150 | |
| 151 | def test_structured_outputs_and_json_schema_conflict(self): |
| 152 | """Test that specifying both structured_outputs and json_schema raises.""" |
| 153 | structured_outputs = StructuredOutputsParams(grammar="root ::= 'hello'") |
| 154 | text_config = ResponseTextConfig() |
| 155 | text_config.format = ResponseFormatTextJSONSchemaConfig( |
| 156 | type="json_schema", |
| 157 | name="test", |
| 158 | schema={"type": "object"}, |
| 159 | ) |
| 160 | request = ResponsesRequest( |
| 161 | model="test-model", |
| 162 | input="test input", |
| 163 | structured_outputs=structured_outputs, |
| 164 | text=text_config, |
| 165 | ) |
| 166 | |
| 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( |
| 171 | exc_info.value |
| 172 | ) |