diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index da07df64f..a2b6283c8 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -160,7 +160,7 @@ for the full API reference.
| `generation.invalid_fraction_threshold` | `0.8` | Invalid record fraction that triggers the patience counter | Leave at default |
| `generation.use_structured_generation` | `false` | Enable structured output to constrain record format (typically at the cost of reducing the quality of generated records and increasing generation time; use when the pipeline struggles to produce valid records) | Leave off unless the pipeline cannot produce valid records |
| `generation.structured_generation_backend` | `"auto"` | vLLM guided-decoding backend | Leave at `"auto"` |
-| `generation.structured_generation_schema_method` | `"regex"` | Schema method (`"regex"` or `"json_schema"`) | Leave at `"regex"` |
+| `generation.structured_generation_schema_method` | `"auto"` | Schema method (`"auto"`, `"structural_tag"`, `"json_schema"`, or `"regex"`) | Leave at `"auto"`; it picks `"structural_tag"` on xgrammar-capable backends and `"regex"` otherwise |
| `generation.structured_generation_use_single_sequence` | `false` | Match exactly one sequence when `max_sequences_per_example` is 1 | Leave at default |
| `generation.enforce_timeseries_fidelity` | `false` | Enforce time series order, intervals, and timestamps | Enable for time series data |
| `generation.attention_backend` | `"auto"` | vLLM attention backend | Leave at `"auto"` |
diff --git a/docs/user-guide/running.md b/docs/user-guide/running.md
index bcf92d748..ebc648ecf 100644
--- a/docs/user-guide/running.md
+++ b/docs/user-guide/running.md
@@ -883,9 +883,11 @@ records. Use it when the pipeline struggles to produce valid records.
```yaml
generation:
use_structured_generation: true
- structured_generation_schema_method: "regex"
+ structured_generation_schema_method: "auto"
```
+- `"auto"`: picks `"structural_tag"` when `structured_generation_backend` is `"auto"` or `"xgrammar"`, otherwise `"regex"`.
+- `"structural_tag"`: uses XGrammar Structural Tag to compose schema-constrained JSONL output.
- `"regex"`: constructs a custom regex from the dataset schema. More comprehensive but slower.
- `"json_schema"`: passes a JSON Schema to the backend. Faster, but may miss edge cases.
diff --git a/pyproject.toml b/pyproject.toml
index 4c4ffc8b8..880f54e6e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -137,6 +137,7 @@ cpu = [
"triton>=2.0.0; sys_platform=='linux'",
"trl>=0.23.0",
"vllm==0.20.0; sys_platform=='linux'",
+ "xgrammar>=0.2.0; sys_platform=='linux'",
]
cu129 = [
@@ -161,6 +162,7 @@ cu129 = [
"triton>=2.0.0; sys_platform == 'linux'",
"trl>=0.23.0",
"vllm==0.20.0+cu129; sys_platform == 'linux'",
+ "xgrammar>=0.2.0; sys_platform == 'linux'",
]
# at some point, do per-subpackage dependencies
diff --git a/src/nemo_safe_synthesizer/config/generate.py b/src/nemo_safe_synthesizer/config/generate.py
index 28c469c5a..8eca34108 100644
--- a/src/nemo_safe_synthesizer/config/generate.py
+++ b/src/nemo_safe_synthesizer/config/generate.py
@@ -3,11 +3,12 @@
from __future__ import annotations
-from typing import Annotated, Literal
+from typing import Annotated, Literal, Self
from pydantic import (
BaseModel,
Field,
+ model_validator,
)
from ..configurator.parameters import (
@@ -17,8 +18,57 @@
ValueValidator,
range_validator,
)
+from ..errors import ParameterError
+
+StructuredGenerationSchemaMethod = Literal["auto", "regex", "json_schema", "structural_tag"]
+ResolvedStructuredGenerationSchemaMethod = Literal["regex", "json_schema", "structural_tag"]
+StructuredGenerationBackend = Literal["auto", "xgrammar", "guidance", "outlines", "lm-format-enforcer"]
+
+STRUCTURAL_TAG_COMPATIBLE_BACKENDS = frozenset({"auto", "xgrammar"})
+
+__all__ = [
+ "GenerateParameters",
+ "ResolvedStructuredGenerationSchemaMethod",
+ "StructuredGenerationBackend",
+ "StructuredGenerationSchemaMethod",
+ "STRUCTURAL_TAG_COMPATIBLE_BACKENDS",
+ "ValidationParameters",
+ "resolve_structured_generation_schema_method",
+ "structural_tag_backend_error_message",
+]
+
+
+def resolve_structured_generation_schema_method(
+ schema_method: StructuredGenerationSchemaMethod,
+ backend: StructuredGenerationBackend | str,
+) -> ResolvedStructuredGenerationSchemaMethod:
+ """Resolve ``auto`` schema method from the configured structured-output backend.
+
+ ``auto`` picks ``structural_tag`` on xgrammar-capable backends and ``regex``
+ elsewhere, preserving legacy behavior for outlines/guidance configs that omit
+ an explicit schema method.
+ """
+ if schema_method != "auto":
+ return schema_method
+ if backend in STRUCTURAL_TAG_COMPATIBLE_BACKENDS:
+ return "structural_tag"
+ return "regex"
+
-__all__ = ["GenerateParameters", "ValidationParameters"]
+def structural_tag_backend_error_message(backend: str) -> str | None:
+ """Return an error message when *backend* cannot serve ``structural_tag``.
+
+ vLLM only supports XGrammar Structural Tag constraints when the guided
+ decoding backend is ``xgrammar`` or ``auto`` (which selects xgrammar for
+ this schema method).
+ """
+ if backend in STRUCTURAL_TAG_COMPATIBLE_BACKENDS:
+ return None
+ return (
+ "Invalid structured generation configuration: "
+ "`structured_generation_schema_method='structural_tag'` requires "
+ f"`structured_generation_backend` to be 'xgrammar' or 'auto', got {backend!r}."
+ )
class ValidationParameters(Parameters, BaseModel):
@@ -147,16 +197,18 @@ class GenerateParameters(Parameters, BaseModel):
] = "auto"
structured_generation_schema_method: Annotated[
- Literal["regex", "json_schema"],
+ StructuredGenerationSchemaMethod,
Field(
title="structured_generation_schema_method",
description=(
"The method used to generate the schema from your dataset and pass it to the generation backend. "
+ "'auto' picks 'structural_tag' on xgrammar-capable backends and 'regex' otherwise. "
"'regex' uses a custom regex construction method that tends to be more comprehensive "
- "than 'json_schema' at the cost of speed."
+ "than 'json_schema' at the cost of speed. 'structural_tag' uses XGrammar Structural Tag "
+ "to compose schema-constrained JSONL output."
),
),
- ] = "regex"
+ ] = "auto"
structured_generation_use_single_sequence: Annotated[
bool,
@@ -191,3 +243,13 @@ class GenerateParameters(Parameters, BaseModel):
),
),
] = "auto"
+
+ @model_validator(mode="after")
+ def _validate_structural_tag_backend(self) -> Self:
+ if not self.use_structured_generation:
+ return self
+ if self.structured_generation_schema_method != "structural_tag":
+ return self
+ if message := structural_tag_backend_error_message(self.structured_generation_backend):
+ raise ParameterError(message)
+ return self
diff --git a/src/nemo_safe_synthesizer/generation/regex_manager.py b/src/nemo_safe_synthesizer/generation/regex_manager.py
index 6728ab48a..990f34437 100644
--- a/src/nemo_safe_synthesizer/generation/regex_manager.py
+++ b/src/nemo_safe_synthesizer/generation/regex_manager.py
@@ -366,3 +366,74 @@ def build_json_based_regex(
regex = rf"({sequence_regex}\n)+"
return regex
+
+
+def _const_string_format(value: str) -> dict[str, str]:
+ """Return a Structural Tag constant-string format."""
+ return {"type": "const_string", "value": value}
+
+
+def _sequence_format(elements: list[dict[str, Any]]) -> dict[str, Any]:
+ """Return a Structural Tag sequence format."""
+ return {"type": "sequence", "elements": elements}
+
+
+def _plus_format(content: dict[str, Any]) -> dict[str, Any]:
+ """Return a Structural Tag one-or-more repetition format."""
+ return {"type": "plus", "content": content}
+
+
+def build_json_structural_tag(
+ schema: dict[str, Any],
+ config: SafeSynthesizerParameters,
+ bos_token: str,
+ eos_token: str,
+) -> str:
+ """Build an XGrammar Structural Tag for schema-constrained JSONL records.
+
+ The raw vLLM ``json`` constraint describes a single JSON value. Structural
+ Tag lets NSS describe the larger generation shape directly: one or more
+ schema-constrained JSON records separated by newlines, optionally wrapped
+ in BOS/EOS group delimiters.
+
+ Args:
+ schema: JSON schema dictionary describing one record.
+ config: Pipeline configuration (used for grouping and
+ structured-generation settings).
+ bos_token: Beginning-of-sequence token (used when grouping).
+ eos_token: End-of-sequence token (used when grouping).
+
+ Returns:
+ JSON string suitable for ``StructuredOutputsParams(structural_tag=...)``.
+ """
+ record_format: dict[str, Any] = {
+ "type": "json_schema",
+ "json_schema": schema,
+ }
+ record_line_format = _sequence_format([record_format, _const_string_format("\n")])
+
+ if config.data.group_training_examples_by is not None:
+ sequence_format = _sequence_format(
+ [
+ _const_string_format(bos_token),
+ _plus_format(record_line_format),
+ _const_string_format(eos_token),
+ ]
+ )
+ else:
+ sequence_format = record_format
+
+ if config.generation.structured_generation_use_single_sequence and config.data.max_sequences_per_example == 1:
+ output_format = sequence_format
+ elif config.data.group_training_examples_by is not None:
+ output_format = _plus_format(_sequence_format([sequence_format, _const_string_format("\n")]))
+ else:
+ output_format = _plus_format(record_line_format)
+
+ return json.dumps(
+ {
+ "type": "structural_tag",
+ "format": output_format,
+ },
+ ensure_ascii=True,
+ )
diff --git a/src/nemo_safe_synthesizer/generation/vllm_backend.py b/src/nemo_safe_synthesizer/generation/vllm_backend.py
index 8f80aef1b..b1ee029d5 100644
--- a/src/nemo_safe_synthesizer/generation/vllm_backend.py
+++ b/src/nemo_safe_synthesizer/generation/vllm_backend.py
@@ -25,12 +25,16 @@
from .. import utils
from ..cli.artifact_structure import Workdir
from ..config import SafeSynthesizerParameters
+from ..config.generate import (
+ resolve_structured_generation_schema_method,
+ structural_tag_backend_error_message,
+)
from ..defaults import DEFAULT_SAMPLING_PARAMETERS, FIXED_RUNTIME_GENERATE_ARGS
-from ..errors import InternalError
+from ..errors import InternalError, ParameterError
from ..generation.backend import GeneratorBackend
from ..generation.batch import Batch
from ..generation.processors import Processor, TabularDataProcessor, create_processor
-from ..generation.regex_manager import build_json_based_regex
+from ..generation.regex_manager import build_json_based_regex, build_json_structural_tag
from ..generation.results import GenerateJobResults, GenerationBatches, GenerationStatus
from ..llm.metadata import ModelMetadata
from ..llm.utils import ModelRef, cleanup_memory, get_max_vram
@@ -324,8 +328,12 @@ def _build_structured_output_params(self) -> StructuredOutputsParams | None:
return None
params: dict[str, Any] = {}
+ schema_method = resolve_structured_generation_schema_method(
+ self.config.generation.structured_generation_schema_method,
+ self.config.generation.structured_generation_backend,
+ )
- if self.config.generation.structured_generation_schema_method == "regex":
+ if schema_method == "regex":
logger.info("Structured generation is enabled, using a regex to enforce the schema")
pc = self.model_metadata.prompt_config
regex = build_json_based_regex(
@@ -335,8 +343,20 @@ def _build_structured_output_params(self) -> StructuredOutputsParams | None:
eos_token=pc.eos_token,
)
params["regex"] = regex
- elif self.config.generation.structured_generation_schema_method == "json_schema":
+ elif schema_method == "json_schema":
params["json"] = self.schema
+ elif schema_method == "structural_tag":
+ backend = self.config.generation.structured_generation_backend
+ if message := structural_tag_backend_error_message(backend):
+ raise ParameterError(message)
+ logger.info("Structured generation is enabled, using an XGrammar Structural Tag")
+ pc = self.model_metadata.prompt_config
+ params["structural_tag"] = build_json_structural_tag(
+ self.schema,
+ self.config,
+ bos_token=pc.bos_token,
+ eos_token=pc.eos_token,
+ )
return StructuredOutputsParams(**params)
diff --git a/tests/benchmarks/test_generation_structured_methods.py b/tests/benchmarks/test_generation_structured_methods.py
new file mode 100644
index 000000000..302aeb00a
--- /dev/null
+++ b/tests/benchmarks/test_generation_structured_methods.py
@@ -0,0 +1,294 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Benchmark vLLM generation across structured-output methods.
+
+This benchmark intentionally invokes the CLI in subprocesses. It is meant for
+comparing real generation runs against an already-trained adapter, not for unit
+testing the private backend helpers.
+
+Adapter setup:
+
+The benchmark expects `NSS_GENERATION_BENCHMARK_RUN_PATH` to point at a trained
+run directory containing adapter layers. It does not train adapters before
+benchmarking. To create the default local SmolLM3 DP adapter/run path, train on
+one of the benchmark input datasets:
+
+ uv run safe-synthesizer run train \
+ --data-source cleaned/amazon_reviews_25k.csv \
+ --config script/slurm/configs/smollm3-dp.yaml \
+ --run-path local_runs/smollm3-dp_amazon_reviews_25k_1_5609622_1
+
+Benchmark run:
+
+ uv run --frozen pytest tests/benchmarks/test_generation_structured_methods.py -m benchmark -n0 -s
+
+The default `cleaned/amazon_reviews_25k.csv` data source above is one of the
+benchmark input datasets. Use a different input CSV/config/run-path trio by
+setting the environment variables below.
+
+By default, this compares unstructured generation, XGrammar JSON schema,
+XGrammar Structural Tag, and JSON schema through the outlines, guidance, and
+lm-format-enforcer backends. Override `NSS_GENERATION_BENCHMARK_METHODS` with a
+comma-separated subset when you only need specific cases.
+
+Override inputs with:
+
+ NSS_GENERATION_BENCHMARK_DATA_SOURCE=cleaned/amazon_reviews_25k.csv
+ NSS_GENERATION_BENCHMARK_CONFIG=script/slurm/configs/smollm3-dp.yaml
+ NSS_GENERATION_BENCHMARK_RUN_PATH=local_runs/smollm3-dp_amazon_reviews_25k_1_5609622_1
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+import sys
+import time
+from dataclasses import asdict, dataclass
+from importlib import metadata
+from pathlib import Path
+
+import pytest
+
+# Keep these defaults aligned with the adapter setup command in the module
+# docstring so the benchmark can run without additional environment overrides.
+DEFAULT_DATA_SOURCE = "cleaned/amazon_reviews_25k.csv"
+DEFAULT_CONFIG = "script/slurm/configs/smollm3-dp.yaml"
+DEFAULT_RUN_PATH = "local_runs/smollm3-dp_amazon_reviews_25k_1_5609622_1"
+DEFAULT_METHODS = (
+ "unstructured",
+ "regex",
+ "json_schema",
+ "structural_tag",
+ "outlines",
+ "outlines_regex",
+ "guidance",
+ "lm_format_enforcer",
+)
+DEFAULT_TIMEOUT_SECONDS = 1800
+
+
+def _timeout_seconds() -> float:
+ return float(os.environ.get("NSS_GENERATION_BENCHMARK_TIMEOUT_SECONDS", str(DEFAULT_TIMEOUT_SECONDS)))
+
+
+@dataclass(frozen=True)
+class GenerationMethod:
+ name: str
+ use_structured_generation: bool
+ schema_method: str | None = None
+ backend: str = "xgrammar"
+
+
+@dataclass(frozen=True)
+class GenerationBenchmarkResult:
+ method: str
+ backend: str | None
+ schema_method: str | None
+ command: list[str]
+ duration_seconds: float
+ output_file: str
+ output_records: int
+ log_file: str
+
+
+def _cuda_available() -> bool:
+ try:
+ import torch
+
+ return bool(torch.cuda.is_available())
+ except ImportError:
+ return False
+
+
+def _package_version(package: str) -> str:
+ try:
+ return metadata.version(package)
+ except metadata.PackageNotFoundError:
+ return "not installed"
+
+
+def _configured_path(env_name: str, default: str, root: Path) -> Path:
+ path = Path(os.environ.get(env_name, default))
+ return path if path.is_absolute() else root / path
+
+
+def _selected_methods() -> list[GenerationMethod]:
+ methods = {
+ "unstructured": GenerationMethod("unstructured", use_structured_generation=False),
+ "regex": GenerationMethod("regex", use_structured_generation=True, schema_method="regex"),
+ "json_schema": GenerationMethod("json_schema", use_structured_generation=True, schema_method="json_schema"),
+ "structural_tag": GenerationMethod(
+ "structural_tag",
+ use_structured_generation=True,
+ schema_method="structural_tag",
+ ),
+ "outlines_regex": GenerationMethod(
+ "outlines_regex",
+ use_structured_generation=True,
+ schema_method="regex",
+ backend="outlines",
+ ),
+ "outlines": GenerationMethod(
+ "outlines",
+ use_structured_generation=True,
+ schema_method="json_schema",
+ backend="outlines",
+ ),
+ "guidance": GenerationMethod(
+ "guidance",
+ use_structured_generation=True,
+ schema_method="json_schema",
+ backend="guidance",
+ ),
+ "lm_format_enforcer": GenerationMethod(
+ "lm_format_enforcer",
+ use_structured_generation=True,
+ schema_method="json_schema",
+ backend="lm-format-enforcer",
+ ),
+ }
+ requested = os.environ.get("NSS_GENERATION_BENCHMARK_METHODS")
+ names = (
+ DEFAULT_METHODS if requested is None else tuple(name.strip() for name in requested.split(",") if name.strip())
+ )
+ unknown = sorted(set(names).difference(methods))
+ if unknown:
+ raise ValueError(f"Unknown generation benchmark methods: {', '.join(unknown)}")
+ return [methods[name] for name in names]
+
+
+def _count_csv_records(path: Path) -> int:
+ if not path.exists():
+ return 0
+ with path.open(encoding="utf-8") as handle:
+ line_count = sum(1 for _ in handle)
+ return max(line_count - 1, 0)
+
+
+def _tail(path: Path, max_lines: int = 80) -> str:
+ if not path.exists():
+ return ""
+ lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
+ return "\n".join(lines[-max_lines:])
+
+
+def _build_command(method: GenerationMethod, output_file: Path, root: Path) -> list[str]:
+ data_source = _configured_path("NSS_GENERATION_BENCHMARK_DATA_SOURCE", DEFAULT_DATA_SOURCE, root)
+ config = _configured_path("NSS_GENERATION_BENCHMARK_CONFIG", DEFAULT_CONFIG, root)
+ run_path = _configured_path("NSS_GENERATION_BENCHMARK_RUN_PATH", DEFAULT_RUN_PATH, root)
+ num_records = os.environ.get("NSS_GENERATION_BENCHMARK_NUM_RECORDS", "100")
+
+ command = [
+ sys.executable,
+ "-m",
+ "nemo_safe_synthesizer.cli.cli",
+ "run",
+ "generate",
+ "--data-source",
+ str(data_source),
+ "--config",
+ str(config),
+ "--run-path",
+ str(run_path),
+ "--output-file",
+ str(output_file),
+ "--generation__num_records",
+ num_records,
+ "--generation__use_structured_generation",
+ str(method.use_structured_generation).lower(),
+ ]
+ if method.use_structured_generation:
+ command.extend(
+ [
+ "--generation__structured_generation_backend",
+ method.backend,
+ "--generation__structured_generation_schema_method",
+ method.schema_method or "regex",
+ ]
+ )
+ return command
+
+
+@pytest.mark.benchmark
+@pytest.mark.slow
+@pytest.mark.requires_gpu
+@pytest.mark.vllm
+@pytest.mark.skipif(not _cuda_available(), reason="CUDA not available")
+@pytest.mark.timeout(_timeout_seconds() + 120)
+@pytest.mark.parametrize("method", _selected_methods(), ids=lambda method: method.name)
+def test_generation_structured_method_benchmark(
+ method: GenerationMethod,
+ tmp_path: Path,
+ pytestconfig: pytest.Config,
+) -> None:
+ """Benchmark one generation structured-output method."""
+ root = Path(pytestconfig.rootpath)
+ for env_name, default in (
+ ("NSS_GENERATION_BENCHMARK_DATA_SOURCE", DEFAULT_DATA_SOURCE),
+ ("NSS_GENERATION_BENCHMARK_CONFIG", DEFAULT_CONFIG),
+ ("NSS_GENERATION_BENCHMARK_RUN_PATH", DEFAULT_RUN_PATH),
+ ):
+ path = _configured_path(env_name, default, root)
+ if not path.exists():
+ pytest.skip(f"{env_name} path does not exist: {path}")
+
+ timeout = _timeout_seconds()
+ output_file = tmp_path / f"{method.name}.csv"
+ log_file = tmp_path / f"{method.name}.log"
+ command = _build_command(method, output_file, root)
+
+ print(f"\nRunning {method.name} generation benchmark")
+ print(f"vLLM version: {_package_version('vllm')}")
+ print(f"XGrammar version: {_package_version('xgrammar')}")
+ print(f"Command: {' '.join(command)}")
+ print(f"Log file: {log_file}")
+
+ start = time.perf_counter()
+ try:
+ with log_file.open("w", encoding="utf-8") as log:
+ completed = subprocess.run(
+ command,
+ cwd=root,
+ stdout=log,
+ stderr=subprocess.STDOUT,
+ text=True,
+ timeout=timeout,
+ check=False,
+ )
+ except subprocess.TimeoutExpired as exc:
+ pytest.fail(
+ f"{method.name} generation benchmark timed out after {exc.timeout:.0f}s\n"
+ f"Command: {' '.join(command)}\n\n{_tail(log_file)}"
+ )
+ duration = time.perf_counter() - start
+ if completed.returncode != 0:
+ pytest.fail(
+ f"{method.name} generation benchmark failed with exit code {completed.returncode}\n"
+ f"Command: {' '.join(command)}\n\n{_tail(log_file)}"
+ )
+
+ result = GenerationBenchmarkResult(
+ method=method.name,
+ backend=method.backend if method.use_structured_generation else None,
+ schema_method=method.schema_method,
+ command=command,
+ duration_seconds=duration,
+ output_file=str(output_file),
+ output_records=_count_csv_records(output_file),
+ log_file=str(log_file),
+ )
+ records_per_second = result.output_records / result.duration_seconds if result.duration_seconds > 0 else 0.0
+ print(
+ f"\nGeneration benchmark result: {result.method}: "
+ f"{result.duration_seconds:.2f}s, {result.output_records} records, "
+ f"{records_per_second:.3f} records/s"
+ )
+
+ summary_file = tmp_path / f"generation_structured_method_{method.name}_benchmark.json"
+ summary_file.write_text(json.dumps(asdict(result), indent=2) + "\n", encoding="utf-8")
+ print(f"Benchmark summary JSON: {summary_file}")
+
+ assert result.output_records > 0
diff --git a/tests/config/test_generate.py b/tests/config/test_generate.py
new file mode 100644
index 000000000..3749e4d35
--- /dev/null
+++ b/tests/config/test_generate.py
@@ -0,0 +1,72 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+from typing import Any, Literal, cast
+
+import pytest
+from pydantic import ValidationError
+
+from nemo_safe_synthesizer.config.generate import (
+ GenerateParameters,
+ resolve_structured_generation_schema_method,
+)
+from nemo_safe_synthesizer.config.parameters import SafeSynthesizerParameters
+
+
+@pytest.mark.unit
+class TestResolveStructuredGenerationSchemaMethod:
+ @pytest.mark.parametrize(
+ ("backend", "expected"),
+ [
+ ("auto", "structural_tag"),
+ ("xgrammar", "structural_tag"),
+ ("outlines", "regex"),
+ ("guidance", "regex"),
+ ("lm-format-enforcer", "regex"),
+ ],
+ )
+ def test_auto_resolves_from_backend(self, backend: str, expected: str) -> None:
+ assert resolve_structured_generation_schema_method("auto", backend) == expected
+
+ @pytest.mark.parametrize("method", ["regex", "json_schema", "structural_tag"])
+ def test_explicit_methods_pass_through(self, method: str) -> None:
+ schema_method = cast(Literal["regex", "json_schema", "structural_tag"], method)
+ assert resolve_structured_generation_schema_method(schema_method, "outlines") == method
+
+
+@pytest.mark.unit
+class TestGenerateParametersStructuralTagValidation:
+ @staticmethod
+ def _generation_kwargs(*, schema_method: str = "structural_tag", backend: str = "xgrammar") -> dict[str, Any]:
+ return {
+ "use_structured_generation": True,
+ "structured_generation_schema_method": schema_method,
+ "structured_generation_backend": backend,
+ }
+
+ @pytest.mark.parametrize("backend", ["xgrammar", "auto"])
+ def test_compatible_backends_validate(self, backend: str) -> None:
+ GenerateParameters(**self._generation_kwargs(backend=backend))
+
+ def test_incompatible_backend_raises_validation_error(self) -> None:
+ with pytest.raises(ValidationError, match="requires `structured_generation_backend`"):
+ GenerateParameters(**self._generation_kwargs(backend="outlines"))
+
+ @pytest.mark.parametrize("backend", ["outlines", "guidance", "lm-format-enforcer"])
+ def test_auto_with_incompatible_backend_validates(self, backend: str) -> None:
+ GenerateParameters(**self._generation_kwargs(schema_method="auto", backend=backend))
+
+ def test_default_schema_method_is_auto(self) -> None:
+ params = GenerateParameters()
+ assert params.structured_generation_schema_method == "auto"
+
+ def test_skipped_when_structured_generation_disabled(self) -> None:
+ GenerateParameters(
+ use_structured_generation=False,
+ structured_generation_schema_method="structural_tag",
+ structured_generation_backend="outlines",
+ )
+
+ def test_from_params_rejects_incompatible_backend(self) -> None:
+ with pytest.raises(ValidationError, match="outlines"):
+ SafeSynthesizerParameters.from_params(**self._generation_kwargs(backend="outlines"))
diff --git a/tests/e2e/test_safe_synthesizer.py b/tests/e2e/test_safe_synthesizer.py
index 6846a4b1b..d71fa2e9c 100644
--- a/tests/e2e/test_safe_synthesizer.py
+++ b/tests/e2e/test_safe_synthesizer.py
@@ -53,7 +53,7 @@ def test_train_and_generate_dp(fixture_financial_transactions_dataset, fixture_s
epsilon=100.0,
num_records=100,
use_structured_generation=True,
- structured_generation_backend="outlines",
+ structured_generation_backend="xgrammar",
)
logger.info(f"Running DP test with config: {config}")
diff --git a/tests/generation/structural_tag_helpers.py b/tests/generation/structural_tag_helpers.py
new file mode 100644
index 000000000..188b66521
--- /dev/null
+++ b/tests/generation/structural_tag_helpers.py
@@ -0,0 +1,35 @@
+# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Test helpers for validating XGrammar Structural Tag constraints."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+if TYPE_CHECKING:
+ from transformers import PreTrainedTokenizerBase
+
+__all__ = ["structural_tag_accepts_text"]
+
+
+def structural_tag_accepts_text(
+ text: str,
+ structural_tag_json: str,
+ tokenizer: PreTrainedTokenizerBase,
+) -> bool:
+ """Return whether *text* is fully accepted by an XGrammar Structural Tag.
+
+ Mirrors regex round-trip tests that use ``re.fullmatch`` against
+ ``build_json_based_regex`` output. Requires xgrammar and a Hugging Face
+ tokenizer compatible with the generation backend.
+ """
+ xgr = pytest.importorskip("xgrammar", reason="xgrammar is required for structural tag acceptance tests")
+
+ compiler = xgr.GrammarCompiler(xgr.TokenizerInfo.from_huggingface(tokenizer))
+ compiled = compiler.compile_structural_tag(structural_tag_json)
+ matcher = xgr.GrammarMatcher(compiled)
+ matcher.reset()
+ return bool(matcher.accept_string(text) and matcher.is_completed())
diff --git a/tests/generation/test_regex_manager.py b/tests/generation/test_regex_manager.py
index 5fe6076e5..27eb1bd06 100644
--- a/tests/generation/test_regex_manager.py
+++ b/tests/generation/test_regex_manager.py
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
+import json
import re
from io import StringIO
@@ -11,8 +12,11 @@
from nemo_safe_synthesizer.data_processing.dataset import make_json_schema
from nemo_safe_synthesizer.generation.regex_manager import (
build_json_based_regex,
+ build_json_structural_tag,
)
+from .structural_tag_helpers import structural_tag_accepts_text
+
BOS_TOKEN = ""
EOS_TOKEN = ""
@@ -68,6 +72,133 @@ def test_build_json_based_regex(fixture_valid_iris_dataset_jsonl_and_schema, fix
)
+def test_build_json_structural_tag_uses_schema_constrained_jsonl(fixture_safe_synthesizer_config):
+ """Structural Tag composes schema-constrained JSON records with JSONL newlines."""
+ schema = {
+ "type": "object",
+ "properties": {"name": {"type": "string"}},
+ "required": ["name"],
+ }
+
+ structural_tag = json.loads(
+ build_json_structural_tag(
+ schema,
+ config=fixture_safe_synthesizer_config,
+ bos_token=BOS_TOKEN,
+ eos_token=EOS_TOKEN,
+ )
+ )
+
+ assert structural_tag == {
+ "type": "structural_tag",
+ "format": {
+ "type": "plus",
+ "content": {
+ "type": "sequence",
+ "elements": [
+ {"type": "json_schema", "json_schema": schema},
+ {"type": "const_string", "value": "\n"},
+ ],
+ },
+ },
+ }
+
+
+def test_build_json_structural_tag_with_single_sequence(fixture_safe_synthesizer_config):
+ """Single-sequence Structural Tag emits exactly one schema-constrained object."""
+ fixture_safe_synthesizer_config.data.max_sequences_per_example = 1
+ fixture_safe_synthesizer_config.generation.structured_generation_use_single_sequence = True
+ schema = {"type": "object", "properties": {"name": {"type": "string"}}}
+
+ structural_tag = json.loads(
+ build_json_structural_tag(
+ schema,
+ config=fixture_safe_synthesizer_config,
+ bos_token=BOS_TOKEN,
+ eos_token=EOS_TOKEN,
+ )
+ )
+
+ assert structural_tag["format"] == {"type": "json_schema", "json_schema": schema}
+
+
+def test_build_json_structural_tag_with_groupby(fixture_safe_synthesizer_config):
+ """Grouped Structural Tag keeps BOS/EOS framing around repeated JSONL records."""
+ fixture_safe_synthesizer_config.data.group_training_examples_by = "id"
+ schema = {"type": "object", "properties": {"name": {"type": "string"}}}
+
+ structural_tag = json.loads(
+ build_json_structural_tag(
+ schema,
+ config=fixture_safe_synthesizer_config,
+ bos_token=BOS_TOKEN,
+ eos_token=EOS_TOKEN,
+ )
+ )
+
+ assert structural_tag["format"] == {
+ "type": "plus",
+ "content": {
+ "type": "sequence",
+ "elements": [
+ {
+ "type": "sequence",
+ "elements": [
+ {"type": "const_string", "value": BOS_TOKEN},
+ {
+ "type": "plus",
+ "content": {
+ "type": "sequence",
+ "elements": [
+ {"type": "json_schema", "json_schema": schema},
+ {"type": "const_string", "value": "\n"},
+ ],
+ },
+ },
+ {"type": "const_string", "value": EOS_TOKEN},
+ ],
+ },
+ {"type": "const_string", "value": "\n"},
+ ],
+ },
+ }
+
+
+def test_build_json_structural_tag_with_groupby_single_sequence(fixture_safe_synthesizer_config):
+ """Grouped single-sequence Structural Tag emits exactly one BOS/EOS-wrapped sequence."""
+ fixture_safe_synthesizer_config.data.group_training_examples_by = "id"
+ fixture_safe_synthesizer_config.data.max_sequences_per_example = 1
+ fixture_safe_synthesizer_config.generation.structured_generation_use_single_sequence = True
+ schema = {"type": "object", "properties": {"name": {"type": "string"}}}
+
+ structural_tag = json.loads(
+ build_json_structural_tag(
+ schema,
+ config=fixture_safe_synthesizer_config,
+ bos_token=BOS_TOKEN,
+ eos_token=EOS_TOKEN,
+ )
+ )
+
+ assert structural_tag["format"] == {
+ "type": "sequence",
+ "elements": [
+ {"type": "const_string", "value": BOS_TOKEN},
+ {
+ "type": "plus",
+ "content": {
+ "type": "sequence",
+ "elements": [
+ {"type": "json_schema", "json_schema": schema},
+ {"type": "const_string", "value": "\n"},
+ ],
+ },
+ },
+ {"type": "const_string", "value": EOS_TOKEN},
+ ],
+ }
+
+
# Purpose: Ensures keys with special chars (e.g., '.') are escaped and string length bounds enforced.
# Data: Object with required key 'full.name', minLength=3, maxLength=10.
# Asserts: Equality against expected regex and behavioral matches via re.fullmatch for valid/invalid cases.
@@ -426,6 +557,73 @@ def test_property_regex(fixture_safe_synthesizer_config):
assert re.fullmatch(regex, '{"b":2,"a":1}\n') is None
+# Purpose: XGrammar Structural Tag round-trip via GrammarMatcher for each output shape.
+@pytest.mark.parametrize(
+ ("config_updates", "text"),
+ [
+ ({}, '{"name":"alice"}\n{"name":"bob"}\n'),
+ (
+ {"max_sequences_per_example": 1, "structured_generation_use_single_sequence": True},
+ '{"name":"alice"}',
+ ),
+ (
+ {"group_training_examples_by": "id"},
+ '{"name":"alice"}\n\n{"name":"bob"}\n\n',
+ ),
+ (
+ {
+ "group_training_examples_by": "id",
+ "max_sequences_per_example": 1,
+ "structured_generation_use_single_sequence": True,
+ },
+ '{"name":"alice"}\n',
+ ),
+ ],
+ ids=["jsonl", "single_sequence", "grouped_jsonl", "grouped_single_sequence"],
+)
+def test_structural_tag_accepts_training_jsonl_shapes(
+ config_updates,
+ text,
+ fixture_safe_synthesizer_config,
+ fixture_tokenizer,
+):
+ for key, value in config_updates.items():
+ if key == "group_training_examples_by":
+ fixture_safe_synthesizer_config.data.group_training_examples_by = value
+ elif key == "max_sequences_per_example":
+ fixture_safe_synthesizer_config.data.max_sequences_per_example = value
+ elif key == "structured_generation_use_single_sequence":
+ fixture_safe_synthesizer_config.generation.structured_generation_use_single_sequence = value
+
+ schema = {
+ "type": "object",
+ "properties": {"name": {"type": "string"}},
+ "required": ["name"],
+ }
+ structural_tag = build_json_structural_tag(
+ schema,
+ config=fixture_safe_synthesizer_config,
+ bos_token=BOS_TOKEN,
+ eos_token=EOS_TOKEN,
+ )
+ assert structural_tag_accepts_text(text, structural_tag, fixture_tokenizer) is True
+
+
+def test_round_trip_structural_tag_rejects_invalid_jsonl(fixture_safe_synthesizer_config, fixture_tokenizer):
+ schema = {
+ "type": "object",
+ "properties": {"name": {"type": "string"}},
+ "required": ["name"],
+ }
+ structural_tag = build_json_structural_tag(
+ schema,
+ config=fixture_safe_synthesizer_config,
+ bos_token=BOS_TOKEN,
+ eos_token=EOS_TOKEN,
+ )
+ assert structural_tag_accepts_text('{"name":123}\n', structural_tag, fixture_tokenizer) is False
+
+
# Purpose: Round-trip regression: DataFrame -> schema -> regex must match the original JSONL rows.
# Data: First 5 rows of Iris dataset serialized to JSONL; same schema used to build regex.
# Asserts: Non-grouped JSONL matches; grouped (BOS/EOS-wrapped) matches when group_by=True.
diff --git a/tests/generation/test_vllm_backend.py b/tests/generation/test_vllm_backend.py
index 5d296d0e2..e084c0122 100644
--- a/tests/generation/test_vllm_backend.py
+++ b/tests/generation/test_vllm_backend.py
@@ -17,6 +17,7 @@
)
from nemo_safe_synthesizer.config.generate import ValidationParameters
from nemo_safe_synthesizer.defaults import DEFAULT_SAMPLING_PARAMETERS
+from nemo_safe_synthesizer.errors import ParameterError
from nemo_safe_synthesizer.generation.processors import TabularDataProcessor
from nemo_safe_synthesizer.llm.metadata import ModelMetadata
@@ -96,6 +97,14 @@ def base_params():
)
+@pytest.fixture
+def params_with_structured_generation_auto(base_params):
+ """Create params with structured generation enabled using auto schema method."""
+ base_params.generation.use_structured_generation = True
+ base_params.generation.structured_generation_schema_method = "auto"
+ return base_params
+
+
@pytest.fixture
def params_with_structured_generation_regex(base_params):
"""Create params with structured generation enabled using regex."""
@@ -114,6 +123,15 @@ def params_with_structured_generation_json(base_params):
return base_params
+@pytest.fixture
+def params_with_structured_generation_structural_tag(base_params):
+ """Create params with structured generation enabled using structural_tag."""
+ base_params.generation.use_structured_generation = True
+ base_params.generation.structured_generation_schema_method = "structural_tag"
+ base_params.generation.structured_generation_backend = "xgrammar"
+ return base_params
+
+
@pytest.fixture
def mock_schema():
"""Create a mock JSON schema."""
@@ -208,6 +226,120 @@ def test_returns_params_with_json_when_json_schema_method(
assert result is not None
assert result.json == mock_schema
+ def test_returns_params_with_structural_tag_when_structural_tag_method(
+ self,
+ params_with_structured_generation_structural_tag,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ ):
+ """Test that structural_tag uses vLLM's Structural Tag constraint."""
+ backend = create_backend(
+ params_with_structured_generation_structural_tag,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ )
+
+ with patch(
+ "nemo_safe_synthesizer.generation.vllm_backend.build_json_structural_tag",
+ return_value='{"type":"structural_tag","format":{"type":"json_schema","json_schema":{}}}',
+ ) as mock_build_structural_tag:
+ result = backend._build_structured_output_params()
+ mock_build_structural_tag.assert_called_once_with(
+ mock_schema,
+ params_with_structured_generation_structural_tag,
+ bos_token=mock_model_metadata.prompt_config.bos_token,
+ eos_token=mock_model_metadata.prompt_config.eos_token,
+ )
+ assert result is not None
+ assert result.structural_tag == '{"type":"structural_tag","format":{"type":"json_schema","json_schema":{}}}'
+
+ @pytest.mark.parametrize("backend", ["auto", "xgrammar"])
+ def test_auto_resolves_to_structural_tag_on_xgrammar_backends(
+ self,
+ params_with_structured_generation_auto,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ backend,
+ ):
+ """Auto schema method uses structural_tag on xgrammar-capable backends."""
+ params_with_structured_generation_auto.generation.structured_generation_backend = backend
+ backend_instance = create_backend(
+ params_with_structured_generation_auto,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ )
+
+ with patch(
+ "nemo_safe_synthesizer.generation.vllm_backend.build_json_structural_tag",
+ return_value='{"type":"structural_tag","format":{"type":"json_schema","json_schema":{}}}',
+ ) as mock_build_structural_tag:
+ result = backend_instance._build_structured_output_params()
+ mock_build_structural_tag.assert_called_once_with(
+ mock_schema,
+ params_with_structured_generation_auto,
+ bos_token=mock_model_metadata.prompt_config.bos_token,
+ eos_token=mock_model_metadata.prompt_config.eos_token,
+ )
+ assert result is not None
+ assert result.structural_tag is not None
+
+ @pytest.mark.parametrize("backend", ["guidance", "outlines", "lm-format-enforcer"])
+ def test_auto_resolves_to_regex_on_other_backends(
+ self,
+ params_with_structured_generation_auto,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ backend,
+ ):
+ """Auto schema method falls back to regex on non-xgrammar backends."""
+ params_with_structured_generation_auto.generation.structured_generation_backend = backend
+ backend_instance = create_backend(
+ params_with_structured_generation_auto,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ )
+
+ with patch(
+ "nemo_safe_synthesizer.generation.vllm_backend.build_json_based_regex",
+ return_value="test_regex_pattern",
+ ) as mock_build_regex:
+ result = backend_instance._build_structured_output_params()
+ mock_build_regex.assert_called_once_with(
+ mock_schema,
+ params_with_structured_generation_auto,
+ bos_token=mock_model_metadata.prompt_config.bos_token,
+ eos_token=mock_model_metadata.prompt_config.eos_token,
+ )
+ assert result is not None
+ assert result.regex == "test_regex_pattern"
+
+ @pytest.mark.parametrize("backend", ["guidance", "outlines", "lm-format-enforcer"])
+ def test_structural_tag_rejects_non_xgrammar_backends(
+ self,
+ params_with_structured_generation_structural_tag,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ backend,
+ ):
+ """Structural Tag requires vLLM's xgrammar backend."""
+ params_with_structured_generation_structural_tag.generation.structured_generation_backend = backend
+ backend_instance = create_backend(
+ params_with_structured_generation_structural_tag,
+ mock_model_metadata,
+ mock_schema,
+ mock_workdir,
+ )
+
+ with pytest.raises(ParameterError, match="requires `structured_generation_backend`"):
+ backend_instance._build_structured_output_params()
+
def test_config_with_grouping_passed_to_build_regex(
self, params_with_structured_generation_regex, mock_model_metadata, mock_schema, mock_workdir
):
diff --git a/uv.lock b/uv.lock
index a06cf84c5..980f4e621 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3109,6 +3109,7 @@ cpu = [
{ name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "trl", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "vllm", version = "0.20.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "xgrammar", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
cu129 = [
{ name = "accelerate", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
@@ -3132,6 +3133,7 @@ cu129 = [
{ name = "triton", version = "3.6.0", source = { registry = "https://download.pytorch.org/whl/cu129" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "trl", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "vllm", version = "0.20.0+cu129", source = { registry = "https://wheels.vllm.ai/88d34c6409e9fb3c7b8ca0c04756f061d2099eb1/cu129" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+ { name = "xgrammar", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
engine = [
{ name = "anyascii", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129') or (sys_platform == 'darwin' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129') or (sys_platform == 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129')" },
@@ -3295,6 +3297,8 @@ requires-dist = [
{ name = "vllm", marker = "sys_platform == 'linux' and extra == 'cpu'", specifier = "==0.20.0" },
{ name = "vllm", marker = "sys_platform == 'linux' and extra == 'cu129'", specifier = "==0.20.0+cu129", index = "https://wheels.vllm.ai/88d34c6409e9fb3c7b8ca0c04756f061d2099eb1/cu129", conflict = { package = "nemo-safe-synthesizer", extra = "cu129" } },
{ name = "wandb", marker = "extra == 'engine'", specifier = "==0.26.1" },
+ { name = "xgrammar", marker = "sys_platform == 'linux' and extra == 'cpu'", specifier = ">=0.2.0" },
+ { name = "xgrammar", marker = "sys_platform == 'linux' and extra == 'cu129'", specifier = ">=0.2.0" },
]
provides-extras = ["cpu", "cu129", "engine"]
@@ -6938,9 +6942,10 @@ wheels = [
[[package]]
name = "xgrammar"
-version = "0.1.32"
+version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "apache-tvm-ffi", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "numpy", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "pydantic", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "(platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129') or (platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu') or (platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu') or (sys_platform != 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129')" },
@@ -6950,22 +6955,22 @@ dependencies = [
{ name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu') or (platform_machine != 'x86_64' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129') or (sys_platform != 'linux' and extra == 'extra-21-nemo-safe-synthesizer-cpu' and extra == 'extra-21-nemo-safe-synthesizer-cu129')" },
{ name = "typing-extensions", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/99/6a/d51b44fc0b43e2d4adae42b6a17fe9ee49e177d6d768be739ed7dec7b57e/xgrammar-0.1.32.tar.gz", hash = "sha256:5d424d52779ca2d3ccaf72f2289d6519efe308e933d0d3fc3c292c780825bb12", size = 2365047, upload-time = "2026-03-04T12:01:52.544Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/28/cd/4b5e67c8030b626a1a00b65b4d149b1b031c885eef86d4e5fa296f6ec72e/xgrammar-0.1.32-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:51b41c47785aa198d19f8d056b394f75b4421deab88c415568f9c588b1f7e238", size = 18425822, upload-time = "2026-03-04T12:00:23.356Z" },
- { url = "https://files.pythonhosted.org/packages/5c/c0/94fbc45642e733a9ad4a9f3f7300a1a06b265f8657af4d6a56acd8cf00c4/xgrammar-0.1.32-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7030192cb1d8579699f1f72fd14d31347a402611aab98a2da6a04c3de07e917", size = 20582669, upload-time = "2026-03-04T12:00:26.463Z" },
- { url = "https://files.pythonhosted.org/packages/90/ea/2f4c8616d8ed0b5a3eb4e417b4987ad5a8d9dd9336ed966a8d48ffd45907/xgrammar-0.1.32-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a332c0364f665b410a6cfc2ada155c3a6ede430e385ac431015e31735a64fec3", size = 37682948, upload-time = "2026-03-04T12:00:29.814Z" },
- { url = "https://files.pythonhosted.org/packages/ac/ae/b9108fadd354ae776c1e7ecd26890a13ac8a30367f9fe8110443aedc4e6a/xgrammar-0.1.32-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b8ad132d0fcf3a51dc054ecb0dc9808566b302122de6edaac7b4aca460adbec", size = 37709617, upload-time = "2026-03-04T12:00:33.068Z" },
- { url = "https://files.pythonhosted.org/packages/9d/48/0096bd1f3b460eac48faaecf79418ea3172269dccf37968e78dff5114faf/xgrammar-0.1.32-cp311-cp311-win_amd64.whl", hash = "sha256:b8b1ca6d3f3c2842660458660e494aaf0a6745f1b07ae74e4c2230ab4ff70c11", size = 6632722, upload-time = "2026-03-04T12:00:36.133Z" },
- { url = "https://files.pythonhosted.org/packages/9f/fd/5e771276fa090e35eaf1cbfdede24b9d93d6bbd2e99cd4f8d558f381fdee/xgrammar-0.1.32-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:9b78d32265f096e5567ab52c72b681855cf473481a48a1e7e6d97d414ba30b82", size = 18425090, upload-time = "2026-03-04T12:00:38.5Z" },
- { url = "https://files.pythonhosted.org/packages/31/66/f06745755ef0750f43955cf679b4bd8bd88ac8bfab760f020225c192884f/xgrammar-0.1.32-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23eacaf826c3aeebca0d91fc271417d9d96e157af2bacf6f14277297af7917ef", size = 20582048, upload-time = "2026-03-04T12:00:42.369Z" },
- { url = "https://files.pythonhosted.org/packages/79/29/3b0306800ccabce8f565123a5b97432dee43822c30142085d9b13b43f166/xgrammar-0.1.32-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a637d4e0c541149e0d409c24f4ec79cd74d87508ee6a17a7e64a9b9c0cf56f", size = 37680849, upload-time = "2026-03-04T12:00:46.712Z" },
- { url = "https://files.pythonhosted.org/packages/69/62/65e664d861cdadf2d788c03dd8fe67f1faaa7bd4bd2317a2ab850aebee20/xgrammar-0.1.32-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96c7a4fcbd68e18b13cb3b6ed5d24b5326b256933f476bdaf2cc8e609c228db", size = 37711100, upload-time = "2026-03-04T12:00:50.188Z" },
- { url = "https://files.pythonhosted.org/packages/80/43/05f27a1739209eb590772f867f3f48e6db0a36f376d85db4e68f49aee799/xgrammar-0.1.32-cp312-cp312-win_amd64.whl", hash = "sha256:ba6e08c385cce53eda8e9b3bbfba63f100ba3dcb76fa0692a65921a36b20ad0a", size = 6632259, upload-time = "2026-03-04T12:00:53.184Z" },
- { url = "https://files.pythonhosted.org/packages/7b/58/b4ff220b28d7d6a4ccf5c229ddbabc7018cd9544356ac8a161086e7a7a0e/xgrammar-0.1.32-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4addb8f5d5699e7df7fca6d299a91b3ef1ad799811c0ab7050d6f96d754c9c21", size = 20582005, upload-time = "2026-03-04T12:00:55.089Z" },
- { url = "https://files.pythonhosted.org/packages/83/95/9fedafd412af05b1d61859c52fd9d26abc9a167fab66bdad53f832da0956/xgrammar-0.1.32-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:028f8d6a105d06549faee0afbebfaada90aa1941c081dcc88f3d5ef373dad934", size = 37680882, upload-time = "2026-03-04T12:00:59.456Z" },
- { url = "https://files.pythonhosted.org/packages/0a/21/a9d328ae9ff4e794281995de3a1f8065517bb9bef70f099ab24f7743b3be/xgrammar-0.1.32-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0150c50eb3a56a35d6f0c0af0bce0f113ec5f84f7918bfd46b49e25ecf7fb5", size = 37710862, upload-time = "2026-03-04T12:01:02.739Z" },
- { url = "https://files.pythonhosted.org/packages/28/dc/8ecf71ad1e9c96fd941d2e9a852e184054596eeb1799de8b2e172eaf705e/xgrammar-0.1.32-cp313-cp313-win_amd64.whl", hash = "sha256:e1072d764705c8e87df6136ce3419f96ab3fd423d85f58c2d81c13a647b78894", size = 6632312, upload-time = "2026-03-04T12:01:05.474Z" },
+sdist = { url = "https://files.pythonhosted.org/packages/d8/ea/6394caddd078d33772070eefaaf77cb0a826a3047b908b688dece7d040b5/xgrammar-0.2.1.tar.gz", hash = "sha256:4c48c251b75d211e9ffa7f4f4ac8b5b0164f89fd5f0d1883ad7ff4554922030d", size = 2427065, upload-time = "2026-05-17T21:39:26.576Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/9e/e1f35da58272099af49eee18545a60d0834d5058b3ad11a7a754d084ca5b/xgrammar-0.2.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:62ac4b8ed9eeb7d48c71c3d4c641bdc0e11f1b4c20c4b36ee670412238d9854d", size = 23290996, upload-time = "2026-05-17T21:37:24.886Z" },
+ { url = "https://files.pythonhosted.org/packages/df/4c/2839328d7577378968db54c5dc2324ed1ffd1f212219f8543f24a7becd4c/xgrammar-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7dfe4b50d12325dc50f65ec837c03e02f2e2c366b7ae88d93bdcf4351e8bd440", size = 23202401, upload-time = "2026-05-17T21:37:29.298Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/a6/838795496cd32429cbb6a05aeb3f9fec92d33a0ad47784666b251c52ef23/xgrammar-0.2.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f16c1adf6bf31d0f2ba40a7fc5df69a01757641a06e1cbf17f2143a830180646", size = 44218971, upload-time = "2026-05-17T21:37:35.158Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c1/b31a56ef2d1e2083da4c0db1aefcc3772556675e4ac8b6ed99a968eea0b1/xgrammar-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a1ae52372ae981c518a83576209343ec0e79a4ac3a1ed104b0126b870c0ca3b", size = 44678458, upload-time = "2026-05-17T21:37:40.91Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/9f/e12dca5a023ee38f4301b8945cdc516825a434c9d0f8e4e15843714e5e78/xgrammar-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8a0f7cc19f058dd9d210b55c1429aea2efb5ab25602885661460bae864c7012d", size = 7409599, upload-time = "2026-05-17T21:37:44.37Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/87/af20928af1c7773b1d064743de0f291698dd531e5deee19fcda388c3495e/xgrammar-0.2.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0560568654e7745a80715dd87457f1feb3a4edf9d3894e47ab5c967f5d799ade", size = 23290997, upload-time = "2026-05-17T21:37:47.413Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/e3/9b803c8168290421b3e79b001a823a7e381039fcdd349999c2eeaf454989/xgrammar-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29756a266a62da398151946f3d912a5b737df2000a7023696449f9eec0996208", size = 23202397, upload-time = "2026-05-17T21:37:50.987Z" },
+ { url = "https://files.pythonhosted.org/packages/44/60/7c6194b66e043f36a7fbdbf7e6e0e4c94b151f7e53ad1b197f53e711f5d1/xgrammar-0.2.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e8dd9853958a263b4015ce79133a0ff4eaa9d22ef781fb2350c7dfc40c2c012", size = 44218929, upload-time = "2026-05-17T21:37:56.155Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4b/327b3cf702b685a2be28d15490faa4beeac00c4fbcf9bb2d7db0fda32931/xgrammar-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbc6014dc1c92fc317b14519121c8163fe35fd934179e5a45d83f780ff231826", size = 44678489, upload-time = "2026-05-17T21:38:00.791Z" },
+ { url = "https://files.pythonhosted.org/packages/57/39/69a5ba4dfa5e11a36265f69de3e16ee65ac9e77840c010056a2d8e99a875/xgrammar-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:9bd16e92b4385cb5ded48d65de80e4ee871b6b427b2ded99ac5028b907bf870f", size = 7409599, upload-time = "2026-05-17T21:38:03.829Z" },
+ { url = "https://files.pythonhosted.org/packages/58/57/31d118a787debf1a0ab9c6a632e17373bffbaca65250c036498f58934246/xgrammar-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2e1964e2a1a5f59e138205996b8b41128c2f385a4de952292e8e47add556aa3c", size = 23202388, upload-time = "2026-05-17T21:38:07.476Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ef/8b5a8f103818a2a2b779c6ca21b5cda3536c78800bf25c6a37f87b130877/xgrammar-0.2.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb015d246a8c87bf46b11759442924cd6f96baf3ba444062a98e9a45ec5a1021", size = 44218981, upload-time = "2026-05-17T21:38:13.295Z" },
+ { url = "https://files.pythonhosted.org/packages/32/75/25ddd211f073a9db8299bdfec4534874d5d5d5f69499bb0c2ce9bf75f483/xgrammar-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba024024a454f31d3cb88679a1b073eba0133860f14525f1f5856fa46dec5228", size = 44678423, upload-time = "2026-05-17T21:38:18.812Z" },
+ { url = "https://files.pythonhosted.org/packages/98/67/f99c7a0cd6221d6db6b67d7d987ef7bbb99e52bdd97d6d7d515e28c57e64/xgrammar-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:58ab4d1b2a9f23e38e9a53ceb30ae5c8363e9a9e14d686bbb9f06147fb2e050c", size = 7409601, upload-time = "2026-05-17T21:38:22.229Z" },
]
[[package]]