Skip to content

[Bugfix] remove fallback in guided_json (int range, patterns) #16725

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion tests/entrypoints/llm/test_guided_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def test_disable_guided_decoding_fallback(sample_regex, llm):
with pytest.raises(
ValueError,
match="xgrammar does not support advanced JSON schema features "
"like enums, patterns or numeric ranges."):
"like string length, item limits, or property bounds."):
llm.generate(prompts="This should fail",
sampling_params=sampling_params,
use_tqdm=True)
Expand Down Expand Up @@ -386,6 +386,62 @@ def test_guided_json_completion_with_enum(llm, guided_decoding_backend: str):
jsonschema.validate(instance=output_json, schema=json_schema)


@pytest.mark.skip_global_cleanup
@pytest.mark.parametrize("guided_decoding_backend", GUIDED_DECODING_BACKENDS)
def test_guided_number_range_json_completion(llm,
guided_decoding_backend: str):
sample_output_schema = {
"type": "object",
"properties": {
"age": {
"type": "integer",
"minimum": 18,
"maximum": 99
},
"score": {
"type": "number",
"minimum": 0.0,
"maximum": 100.0
},
"zipcode": {
"type": "string",
"pattern": r"^\d{5}(-\d{4})?$"
},
},
"required": ["age", "score", "zipcode"],
}
sampling_params = SamplingParams(
temperature=1.0,
max_tokens=1000,
guided_decoding=GuidedDecodingParams(json=sample_output_schema,
backend=guided_decoding_backend),
)
outputs = llm.generate(
prompts=[
"Create a JSON object for a user with age, score, and zipcode."
] * 2,
sampling_params=sampling_params,
use_tqdm=True,
)

assert outputs is not None

for output in outputs:
assert output is not None
assert isinstance(output, RequestOutput)
prompt = output.prompt

generated_text = output.outputs[0].text
assert generated_text is not None
print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")
output_json = json.loads(generated_text)
jsonschema.validate(instance=output_json, schema=sample_output_schema)
assert 18 <= output_json["age"] <= 99
assert 0.0 <= output_json["score"] <= 100.0
assert (re.fullmatch(r"^\d{5}(-\d{4})?$", output_json["zipcode"])
is not None)


@pytest.mark.skip_global_cleanup
def test_guidance_no_additional_properties(llm):
schema = {
Expand Down
34 changes: 18 additions & 16 deletions tests/v1/entrypoints/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ def sample_json_schema():
"type": "string",
}
},
"grade": {
"type": "string",
"pattern": "^[A-D]$" # Regex pattern
},
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"work_history": {
"type": "array",
"items": {
Expand All @@ -56,17 +64,20 @@ def sample_json_schema():
"type": "string"
},
"duration": {
"type": "number"
"type": "number",
"minimum": 0.0,
"maximum": 100.0, # Numeric range
},
"position": {
"type": "string"
}
},
"required": ["company", "position"]
"required": ["company", "duration", "position"]
}
}
},
"required": ["name", "age", "skills", "work_history"]
"required":
["name", "age", "skills", "grade", "email", "work_history"]
}


Expand All @@ -78,27 +89,18 @@ def unsupported_json_schema():
"properties": {
"score": {
"type": "integer",
"minimum": 0,
"maximum": 100 # Numeric range
},
"grade": {
"type": "string",
"pattern": "^[A-D]$" # Regex pattern
},
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
"multipleOf": 5 # Numeric multiple
},
"tags": {
"type": "array",
"items": {
"type": "string",
"pattern":
"^[a-z]{1,10}$" # Combining length and pattern restrictions
"minLength": 10,
"maxLength": 20
}
}
},
"required": ["score", "grade", "email", "tags"]
"required": ["score", "tags"]
}


Expand Down
53 changes: 16 additions & 37 deletions tests/v1/structured_output/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
@pytest.fixture
def unsupported_string_schemas():
return [
{
"type": "string",
"pattern": "^[a-zA-Z]+$"
},
{
"type": "string",
"format": "email"
Expand All @@ -23,22 +19,6 @@ def unsupported_string_schemas():
@pytest.fixture
def unsupported_integer_schemas():
return [
{
"type": "integer",
"minimum": 0
},
{
"type": "integer",
"maximum": 120
},
{
"type": "integer",
"exclusiveMinimum": 120
},
{
"type": "integer",
"exclusiveMaximum": 120
},
{
"type": "integer",
"multipleOf": 120
Expand All @@ -49,22 +29,6 @@ def unsupported_integer_schemas():
@pytest.fixture
def unsupported_number_schemas():
return [
{
"type": "number",
"minimum": 0
},
{
"type": "number",
"maximum": 120
},
{
"type": "number",
"exclusiveMinimum": 120
},
{
"type": "number",
"exclusiveMaximum": 120
},
{
"type": "number",
"multipleOf": 120
Expand Down Expand Up @@ -156,13 +120,28 @@ def supported_schema():
"type": "string",
"enum": ["sedan", "suv", "truck"]
},
"car_brand": {
"type": "string",
"pattern": "^[a-zA-Z]+$"
},
"short_description": {
"type": "string",
"maxLength": 50
},
"mileage": {
"type": "number",
"minimum": 0,
"maximum": 1000000
},
"model_year": {
"type": "integer",
"exclusiveMinimum": 1900,
"exclusiveMaximum": 2100
},
"long_description": {
"type": "string",
"minLength": 50
"minLength": 50,
"maxLength": 2000
},
"address": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion vllm/model_executor/guided_decoding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def fallback_or_error(guided_params: GuidedDecodingParams, message: str,
fallback_or_error(
guided_params,
"xgrammar does not support advanced JSON schema features like "
"enums, patterns or numeric ranges.", "outlines")
"string length, item limits, or property bounds.", "outlines")

# xgrammar only supports GBNF grammars, so we must convert Lark.
# We must check if the grammar is likely Lark and if that
Expand Down
10 changes: 1 addition & 9 deletions vllm/model_executor/guided_decoding/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,8 @@ def check_object(obj: dict) -> bool:
if not isinstance(obj, dict):
return False

# Check for pattern restrictions
if "pattern" in obj:
return True

# Check for numeric ranges
if obj.get("type") in ("integer", "number") and any(
key in obj for key in [
"minimum", "maximum", "exclusiveMinimum",
"exclusiveMaximum", "multipleOf"
]):
if obj.get("type") in ("integer", "number") and ("multipleOf" in obj):
return True

# Check for array unsupported keywords
Expand Down
9 changes: 1 addition & 8 deletions vllm/v1/structured_output/backend_xgrammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,8 @@ def check_object(obj: dict[str, Any]) -> bool:
if not isinstance(obj, dict):
return False

# Check for pattern restrictions
if "pattern" in obj:
return True

# Check for numeric ranges
if obj.get("type") in ("integer", "number") and any(
key in obj
for key in ("minimum", "maximum", "exclusiveMinimum",
"exclusiveMaximum", "multipleOf")):
if obj.get("type") in ("integer", "number") and ("multipleOf" in obj):
return True

# Check for array unsupported keywords
Expand Down