Skip to content

Commit a223a3f

Browse files
committed
feat: v1 support for no-additional-properties
Signed-off-by: Travis Johnson <[email protected]>
1 parent 1fde78b commit a223a3f

File tree

3 files changed

+100
-24
lines changed

3 files changed

+100
-24
lines changed

tests/v1/entrypoints/llm/test_struct_output_generate.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,59 @@ def test_structured_output_auto_mode(
365365
# Parse to verify it is valid JSON
366366
parsed_json = json.loads(generated_text)
367367
assert isinstance(parsed_json, dict)
368+
369+
370+
@pytest.mark.skip_global_cleanup
371+
def test_guidance_no_additional_properties(monkeypatch: pytest.MonkeyPatch):
372+
monkeypatch.setenv("VLLM_USE_V1", "1")
373+
374+
backend = 'guidance:no-additional-properties,disable-any-whitespace'
375+
llm = LLM(model="Qwen/Qwen2.5-1.5B-Instruct",
376+
max_model_len=1024,
377+
guided_decoding_backend=backend)
378+
379+
schema = {
380+
'type': 'object',
381+
'properties': {
382+
'a1': {
383+
'type': 'string'
384+
},
385+
'a2': {
386+
'type': 'string'
387+
},
388+
'a3': {
389+
'type': 'string'
390+
}
391+
},
392+
'required': ['a1', 'a2', 'a3'],
393+
}
394+
395+
prompt = (
396+
"<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a "
397+
"helpful assistant.<|im_end|>\n<|im_start|>user\nPlease generate a "
398+
"large JSON object with key-value pairs a1=b1, a2=b2, ..., a20=b20"
399+
"<|im_end|>\n<|im_start|>assistant\n")
400+
401+
def generate_with_backend(backend):
402+
guided_params = GuidedDecodingParams(json=schema, backend=backend)
403+
sampling_params = SamplingParams(temperature=0,
404+
max_tokens=256,
405+
guided_decoding=guided_params)
406+
407+
outputs = llm.generate(prompts=prompt, sampling_params=sampling_params)
408+
assert outputs is not None
409+
generated_text = outputs[0].outputs[0].text
410+
assert generated_text is not None
411+
parsed_json = json.loads(generated_text)
412+
assert isinstance(parsed_json, dict)
413+
jsonschema.validate(instance=parsed_json, schema=schema)
414+
return parsed_json
415+
416+
generated = generate_with_backend(
417+
'guidance:no-additional-properties,disable-any-whitespace')
418+
assert "a1" in generated
419+
assert "a2" in generated
420+
assert "a3" in generated
421+
assert "a4" not in generated
422+
assert "a5" not in generated
423+
assert "a6" not in generated

vllm/model_executor/guided_decoding/guidance_decoding.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# SPDX-License-Identifier: Apache-2.0
2-
import copy
32
import json
43
from re import escape as regex_escape
54

@@ -9,18 +8,8 @@
98
from vllm.model_executor.guided_decoding.guidance_logits_processors import (
109
GuidanceLogitsProcessor)
1110
from vllm.sampling_params import GuidedDecodingParams
12-
13-
14-
def _walk_json_for_additional_properties(data: object):
15-
if isinstance(data, dict):
16-
for value in data.values():
17-
_walk_json_for_additional_properties(value)
18-
if 'additionalProperties' not in data and \
19-
('properties' in data or 'patternProperties' in data):
20-
data['additionalProperties'] = False
21-
elif isinstance(data, list):
22-
for item in data:
23-
_walk_json_for_additional_properties(item)
11+
from vllm.v1.structured_output.backend_guidance import (
12+
process_for_additional_properties)
2413

2514

2615
def get_local_guidance_guided_decoding_logits_processor(
@@ -39,12 +28,9 @@ def get_local_guidance_guided_decoding_logits_processor(
3928
# By default, other backends do not allow additional top-level
4029
# properties, so this makes guidance more similar to other backends
4130
if 'no-additional-properties' in guided_params.backend_options():
42-
if isinstance(guide_json, str):
43-
guide_json = json.loads(guide_json)
44-
else:
45-
# copy for modifications
46-
guide_json = copy.deepcopy(guide_json)
47-
_walk_json_for_additional_properties(guide_json)
31+
if not isinstance(guide_json, str):
32+
guide_json = json.dumps(guide_json)
33+
guide_json = process_for_additional_properties(guide_json)
4834

4935
grm = llguidance.LLMatcher.grammar_from_json_schema(
5036
guide_json,

vllm/v1/structured_output/backend_guidance.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# SPDX-License-Identifier: Apache-2.0
22

3+
import copy
4+
import json
35
import os
46
from dataclasses import dataclass
5-
from typing import TYPE_CHECKING, Optional
7+
from typing import TYPE_CHECKING, Any, Optional
68

79
import torch
810

@@ -29,6 +31,29 @@
2931
logger = init_logger(__name__)
3032

3133

34+
def _walk_json_for_additional_properties(data: object):
35+
if isinstance(data, dict):
36+
for value in data.values():
37+
_walk_json_for_additional_properties(value)
38+
if 'additionalProperties' not in data and \
39+
('properties' in data or 'patternProperties' in data):
40+
data['additionalProperties'] = False
41+
elif isinstance(data, list):
42+
for item in data:
43+
_walk_json_for_additional_properties(item)
44+
45+
46+
def process_for_additional_properties(
47+
guide_json: str | dict[str, Any]) -> dict[str, Any]:
48+
if isinstance(guide_json, str):
49+
guide_json_obj = json.loads(guide_json)
50+
else:
51+
# copy for modifications
52+
guide_json_obj = copy.deepcopy(guide_json)
53+
_walk_json_for_additional_properties(guide_json_obj)
54+
return guide_json_obj
55+
56+
3257
class GuidanceBackend(StructuredOutputBackend):
3358

3459
def __init__(self, vllm_config: VllmConfig):
@@ -43,12 +68,15 @@ def __init__(self, vllm_config: VllmConfig):
4368
self.vocab_size = vllm_config.model_config.get_vocab_size()
4469

4570
self.disable_any_whitespace = False
71+
self.no_additional_properties = False
4672
backend_options = GuidedDecodingParams(
4773
backend=vllm_config.decoding_config.guided_decoding_backend
4874
).backend_options()
4975
for option in backend_options:
5076
if option == "disable-any-whitespace":
5177
self.disable_any_whitespace = True
78+
elif option == "no-additional-properties":
79+
self.no_additional_properties = True
5280
else:
5381
raise ValueError(
5482
f"Unsupported option for the guidance backend: {option}")
@@ -60,7 +88,8 @@ def __init__(self, vllm_config: VllmConfig):
6088
def compile_grammar(self, request_type: StructuredOutputOptions,
6189
grammar_spec: str) -> StructuredOutputGrammar:
6290
self.serialized_grammar = serialize_guidance_grammar(
63-
request_type, grammar_spec, self.disable_any_whitespace)
91+
request_type, grammar_spec, self.disable_any_whitespace,
92+
self.no_additional_properties)
6493

6594
ll_matcher = llguidance.LLMatcher(
6695
self.ll_tokenizer,
@@ -137,10 +166,15 @@ def reset(self):
137166
self.ll_matcher.reset()
138167

139168

140-
def serialize_guidance_grammar(request_type: StructuredOutputOptions,
141-
grammar_spec: str,
142-
disable_any_whitespace: bool = False) -> str:
169+
def serialize_guidance_grammar(
170+
request_type: StructuredOutputOptions,
171+
grammar_spec: str | dict[str, Any],
172+
disable_any_whitespace: bool = False,
173+
no_additional_properties: bool = False,
174+
) -> str:
143175
if request_type == StructuredOutputOptions.JSON:
176+
if no_additional_properties:
177+
grammar_spec = process_for_additional_properties(grammar_spec)
144178
return llguidance.LLMatcher.grammar_from_json_schema(
145179
grammar_spec,
146180
defaults={

0 commit comments

Comments
 (0)