From feca2310d136230715c0005232ed296ae19b0000 Mon Sep 17 00:00:00 2001 From: Nolan Date: Tue, 27 Oct 2026 12:18:35 +0800 Subject: [PATCH] fix(so): reject an unsupported schema_type at the request boundary An unknown schema_type failed three layers away from its cause: the compiler raised ValueError('Unsupported Grammar Type') inside the try block, the except ValueError meant for xgrammar's own input errors rewrapped it as RuntimeError('xgrammar invalid input: ...'), and try_result()'s except RuntimeError branch then reported it as an xgrammar timeout. A typo in schema_type surfaced as a timeout. Expose the supported types from the compiler and check the value in build() next to the presence check, so a wrong schema_type fails the same synchronous way a missing one does. The compiler keeps its own guard, moved ahead of the try so it is no longer rewrapped. --- deepinfer/structure_output/compiler.py | 13 ++++++++++--- deepinfer/structure_output/so_matcher_builder.py | 4 +++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/deepinfer/structure_output/compiler.py b/deepinfer/structure_output/compiler.py index 4bbcddf..829d4e4 100644 --- a/deepinfer/structure_output/compiler.py +++ b/deepinfer/structure_output/compiler.py @@ -10,6 +10,10 @@ from deepinfer import envs logger = logging.getLogger(__name__) +#: The ``schema_type`` values ``dispatch_and_compile`` knows how to compile. Shared with the +#: matcher builder so a wrong type is rejected at the request boundary, not inside the pool. +SUPPORTED_SCHEMA_TYPES: tuple[str, ...] = ("json", "regex", "ebnf") + class XGrammarCompiler: def __init__(self, tokenizer_info: TokenizerInfo, num_threads: int) -> None: @@ -52,15 +56,18 @@ class XGrammarCompiler: self.close() def dispatch_and_compile(self, schema_type: str, schema: str) -> CompiledGrammar: + # Checked outside the try: the `except ValueError` below is for xgrammar's own input + # errors and used to swallow this one too, re-raising it as a RuntimeError that + # try_result() then reported as a timeout. + if schema_type not in SUPPORTED_SCHEMA_TYPES: + raise ValueError(f"unsupported schema_type {schema_type!r}; expected one of {SUPPORTED_SCHEMA_TYPES}") try: if schema_type == "json": ctx = self.inner_compiler.compile_json_schema(schema, strict_mode=True) elif schema_type == "regex": ctx = self.inner_compiler.compile_regex(schema) - elif schema_type == "ebnf": + else: # "ebnf" — the guard above makes this exhaustive ctx = self.inner_compiler.compile_grammar(schema) - else: - raise ValueError("Unsupported Grammar Type") except RuntimeError as e: raise RuntimeError(f"xgrammar compilation RE: {e}") from e except ValueError as e: diff --git a/deepinfer/structure_output/so_matcher_builder.py b/deepinfer/structure_output/so_matcher_builder.py index ab3b867..2afc2d7 100644 --- a/deepinfer/structure_output/so_matcher_builder.py +++ b/deepinfer/structure_output/so_matcher_builder.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING from xgrammar import TokenizerInfo -from deepinfer.structure_output.compiler import XGrammarCompiler +from deepinfer.structure_output.compiler import SUPPORTED_SCHEMA_TYPES, XGrammarCompiler from deepinfer.structure_output.matcher import XGrammarMatcher, XGrammarMatcherFuture, validate_trigger_bound if TYPE_CHECKING: @@ -65,6 +65,8 @@ class SOMatcherBuilder: if missing: raise ValueError(f"so_args is missing required key(s) {missing}; provide schema_type and schema") schema_type = so_args["schema_type"] + if schema_type not in SUPPORTED_SCHEMA_TYPES: + raise ValueError(f"unsupported schema_type {schema_type!r}; expected one of {SUPPORTED_SCHEMA_TYPES}") schema = so_args["schema"] trigger_tokens = so_args.get("trigger_token_ids")