From 340b7e4909f14ff33943ab6ac7955191e1b8b4ff Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:29:32 +0800 Subject: [PATCH] fix: reject string schemas that mix pattern/format with length bounds (#49996) Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> --- tests/v1/structured_output/test_utils.py | 6 ++++++ vllm/v1/structured_output/backend_xgrammar.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/v1/structured_output/test_utils.py b/tests/v1/structured_output/test_utils.py index c026ab0e4e..11b3e5c01b 100644 --- a/tests/v1/structured_output/test_utils.py +++ b/tests/v1/structured_output/test_utils.py @@ -14,6 +14,12 @@ pytestmark = pytest.mark.cpu_test def unsupported_string_schemas(): return [ {"type": "string", "format": "non_existing_format"}, + # pattern/format is compiled but length bounds are silently dropped, + # so the combination must be rejected instead of producing quietly + # wrong output + {"type": "string", "pattern": "^a+$", "maxLength": 2}, + {"type": "string", "pattern": "^a+$", "minLength": 3}, + {"type": "string", "format": "email", "maxLength": 10}, ] diff --git a/vllm/v1/structured_output/backend_xgrammar.py b/vllm/v1/structured_output/backend_xgrammar.py index 5b24a19780..3809ba2d91 100644 --- a/vllm/v1/structured_output/backend_xgrammar.py +++ b/vllm/v1/structured_output/backend_xgrammar.py @@ -262,6 +262,20 @@ def has_xgrammar_unsupported_json_features(schema: dict[str, Any]) -> bool: ): return True + # A string mixing a generative constraint (pattern or format) with + # explicit length bounds. xgrammar compiles the pattern/format side + # and silently drops minLength/maxLength from the grammar, so output + # can violate the bound without any error surfacing. Verified against + # the compiled EBNF: pattern/format grammars come out byte-identical + # with and without the length keywords, while maxLength alone lowers + # to {0, N} correctly. + if ( + obj.get("type") == "string" + and ("pattern" in obj or "format" in obj) + and ("minLength" in obj or "maxLength" in obj) + ): + return True + # Unsupported keywords for objects if obj.get("type") == "object" and any( key in obj for key in ("patternProperties", "propertyNames")