[Bug][Structured Outputs] Fix bug that leads to unconstrained generations with structural tags (#42452)

Signed-off-by: rishitdholakia13 <rishit+github@cohere.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
rishitdholakia13
2026-05-20 07:08:58 -07:00
committed by GitHub
co-authored by Cursor
parent df84fb07a6
commit ded871201a
2 changed files with 42 additions and 2 deletions
@@ -10,6 +10,7 @@ import pytest
from vllm.config import ModelConfig, SchedulerConfig, VllmConfig
from vllm.v1.request import Request
from vllm.v1.structured_output import StructuredOutputManager
from vllm.v1.structured_output.backend_types import StructuredOutputOptions
class MockReasoner:
@@ -214,6 +215,32 @@ class TestReasoningStructuredOutput:
)
assert result is False
def test_should_advance_reasoning_just_ended_with_spec_decode_structural_tag(
self,
manager_with_reasoner,
mock_request_with_structured_output,
):
"""When reasoning ends this step, advance immediately for structural
tags with speculative decoding."""
structured_req = mock_request_with_structured_output.structured_output_request
structured_req.reasoning_ended = False
structured_req.structured_output_key = (
StructuredOutputOptions.STRUCTURAL_TAG,
"{}",
)
reasoner = MockReasoner(tokenizer=Mock())
reasoner.is_reasoning_end_streaming.return_value = True
structured_req.reasoner = reasoner
manager_with_reasoner.vllm_config.speculative_config = Mock()
result = manager_with_reasoner.should_advance(
mock_request_with_structured_output
)
assert structured_req.reasoning_ended is True
assert result is True
def test_should_advance_reasoning_already_ended(
self,
manager_with_reasoner,
+15 -2
View File
@@ -15,6 +15,7 @@ from vllm.v1.structured_output.backend_guidance import GuidanceBackend
from vllm.v1.structured_output.backend_types import (
StructuredOutputBackend,
StructuredOutputGrammar,
StructuredOutputOptions,
)
from vllm.v1.structured_output.backend_xgrammar import XgrammarBackend
@@ -350,10 +351,22 @@ class StructuredOutputManager:
if reasoner.is_reasoning_end_streaming(
all_token_ids, itertools.islice(all_token_ids, start, None)
):
# Reasoning just ended, so we shouldn't advance til
# next pass
structured_req.reasoning_ended = True
# Reasoning just ended this step. Defer FSM advance until the next
# pass (see reasoning_ended check above) for JSON/regex/choice/grammar:
# advancing on the closing boundary token can accept tokens that still
# belong to the reasoning stream. Structural tags are the only safe
# same-step exception: they model phased output (e.g. thinking tag ->
# answer tag), and speculative decoding must run grammar.validate_tokens
# on draft tokens produced immediately after that transition.
if (
self.vllm_config.speculative_config is not None
and structured_req.structured_output_key[0]
== StructuredOutputOptions.STRUCTURAL_TAG
):
return True
return False
def clear_backend(self) -> None: