diff --git a/vllm/parser/abstract_parser.py b/vllm/parser/abstract_parser.py index 915d401f7bdd..7831040189bf 100644 --- a/vllm/parser/abstract_parser.py +++ b/vllm/parser/abstract_parser.py @@ -463,6 +463,18 @@ def adjust_request( request = self._tool_parser.adjust_request(request) return request + def _reasoning_enabled(self) -> bool: + """Whether thinking is enabled for this request. + + The reasoning parser is built per request from its + ``chat_template_kwargs`` and already reflects the thinking state. + Defaults to ``True`` to match xgrammar's default. + """ + 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: @@ -484,9 +496,14 @@ def _apply_structural_tag( if not need_tool_calling: return request + reasoning_enabled = self._reasoning_enabled() + + # The grammar must include the reasoning section when the model thinks, + # else the FSM rejects the reasoning tokens at the reasoning-to-tool + # boundary. structure_tag = self._tool_parser.get_structural_tag( request, - reasoning=False, + reasoning=reasoning_enabled, ) if structure_tag is None: return request @@ -499,6 +516,14 @@ def _apply_structural_tag( request.text = None else: request.response_format = None + + # The tag models the reasoning prefix, so enforce the grammar from the + # first token rather than deferring past a reasoning section. Otherwise + # the tokens sampled right after the reasoning-end marker escape the + # bitmask and a forced tool call can be violated. The attr is a + # ChatCompletionRequest PrivateAttr, hence the guard. + if reasoning_enabled and hasattr(request, "_grammar_from_tool_parser"): + request._grammar_from_tool_parser = True return request def extract_reasoning_streaming( diff --git a/vllm/parser/engine/adapters.py b/vllm/parser/engine/adapters.py index 2482dad437b2..9efd843ff223 100644 --- a/vllm/parser/engine/adapters.py +++ b/vllm/parser/engine/adapters.py @@ -110,6 +110,16 @@ def adjust_request( def has_engine_confirmed_reasoning_end(self) -> bool: return self._parser_engine.reasoning_ended + @property + def thinking_enabled(self) -> bool: + """Whether thinking is enabled for this request. + + Delegates to the wrapped parser engine, which derives it from the + request's ``chat_template_kwargs``. Defaults to ``True`` to match + xgrammar's default. + """ + return bool(getattr(self._parser_engine, "thinking_enabled", True)) + def finish_streaming(self) -> DeltaMessage | None: return self._parser_engine.finish_streaming()