Skip to content
Closed
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,4 @@ csrc/moe/marlin_moe_wna16/kernel_*

# Ignore ep_kernels_workspace folder
ep_kernels_workspace/
xgrammar/
Comment thread
Hanchenli marked this conversation as resolved.
Outdated
51 changes: 51 additions & 0 deletions tests/v1/entrypoints/llm/test_struct_output_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,3 +814,54 @@ def test_structured_output_batched_with_non_guided_requests(
# non-guided requests should not return a valid JSON here
with pytest.raises(ValueError):
output_json = json.loads(generated_text)


@pytest.mark.parametrize("guided_decoding_backend", ["xgrammar"])
def test_structured_output_with_structural_tag(
monkeypatch: pytest.MonkeyPatch,
guided_decoding_backend: str,
):
monkeypatch.setenv("VLLM_USE_V1", "1")

llm = LLM(
model="Qwen/Qwen2.5-1.5B-Instruct",
guided_decoding_backend=guided_decoding_backend,
)

structural_tag_config = {
"type": "structural_tag",
"format": {

@chaunceyjiang chaunceyjiang Oct 9, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding some usage examples in the examples directory.

"type":
"triggered_tags",
"tags": [{
"begin": "hello_flag",
"content": {
"type": "any_text"
},
"end": "hello"
}],
"triggers": ["hello"],
"stop_after_first":
False
}
}

sampling_params = SamplingParams(
temperature=0.0,
max_tokens=500,
guided_decoding=GuidedDecodingParams(
structural_tag=json.dumps(structural_tag_config)),
)

prompt = "Hello and repete hello 10 times, do not say anything else. Only say hello hello hello, now start"
Comment thread
aarnphm marked this conversation as resolved.
Outdated
outputs = llm.generate(prompt,
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
assert "hello_flag" in generated_text, f"Expected 'hello_flag' to be in generated text, but got: {generated_text}"
20 changes: 15 additions & 5 deletions vllm/entrypoints/openai/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
strict: Optional[bool] = None


class StructuralTag(OpenAIBaseModel):
class LegacyStructuralTag(OpenAIBaseModel):
Comment thread
aarnphm marked this conversation as resolved.
begin: str
# schema is the field, but that causes conflicts with pydantic so
# instead use structural_tag_schema with an alias
Expand All @@ -163,19 +163,29 @@
end: str


class StructuralTagResponseFormat(OpenAIBaseModel):
class LegacyStructuralTagResponseFormat(OpenAIBaseModel):
type: Literal["structural_tag"]
structures: list[StructuralTag]
structures: list[LegacyStructuralTag]
triggers: list[str]


class StructuralTagResponseFormat(OpenAIBaseModel):
type: Literal["structural_tag"]
format: Any
Comment thread
Hanchenli marked this conversation as resolved.


AnyStructuralTagResponseFormat = Union[LegacyStructuralTagResponseFormat,
StructuralTagResponseFormat]


class ResponseFormat(OpenAIBaseModel):
# type must be "json_schema", "json_object", or "text"
type: Literal["text", "json_object", "json_schema"]
json_schema: Optional[JsonSchemaResponseFormat] = None


AnyResponseFormat = Union[ResponseFormat, StructuralTagResponseFormat]
AnyResponseFormat = Union[ResponseFormat, LegacyStructuralTagResponseFormat,
StructuralTagResponseFormat]


class StreamOptions(OpenAIBaseModel):
Expand Down Expand Up @@ -682,8 +692,8 @@
self.guided_json = json_schema.json_schema
elif self.response_format.type == "structural_tag":
structural_tag = self.response_format
assert structural_tag is not None and isinstance(

Check failure on line 695 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Parameterized generics cannot be used with class or instance checks [misc]

Check failure on line 695 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Parameterized generics cannot be used with class or instance checks [misc]

Check failure on line 695 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Parameterized generics cannot be used with class or instance checks [misc]

Check failure on line 695 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Parameterized generics cannot be used with class or instance checks [misc]

Check failure on line 695 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Parameterized generics cannot be used with class or instance checks [misc]
structural_tag, StructuralTagResponseFormat)
structural_tag, AnyStructuralTagResponseFormat)

Check failure on line 696 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "_ClassInfo" [arg-type]

Check failure on line 696 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "_ClassInfo" [arg-type]

Check failure on line 696 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "_ClassInfo" [arg-type]

Check failure on line 696 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "_ClassInfo" [arg-type]

Check failure on line 696 in vllm/entrypoints/openai/protocol.py

View workflow job for this annotation

GitHub Actions / pre-commit

Argument 2 to "isinstance" has incompatible type "<typing special form>"; expected "_ClassInfo" [arg-type]
s_tag_obj = structural_tag.model_dump(by_alias=True)
self.structural_tag = json.dumps(s_tag_obj)

Expand Down
42 changes: 26 additions & 16 deletions vllm/v1/structured_output/backend_xgrammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,19 @@ def compile_grammar(self, request_type: StructuredOutputOptions,
ctx = self.compiler.compile_regex(grammar_spec)
elif request_type == StructuredOutputOptions.STRUCTURAL_TAG:
s_tag = json.loads(grammar_spec)
tags = [
xgr.StructuralTagItem(
begin=s["begin"],
schema=json.dumps(s["schema"]),
end=s["end"],
) for s in s_tag["structures"]
]
ctx = self.compiler.compile_structural_tag(tags, s_tag["triggers"])
if "structures" in s_tag:
#Falling back to deprecated method of compiling structural tag
tags = [
xgr.StructuralTagItem(
begin=s["begin"],
schema=json.dumps(s["schema"]),
end=s["end"],
) for s in s_tag["structures"]
]
ctx = self.compiler.compile_structural_tag(
tags, s_tag["triggers"])
Comment thread
aarnphm marked this conversation as resolved.
Outdated
else:
ctx = self.compiler.compile_structural_tag(grammar_spec)
else:
logger.error(
"Validation should have already occurred. Please file an issue."
Expand Down Expand Up @@ -311,13 +316,18 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
if gd_params.structural_tag:
try:
s_tag = json.loads(gd_params.structural_tag)
tags = [
xgr.StructuralTagItem(
begin=s["begin"],
schema=json.dumps(s["schema"]),
end=s["end"],
) for s in s_tag["structures"]
]
xgr.Grammar.from_structural_tag(tags, s_tag["triggers"])

# Using the deprecated method of compiling structural tag
if "structures" in s_tag:
tags = [
xgr.StructuralTagItem(
begin=s["begin"],
schema=json.dumps(s["schema"]),
end=s["end"],
) for s in s_tag["structures"]
]
xgr.Grammar.from_structural_tag(tags, s_tag["triggers"])
else:
xgr.Grammar.from_structural_tag(gd_params.structural_tag)
except Exception as e:
raise ValueError("Invalid structural tag specification.") from e
1 change: 0 additions & 1 deletion vllm/v1/worker/gpu_model_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@

if TYPE_CHECKING:
import xgrammar as xgr

from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
from vllm.v1.core.sched.output import SchedulerOutput
else:
Expand Down