[Bugfix][Structured Output] Avoid spurious FSM errors after speculative reasoning end (#53046)

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>

Chauncey committed Aug 21, 2026 at 10:01 UTC c6e19b3be24338759a443e03c8325d76da9ee202
2 files changed +8 -2
tests/v1/spec_decode/test_mtp_structured_output.py
+2 -1
@@ -189,7 +189,7 @@ def test_bitmask_constrained_when_reasoning_ends_midwindow(backend):
189
190
191 @pytest.mark.parametrize("backend", ["xgrammar", "guidance"])
192 -def test_bitmask_post_reasoning_end_drafts_skip_grammar_advance(backend):
192 +def test_bitmask_post_reasoning_end_drafts_skip_grammar_advance(backend, caplog):
193 """Post-marker drafts predate the bitmask and may be grammar-invalid;
194 grammar_bitmask must skip the grammar advance instead of asserting.
195 """
@@ -235,6 +235,7 @@ def test_bitmask_post_reasoning_end_drafts_skip_grammar_advance(backend):
235 assert not (bitmask[2] == -1).all()
236 # Grammar must not have advanced through the unvalidated draft.
237 assert not grammar.is_terminated()
238 + assert "Failed to advance FSM" not in caplog.text
239
240
241 @pytest.mark.parametrize("backend", ["xgrammar", "guidance"])
vllm/v1/structured_output/__init__.py
+6 -1
@@ -326,7 +326,12 @@ class StructuredOutputManager:
326 advance_grammar = False
327 post_reasoning_end_in_window = True
328 if advance_grammar and not grammar.is_terminated():
329 - accepted = grammar.accept_tokens(req_id, [token])
329 + if post_reasoning_end_in_window:
330 + accepted = bool(grammar.validate_tokens([token]))
331 + if accepted:
332 + accepted = grammar.accept_tokens(req_id, [token])
333 + else:
334 + accepted = grammar.accept_tokens(req_id, [token])
335 if accepted:
336 state_advancements += 1
337 elif not post_reasoning_end_in_window: