From b6badee0fdd429e06d84697064aa82e87331a1cf Mon Sep 17 00:00:00 2001 From: dhanavanthesh Date: Tue, 14 Jul 2026 18:26:50 +0530 Subject: [PATCH 1/2] Fix the JsonSchema whitespace_pattern being ignored by the backend The whitespace_pattern set on a JsonSchema output type was not passed to the backend during generation, so it had no effect on the default outlines_core backend even though to_regex already honors it. Thread the whitespace_pattern through the backend interface and forward it to build_regex_from_schema. The llguidance and xgrammar backends raise NotImplementedError when a pattern is set, since they compile the schema with their own grammar engines and cannot apply it. --- docs/features/core/output_types.md | 2 +- src/outlines/backends/__init__.py | 8 ++++- src/outlines/backends/base.py | 5 ++- src/outlines/backends/llguidance.py | 10 +++++- src/outlines/backends/outlines_core.py | 9 ++++-- src/outlines/backends/xgrammar.py | 10 +++++- src/outlines/generator.py | 1 + tests/backends/test_outlines_core.py | 42 ++++++++++++++++++++++++++ 8 files changed, 80 insertions(+), 7 deletions(-) diff --git a/docs/features/core/output_types.md b/docs/features/core/output_types.md index dc3b046671..5b8f1d24cf 100644 --- a/docs/features/core/output_types.md +++ b/docs/features/core/output_types.md @@ -178,7 +178,7 @@ output_type = JsonSchema(schema_dict) ``` `JsonSchema` accepts two optional parameters: -- `whitespace_pattern` (defaults to `None`): specifies the pattern to use for JSON syntactic whitespace. If none is provided, the default permissive JSON whitespace rules are used. +- `whitespace_pattern` (defaults to `None`): specifies the pattern to use for JSON syntactic whitespace. If none is provided, the default permissive JSON whitespace rules are used. It is applied by the `outlines_core` backend; the `llguidance` and `xgrammar` backends do not support it and raise an error if one is set. - `ensure_ascii` (defaults to `True`): defines the value to use for the argument `ensure_ascii` of the `json.dumps` method. If false, non-ASCII characters will be turned into unicodes. ### Regex Patterns diff --git a/src/outlines/backends/__init__.py b/src/outlines/backends/__init__.py index a7911fc291..bba540ba40 100644 --- a/src/outlines/backends/__init__.py +++ b/src/outlines/backends/__init__.py @@ -59,6 +59,7 @@ def get_json_schema_logits_processor( backend_name: str | None, model: SteerableModel, json_schema: str, + whitespace_pattern: str | None = None, ) -> LogitsProcessorType: """Create a logits processor from a JSON schema. @@ -70,6 +71,9 @@ def get_json_schema_logits_processor( The Outlines model of the user. json_schema: str The JSON schema to create a logits processor from. + whitespace_pattern: str | None + The pattern to use to control the whitespace allowed between JSON + tokens. `None` uses the backend default. Returns ------- @@ -81,7 +85,9 @@ def get_json_schema_logits_processor( backend_name or JSON_SCHEMA_DEFAULT_BACKEND, model, ) - return backend.get_json_schema_logits_processor(json_schema) + return backend.get_json_schema_logits_processor( + json_schema, whitespace_pattern + ) def get_regex_logits_processor( diff --git a/src/outlines/backends/base.py b/src/outlines/backends/base.py index 1de7f21f01..5737782408 100644 --- a/src/outlines/backends/base.py +++ b/src/outlines/backends/base.py @@ -17,7 +17,7 @@ class BaseBackend(ABC): @abstractmethod def get_json_schema_logits_processor( - self, json_schema: str + self, json_schema: str, whitespace_pattern: str | None = None ) -> LogitsProcessorType: """Create a logits processor from a JSON schema. @@ -25,6 +25,9 @@ def get_json_schema_logits_processor( ---------- json_schema: str The JSON schema to create a logits processor from. + whitespace_pattern: str | None + The pattern to use to control the whitespace allowed between JSON + tokens. `None` uses the backend default. Returns ------- diff --git a/src/outlines/backends/llguidance.py b/src/outlines/backends/llguidance.py index 5b6d168b1e..6e943bdd0d 100644 --- a/src/outlines/backends/llguidance.py +++ b/src/outlines/backends/llguidance.py @@ -235,7 +235,7 @@ def _create_llg_tokenizer(self, model: SteerableModel) -> "LLGTokenizer": ) def get_json_schema_logits_processor( - self, json_schema: str + self, json_schema: str, whitespace_pattern: str | None = None ) -> LLGuidanceLogitsProcessor: """Create a logits processor from a JSON schema. @@ -243,6 +243,8 @@ def get_json_schema_logits_processor( ---------- json_schema: str The JSON schema to create a logits processor from. + whitespace_pattern: str | None + Not supported by the llguidance backend; must be `None`. Returns ------- @@ -250,6 +252,12 @@ def get_json_schema_logits_processor( The logits processor to use to constrain the generation. """ + if whitespace_pattern is not None: + raise NotImplementedError( + "The llguidance backend does not support the " + "`whitespace_pattern` argument. Use the `outlines_core` " + "backend to control JSON whitespace." + ) grammar_spec = self.llg.grammar_from("json_schema", json_schema) return LLGuidanceLogitsProcessor( grammar_spec, self.llg_tokenizer, self.tensor_library_name diff --git a/src/outlines/backends/outlines_core.py b/src/outlines/backends/outlines_core.py index 2e4392ee65..7f2747cdc9 100644 --- a/src/outlines/backends/outlines_core.py +++ b/src/outlines/backends/outlines_core.py @@ -211,13 +211,18 @@ def __init__(self, model: SteerableModel): ) self.tensor_library_name = model.tensor_library_name - def get_json_schema_logits_processor(self, json_schema: str): + def get_json_schema_logits_processor( + self, json_schema: str, whitespace_pattern: str | None = None + ): """Create a logits processor from a JSON schema. Parameters ---------- json_schema: str The JSON schema to create a logits processor from. + whitespace_pattern: str | None + The pattern to use to control the whitespace allowed between JSON + tokens. `None` uses the outlines_core default. Returns ------- @@ -225,7 +230,7 @@ def get_json_schema_logits_processor(self, json_schema: str): The logits processor to use to constrain the generation. """ - regex = build_regex_from_schema(json_schema) + regex = build_regex_from_schema(json_schema, whitespace_pattern) return self.get_regex_logits_processor(regex) def get_regex_logits_processor(self, regex: str): diff --git a/src/outlines/backends/xgrammar.py b/src/outlines/backends/xgrammar.py index 4b6a59742e..bb34c7aba6 100644 --- a/src/outlines/backends/xgrammar.py +++ b/src/outlines/backends/xgrammar.py @@ -141,7 +141,7 @@ def __init__(self, model: SteerableModel): self.tensor_library_name = model.tensor_library_name def get_json_schema_logits_processor( - self, json_schema: str + self, json_schema: str, whitespace_pattern: str | None = None ) -> XGrammarLogitsProcessor: """Create a logits processor from a JSON schema. @@ -149,6 +149,8 @@ def get_json_schema_logits_processor( ---------- json_schema: str The JSON schema to create a logits processor from. + whitespace_pattern: str | None + Not supported by the xgrammar backend; must be `None`. Returns ------- @@ -156,6 +158,12 @@ def get_json_schema_logits_processor( The logits processor to use to constrain the generation. """ + if whitespace_pattern is not None: + raise NotImplementedError( + "The xgrammar backend does not support the " + "`whitespace_pattern` argument. Use the `outlines_core` " + "backend to control JSON whitespace." + ) compiled_grammar = self.grammar_compiler.compile_json_schema( json_schema ) diff --git a/src/outlines/generator.py b/src/outlines/generator.py index f2e669d8fc..2d6d985bdd 100644 --- a/src/outlines/generator.py +++ b/src/outlines/generator.py @@ -247,6 +247,7 @@ def __init__( backend_name, model, term.schema, + term.whitespace_pattern, ) else: regex_string = to_regex(term) diff --git a/tests/backends/test_outlines_core.py b/tests/backends/test_outlines_core.py index df4f71bf87..4d76addaa6 100644 --- a/tests/backends/test_outlines_core.py +++ b/tests/backends/test_outlines_core.py @@ -279,3 +279,45 @@ def mock_vocabulary(eos_id, fmt_vocab): assert captured["hi"] == [3] assert len(captured) == 2 assert sum(len(ids) for ids in captured.values()) == 3 + + +def test_json_schema_logits_processor_applies_whitespace_pattern(): + """A user-provided whitespace_pattern is forwarded to the regex builder. + + Regression: the pattern was dropped before reaching the backend, so setting + it had no effect on the whitespace of the generated JSON. + """ + backend = object.__new__(OutlinesCoreBackend) + schema = '{"type": "object", "properties": {"a": {"type": "integer"}}}' + + with ( + patch( + "outlines.backends.outlines_core.build_regex_from_schema", + return_value=r"\{\}", + ) as build_regex, + patch.object( + backend, "get_regex_logits_processor", return_value="processor" + ), + ): + backend.get_json_schema_logits_processor(schema, whitespace_pattern="") + + build_regex.assert_called_once_with(schema, "") + + +def test_json_schema_logits_processor_defaults_whitespace_pattern_to_none(): + """When no whitespace_pattern is given, None is forwarded to the builder.""" + backend = object.__new__(OutlinesCoreBackend) + schema = '{"type": "object"}' + + with ( + patch( + "outlines.backends.outlines_core.build_regex_from_schema", + return_value=r"\{\}", + ) as build_regex, + patch.object( + backend, "get_regex_logits_processor", return_value="processor" + ), + ): + backend.get_json_schema_logits_processor(schema) + + build_regex.assert_called_once_with(schema, None) From a5ff4673592cdfa3d5f27d1d47d97eab3b1bfb0a Mon Sep 17 00:00:00 2001 From: dhanavanthesh Date: Fri, 17 Jul 2026 11:15:46 +0530 Subject: [PATCH 2/2] Test the whitespace_pattern error on the llguidance and xgrammar backends The backends raise NotImplementedError when whitespace_pattern is set, but only the outlines_core backend had a test. Add a regression test for each so the unsupported whitespace path is exercised. --- tests/backends/test_llguidance.py | 8 ++++++++ tests/backends/test_xgrammar.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/tests/backends/test_llguidance.py b/tests/backends/test_llguidance.py index 465f548719..259b942f3d 100644 --- a/tests/backends/test_llguidance.py +++ b/tests/backends/test_llguidance.py @@ -201,3 +201,11 @@ def test_llguidance_backend(model, tensor_library_name, json_schema, regex, cfg_ else: response = generator("Create a character", max_tokens=20) assert response[0] == "{" + + +def test_json_schema_logits_processor_rejects_whitespace_pattern(): + """The llguidance backend does not support whitespace_pattern and raises.""" + backend = object.__new__(LLGuidanceBackend) + schema = '{"type": "object"}' + with pytest.raises(NotImplementedError, match="whitespace_pattern"): + backend.get_json_schema_logits_processor(schema, whitespace_pattern=" ") diff --git a/tests/backends/test_xgrammar.py b/tests/backends/test_xgrammar.py index 855213990e..7e18fe4c48 100644 --- a/tests/backends/test_xgrammar.py +++ b/tests/backends/test_xgrammar.py @@ -166,3 +166,11 @@ def test_xgrammar_backend_invalid_model(): match="The xgrammar backend only supports Transformers and MLXLM models", ): XGrammarBackend(model_llamacpp()) + + +def test_json_schema_logits_processor_rejects_whitespace_pattern(): + """The xgrammar backend does not support whitespace_pattern and raises.""" + backend = object.__new__(XGrammarBackend) + schema = '{"type": "object"}' + with pytest.raises(NotImplementedError, match="whitespace_pattern"): + backend.get_json_schema_logits_processor(schema, whitespace_pattern=" ")