Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion vllm/parser/abstract_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,22 @@ def adjust_request(
request = self._tool_parser.adjust_request(request)
return request

def _reasoning_enabled(self) -> bool:
"""Whether the model will emit a reasoning prefix before its answer
/ tool call for this request.

The parser (and thus its reasoning parser) is constructed per request
with that request's ``chat_template_kwargs``, so the reasoning parser
already reflects whether thinking is enabled. Defaults to ``True`` —
xgrammar's own default — when the reasoning parser cannot report it,
so the structural-tag grammar models the reasoning phase the engine's
structured-output FSM expects.
"""
reasoner = self._reasoning_parser
if reasoner is None:
return False
return bool(getattr(reasoner, "thinking_enabled", True))

def _apply_structural_tag(
self, request: ChatCompletionRequest | ResponsesRequest
) -> ChatCompletionRequest | ResponsesRequest:
Expand All @@ -484,9 +500,18 @@ def _apply_structural_tag(
if not need_tool_calling:
return request

reasoning_enabled = self._reasoning_enabled()

# When the model emits a reasoning prefix (e.g. ``...</think>``) before
# its tool call, the structural-tag grammar MUST include the reasoning
# section. Otherwise the grammar only models the tool-call format and
# the structured-output FSM rejects the reasoning tokens at the
# reasoning->tool boundary (HTTP 500 non-streaming / mid-stream error
# streaming). The engine's structural-tag same-step advance also assumes
# the tag models this phased (reasoning -> tool) output.
structure_tag = self._tool_parser.get_structural_tag(
request,
reasoning=False,
reasoning=reasoning_enabled,
)
if structure_tag is None:
return request
Expand All @@ -499,6 +524,23 @@ def _apply_structural_tag(
request.text = None
else:
request.response_format = None

# The structural tag now models the reasoning phase itself (an
# unconstrained prefix terminated by the reasoning-end marker), so the
# structured-output engine must enforce the grammar from the FIRST
# token rather than deferring past a reasoning section it would no
# longer detect separately. Without this, tokens sampled immediately
# after the reasoning-end marker escape the grammar bitmask, so a
# forced/required tool suffix can be violated (e.g. the model samples
# prose instead of the tool call); the FSM advance then rejects those
# tokens and the request fails with an internal error. This reuses the
# same signal the Mistral grammar path uses to mark "the grammar
# handles reasoning" (-> reasoning_ended=True at generation start). The
# reasoning prefix is unconstrained, so thinking is not restricted; only
# the post-reasoning tool call is enforced. PrivateAttr exists only on
# ChatCompletionRequest, hence the guard.
if reasoning_enabled and hasattr(request, "_grammar_from_tool_parser"):
request._grammar_from_tool_parser = True
Comment on lines 499 to +526

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clean up the comments in this PR? They are quite specific and sloppy

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed in 80e6fe4. Kept the non-obvious rationale (per-request parser construction, the PrivateAttr guard, why the grammar is enforced from the first token) and dropped the restatement and symptom narration. Logic unchanged.

return request

def extract_reasoning_streaming(
Expand Down
12 changes: 12 additions & 0 deletions vllm/parser/engine/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ def adjust_request(
def has_engine_confirmed_reasoning_end(self) -> bool:
return self._parser_engine.reasoning_ended

@property
def thinking_enabled(self) -> bool:
"""Whether reasoning/thinking output is enabled for this request.

Mirrors the wrapped parser engine, which derives this from the
request's ``chat_template_kwargs`` (e.g. ``enable_thinking``). Used by
structural-tag construction to decide whether the grammar must model a
reasoning prefix. Defaults to ``True`` when the engine does not expose
it.
"""
return bool(getattr(self._parser_engine, "thinking_enabled", True))

def finish_streaming(self) -> DeltaMessage | None:
return self._parser_engine.finish_streaming()

Expand Down
Loading