[Bugfix] Raise `VLLMValidationError` from structured output validators (#52394)
Signed-off-by: Jeffrey Wang <jeffreywang@anyscale.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jeffrey Wang committed
Aug 16, 2026 at 01:04 UTC
41f12a0daf193c80f2e748f6a65f0735623be3a5
8 files changed
+109
-30
tests/entrypoints/llm/test_struct_output_generate.py
+5
-2
@@ -13,6 +13,7 @@ from pydantic import BaseModel
13
14
from tests.reasoning.utils import run_reasoning_extraction
15
from vllm.config import StructuredOutputsConfig
16
+from vllm.exceptions import VLLMValidationError
17
from vllm.outputs import RequestOutput
18
from vllm.platforms import current_platform
19
from vllm.reasoning.abs_reasoning_parsers import ReasoningParserManager
@@ -326,7 +327,7 @@ def test_structured_output(
327
)
328
if backend.startswith("xgrammar"):
329
with pytest.raises(
329
- ValueError,
330
+ VLLMValidationError,
331
match="The provided JSON schema contains features "
332
"not supported by xgrammar.",
333
):
@@ -453,7 +454,9 @@ def test_structured_output(
454
max_tokens=1000,
455
structured_outputs=StructuredOutputsParams(grammar="not a grammar"),
456
)
456
- with pytest.raises(ValueError, match="Failed to convert the grammar "):
457
+ with pytest.raises(
458
+ VLLMValidationError, match="Failed to convert the grammar "
459
+ ):
460
runner.llm.generate(
461
(
462
"Generate a sql statement that selects col_1 from "
tests/v1/structured_output/test_validation.py
+58
-1
@@ -5,7 +5,7 @@
5
import pytest
6
7
from vllm.config import StructuredOutputsConfig
8
-from vllm.exceptions import VLLMValidationError
8
+from vllm.exceptions import VLLMClientError, VLLMValidationError
9
from vllm.sampling_params import SamplingParams, StructuredOutputsParams
10
11
pytestmark = pytest.mark.cpu_test
@@ -103,3 +103,60 @@ def test_regex_with_nul_byte_rejected(regex):
103
104
with pytest.raises(ValueError, match="NUL"):
105
validate_xgrammar_grammar(params)
106
+
107
+
108
+INVALID_JSON_SCHEMA = {"type": "object", "properties": {"name": {"type": "str"}}}
109
+
110
+
111
+@pytest.mark.parametrize(
112
+ "backend, structured_outputs",
113
+ [
114
+ ("xgrammar", StructuredOutputsParams(json=INVALID_JSON_SCHEMA)),
115
+ ("outlines", StructuredOutputsParams(json=INVALID_JSON_SCHEMA)),
116
+ ("auto", StructuredOutputsParams(json=INVALID_JSON_SCHEMA)),
117
+ ("auto", StructuredOutputsParams(json='{"type": ')),
118
+ ("xgrammar", StructuredOutputsParams(grammar="not a grammar")),
119
+ ("guidance", StructuredOutputsParams(grammar="not a grammar")),
120
+ ("lm-format-enforcer", StructuredOutputsParams(grammar="not a grammar")),
121
+ ("outlines", StructuredOutputsParams(regex="(")),
122
+ ("guidance", StructuredOutputsParams(structural_tag='{"nope": 1}')),
123
+ ],
124
+)
125
+def test_unsupported_grammar_is_a_client_error(backend, structured_outputs):
126
+ """Only `VLLMClientError` survives `AsyncLLM.generate` untouched; anything else
127
+ is wrapped in `EngineGenerateError` and served as a 500 instead of a 400."""
128
+ params = SamplingParams(structured_outputs=structured_outputs)
129
+ with pytest.raises(VLLMClientError):
130
+ params._validate_structured_outputs(
131
+ _StubModelConfig(is_diffusion=False),
132
+ StructuredOutputsConfig(backend=backend),
133
+ tokenizer=object(),
134
+ )
135
+
136
+
137
+@pytest.mark.parametrize(
138
+ "schema, expected_backend",
139
+ [
140
+ # multipleOf is unsupported by xgrammar, patternProperties also by guidance.
141
+ (
142
+ {
143
+ "type": "object",
144
+ "properties": {"n": {"type": "integer", "multipleOf": 2}},
145
+ },
146
+ "guidance",
147
+ ),
148
+ (
149
+ {"type": "object", "patternProperties": {"^a": {"type": "string"}}},
150
+ "outlines",
151
+ ),
152
+ ],
153
+)
154
+def test_auto_backend_falls_back_on_unsupported_schema(schema, expected_backend):
155
+ """`auto` falls back on rejection, so it must catch what the validators raise."""
156
+ params = SamplingParams(structured_outputs=StructuredOutputsParams(json=schema))
157
+ params._validate_structured_outputs(
158
+ _StubModelConfig(is_diffusion=False),
159
+ StructuredOutputsConfig(backend="auto"),
160
+ tokenizer=object(),
161
+ )
162
+ assert params.structured_outputs._backend == expected_backend
vllm/entrypoints/openai/engine/protocol.py
+1
-1
@@ -266,7 +266,7 @@ def validate_structural_tag_payload(payload: Any, *, parameter: str) -> None:
266
structured_outputs=StructuredOutputsParams(structural_tag=payload)
267
)
268
)
269
- except (TypeError, ValueError) as exc:
269
+ except (TypeError, ValueError, VLLMValidationError) as exc:
270
raise VLLMValidationError(
271
f"Invalid {parameter} structural_tag specification.",
272
parameter=parameter,
vllm/sampling_params.py
+7
-2
@@ -1092,7 +1092,7 @@ class SamplingParams(
1092
try:
1093
validate_xgrammar_grammar(self)
1094
self.structured_outputs._backend = "xgrammar"
1095
- except ValueError:
1095
+ except VLLMValidationError:
1096
# The request either failed validation
1097
# or includes some jsonschema feature(s) that
1098
# are not supported in xgrammar.
@@ -1103,7 +1103,12 @@ class SamplingParams(
1103
so_params = self.structured_outputs
1104
if not skip_guidance and so_params.json:
1105
if isinstance(so_params.json, str):
1106
- schema = json_mod.loads(so_params.json)
1106
+ try:
1107
+ schema = json_mod.loads(so_params.json)
1108
+ except json_mod.JSONDecodeError as e:
1109
+ raise VLLMValidationError(
1110
+ "Invalid JSON grammar specification."
1111
+ ) from e
1112
else:
1113
schema = so_params.json
1114
skip_guidance = has_guidance_unsupported_json_features(schema)
vllm/v1/structured_output/backend_guidance.py
+10
-4
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
10
import torch
11
from transformers import MistralCommonBackend
12
13
+from vllm.exceptions import VLLMValidationError
14
from vllm.logger import init_logger
15
from vllm.sampling_params import SamplingParams
16
from vllm.utils.import_utils import LazyLoader
@@ -269,7 +270,7 @@ def serialize_guidance_grammar(
270
begin: str = s["begin"]
271
trig = next((t for t in triggers if begin.startswith(t)), None)
272
if trig is None:
272
- raise ValueError(
273
+ raise VLLMValidationError(
274
f"Trigger {begin} not found in triggers {triggers}"
275
)
276
tags.append(
@@ -281,7 +282,9 @@ def serialize_guidance_grammar(
282
)
283
)
284
if not tags:
284
- raise ValueError("No structural tags found in the grammar spec.")
285
+ raise VLLMValidationError(
286
+ "No structural tags found in the grammar spec."
287
+ )
288
return llguidance.StructTag.to_grammar(tags)
289
else:
290
logger.error(
@@ -300,7 +303,10 @@ def validate_guidance_grammar(
303
if sampling_params.structured_outputs is None:
304
return
305
tp, grm = get_structured_output_key(sampling_params.structured_outputs)
303
- guidance_grm = serialize_guidance_grammar(tp, grm)
306
+ try:
307
+ guidance_grm = serialize_guidance_grammar(tp, grm)
308
+ except (ValueError, KeyError, TypeError) as e:
309
+ raise VLLMValidationError(f"Invalid grammar specification: {e}") from e
310
err = llguidance.LLMatcher.validate_grammar(guidance_grm, tokenizer)
311
if err:
306
- raise ValueError(f"Grammar error: {err}")
312
+ raise VLLMValidationError(f"Grammar error: {err}")
vllm/v1/structured_output/backend_lm_format_enforcer.py
+5
-4
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
9
import torch
10
from transformers import PreTrainedTokenizerBase
11
12
+from vllm.exceptions import VLLMValidationError
13
from vllm.sampling_params import SamplingParams
14
from vllm.utils.import_utils import LazyLoader
15
from vllm.utils.torch_utils import PIN_MEMORY
@@ -166,7 +167,7 @@ def validate_structured_output_request_lm_format_enforcer(params: SamplingParams
167
so_params.regex,
168
)
169
except Exception as err:
169
- raise ValueError(
170
+ raise VLLMValidationError(
171
f"Failed to compile regex for lm-format-enforcer: {err}"
172
) from err
173
return
@@ -176,19 +177,19 @@ def validate_structured_output_request_lm_format_enforcer(params: SamplingParams
177
# make sure schema is valid json
178
json.loads(so_params.json)
179
except json.JSONDecodeError as e:
179
- raise ValueError("Invalid JSON grammar specification.") from e
180
+ raise VLLMValidationError("Invalid JSON grammar specification.") from e
181
else:
182
try:
183
json.dumps(so_params.json)
184
except Exception as e:
184
- raise ValueError(
185
+ raise VLLMValidationError(
186
f"Error serializing structured outputs jsonschema: {e}"
187
) from e
188
return
189
elif so_params.choice:
190
return
191
elif so_params.grammar:
191
- raise ValueError(
192
+ raise VLLMValidationError(
193
"LM Format Enforcer structured outputs backend "
194
"does not support grammar specifications"
195
)
vllm/v1/structured_output/backend_outlines.py
+13
-7
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING
13
import torch
14
from regex import escape as regex_escape
15
16
+from vllm.exceptions import VLLMValidationError
17
from vllm.sampling_params import SamplingParams
18
from vllm.utils.import_utils import LazyLoader
19
from vllm.utils.torch_utils import PIN_MEMORY
@@ -186,22 +187,27 @@ def validate_structured_output_request_outlines(params: SamplingParams):
187
json.loads(so_params.json)
188
schema = so_params.json
189
except json.JSONDecodeError as e:
189
- raise ValueError("Invalid JSON grammar specification.") from e
190
+ raise VLLMValidationError("Invalid JSON grammar specification.") from e
191
else:
192
try:
193
schema = json.dumps(so_params.json)
194
except Exception as e:
194
- raise ValueError(
195
+ raise VLLMValidationError(
196
f"Error serializing structured outputs jsonschema: {e}"
197
) from e
197
- pattern = json_schema.build_regex_from_schema(schema)
198
+ try:
199
+ pattern = json_schema.build_regex_from_schema(schema)
200
+ except Exception as e:
201
+ raise VLLMValidationError(
202
+ f"Failed to transform json schema into a regex: {e}"
203
+ ) from e
204
validate_regex_is_buildable(pattern)
205
elif so_params.choice:
206
choices = [regex_escape(str(choice)) for choice in so_params.choice]
207
regex = "(" + "|".join(choices) + ")"
208
validate_regex_is_buildable(regex)
209
elif so_params.grammar:
204
- raise ValueError(
210
+ raise VLLMValidationError(
211
"Outlines structured outputs backend "
212
"does not support grammar specifications"
213
)
@@ -315,19 +321,19 @@ def validate_regex_is_buildable(pattern: str) -> None:
321
parsed = sre_parse.parse(pattern)
322
323
except sre_constants.error as e:
318
- raise ValueError(f"Error parsing regex: {e}") from e
324
+ raise VLLMValidationError(f"Error parsing regex: {e}") from e
325
326
try:
327
_check_unsupported(parsed)
328
except ValueError as e:
323
- raise ValueError(
329
+ raise VLLMValidationError(
330
f"Regex uses unsupported feature for structured outputs: {e}. "
331
"Only basic matching constructs are supported—lookarounds, "
332
"backreferences, and unicode boundaries are not."
333
) from e
334
335
if _prefix_needs_context(parsed):
330
- raise ValueError(
336
+ raise VLLMValidationError(
337
"Regex does not have a anchored universal start state"
338
"This means that the Regex uses anchors (^) or look-arounds "
339
"in a way which requires context before any token is matched."
vllm/v1/structured_output/backend_xgrammar.py
+10
-9
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
8
import torch
9
10
import vllm.envs
11
+from vllm.exceptions import VLLMValidationError
12
from vllm.logger import init_logger
13
from vllm.sampling_params import SamplingParams
14
from vllm.utils.import_utils import LazyLoader
@@ -276,7 +277,7 @@ def has_xgrammar_unsupported_json_features(schema: dict[str, Any]) -> bool:
277
def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
278
"""Validate that the request is supported by structured output.
279
279
- Raises ValueError if the request is not supported.
280
+ Raises VLLMValidationError if the request is not supported.
281
"""
282
if sampling_params.structured_outputs is None:
283
return
@@ -298,7 +299,7 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
299
so_params.regex,
300
)
301
except Exception as err:
301
- raise ValueError(
302
+ raise VLLMValidationError(
303
f"Failed to transform regex into a grammar: {err}"
304
) from err
305
@@ -307,7 +308,7 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
308
try:
309
xgr.Grammar.from_ebnf(choice_grammar)
310
except Exception as err:
310
- raise ValueError(
311
+ raise VLLMValidationError(
312
f"Failed to transform choices into a grammar: {err}"
313
) from err
314
so_params.choice = None
@@ -319,19 +320,19 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
320
try:
321
schema = json.loads(so_params.json)
322
except json.JSONDecodeError as e:
322
- raise ValueError("Invalid JSON grammar specification.") from e
323
+ raise VLLMValidationError("Invalid JSON grammar specification.") from e
324
else:
325
schema = so_params.json
326
327
if has_xgrammar_unsupported_json_features(schema):
327
- raise ValueError(
328
+ raise VLLMValidationError(
329
"The provided JSON schema contains features not supported by xgrammar."
330
)
331
332
try:
333
xgr.Grammar.from_json_schema(schema)
334
except Exception as err:
334
- raise ValueError(
335
+ raise VLLMValidationError(
336
f"Failed to transform json schema into a grammar: {err}"
337
) from err
338
return
@@ -342,7 +343,7 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
343
try:
344
so_params.grammar = convert_lark_to_ebnf(so_params.grammar)
345
except ValueError as e:
345
- raise ValueError(
346
+ raise VLLMValidationError(
347
"Failed to convert the grammar from Lark to EBNF. "
348
) from e
349
@@ -351,7 +352,7 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
352
# parse the grammar, but we aren't compiling it.
353
xgr.Grammar.from_ebnf(so_params.grammar)
354
except Exception as e:
354
- raise ValueError("Invalid grammar specification.") from e
355
+ raise VLLMValidationError("Invalid grammar specification.") from e
356
return
357
358
if so_params.structural_tag:
@@ -372,4 +373,4 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
373
else:
374
xgr.Grammar.from_structural_tag(so_params.structural_tag)
375
except Exception as e:
375
- raise ValueError("Invalid structural tag specification.") from e
376
+ raise VLLMValidationError("Invalid structural tag specification.") from e