[Bugfix][Frontend] Force reasoning mode for GLM-5.3 chat templates in the GLM MoE parser (#56994)

Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
This commit is contained in:
Shijin Zhang
2026-09-29 21:26:34 +08:00
committed by GitHub
co-authored by Chauncey
parent dfc8e0f3e2
commit d95d1dcfb9
3 changed files with 111 additions and 1 deletions
@@ -1445,7 +1445,7 @@ class TestDetectMergeInlineSystem:
AnthropicServingMessages._should_merge_inline_system(online_renderer)
is expected
)
assert mock_logger.warning.called is expected
assert mock_logger.warning_once.called is expected
# ======================================================================
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import copy
import pytest
from transformers import AutoTokenizer
@@ -227,3 +229,81 @@ def test_is_reasoning_end_full_prompt(
token_ids = glm45_tokenizer.convert_tokens_to_ids(tokens)
check_is_reasoning_end = parser.is_reasoning_end(token_ids)
assert check_is_reasoning_end == is_reasoning_end
GLM53_TEMPLATE = (
"[gMASK]<sop>\n"
"{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is"
" defined and reasoning_effort in ['low', 'high'] else 'max' -%}\n"
"<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}\n"
"{% for tc in m.tool_calls %}\n"
"{{- '<tool_call>' + tc.name -}}\n"
"{% set _args = tc.arguments %}"
"{% for k, v in _args.items() %}"
"<arg_key>{{ k }}</arg_key><arg_value>{{ v }}</arg_value>"
"{% endfor %}</tool_call>\n"
"{% endfor %}\n"
"<|assistant|>{{- '<think>' -}}"
)
GLM53_LEAK = {
"output": "Simple question.</think>2 + 2 = **4**",
"reasoning": "Simple question.",
"content": "2 + 2 = **4**",
"is_reasoning_end": True,
}
@pytest.fixture()
def glm53_style_tokenizer(glm45_tokenizer):
tokenizer = copy.copy(glm45_tokenizer)
tokenizer.chat_template = GLM53_TEMPLATE
return tokenizer
@pytest.mark.parametrize(
"disable_kwargs", [{"enable_thinking": False}, {"thinking": False}]
)
def test_glm53_template_forces_reasoning(disable_kwargs: dict, glm53_style_tokenizer):
parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
parser = parser_cls(glm53_style_tokenizer, chat_template_kwargs=disable_kwargs)
assert parser._parser_engine.thinking_enabled
output = glm53_style_tokenizer.tokenize(GLM53_LEAK["output"])
output_tokens: list[str] = [
glm53_style_tokenizer.convert_tokens_to_string([token]) for token in output
]
reasoning, content = run_reasoning_extraction(parser, output_tokens)
assert reasoning == GLM53_LEAK["reasoning"]
assert content == GLM53_LEAK["content"]
output_ids = glm53_style_tokenizer.convert_tokens_to_ids(output)
assert parser.is_reasoning_end(output_ids) == GLM53_LEAK["is_reasoning_end"]
def test_glm53_template_forces_reasoning_streaming(glm53_style_tokenizer):
parser = ReasoningParserManager.get_reasoning_parser(parser_name)(
glm53_style_tokenizer, chat_template_kwargs={"enable_thinking": False}
)
output = glm53_style_tokenizer.tokenize(GLM53_LEAK["output"])
output_tokens: list[str] = [
glm53_style_tokenizer.convert_tokens_to_string([token]) for token in output
]
reasoning, content = run_reasoning_extraction(parser, output_tokens, streaming=True)
assert reasoning == GLM53_LEAK["reasoning"]
assert content == GLM53_LEAK["content"]
def test_glm47_template_honors_thinking_disable(glm45_tokenizer):
parser = ReasoningParserManager.get_reasoning_parser(parser_name)(
glm45_tokenizer, chat_template_kwargs={"enable_thinking": False}
)
assert not parser._parser_engine.thinking_enabled
output = glm45_tokenizer.tokenize(GLM53_LEAK["output"])
output_tokens: list[str] = [
glm45_tokenizer.convert_tokens_to_string([token]) for token in output
]
reasoning, content = run_reasoning_extraction(parser, output_tokens)
assert reasoning is None
assert content == GLM53_LEAK["output"]
+30
View File
@@ -20,6 +20,7 @@ import regex as re
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.logger import init_logger
from vllm.parser.engine.events import EventType
from vllm.parser.engine.parser_engine import ParserEngine
from vllm.parser.engine.parser_engine_config import (
@@ -28,6 +29,8 @@ from vllm.parser.engine.parser_engine_config import (
Transition,
)
logger = init_logger(__name__)
if TYPE_CHECKING:
from vllm.tokenizers import TokenizerLike
from vllm.tool_parsers.abstract_tool_parser import Tool
@@ -77,6 +80,23 @@ def _glm47_arg_converter(raw_args: str, partial: bool) -> str:
return json.dumps(params, ensure_ascii=False)
def _glm53_always_thinks(tokenizer: TokenizerLike) -> bool:
# GLM-5.3 keeps the GLM-4.x prompt and tool-call format but replaces the
# enable_thinking toggle with an always-on "Reasoning Effort:" header, so
# its generation prompt always opens <think> and the model output carries
# no opening tag.
template = getattr(tokenizer, "chat_template", None)
return (
isinstance(template, str)
and "[gMASK]<sop>" in template
and "Reasoning Effort:" in template
and "enable_thinking" not in template
and "<tool_call>" in template
and "<arg_key>" in template
and "<arg_value>" in template
)
@functools.cache
def glm47_moe_config(thinking: bool = True) -> ParserEngineConfig:
arg_tag_transitions = {
@@ -192,6 +212,16 @@ class Glm47MoeParser(ParserEngine):
chat_kwargs = kwargs.get("chat_template_kwargs", {}) or {}
thinking = chat_kwargs.get("thinking", None)
enable_thinking = chat_kwargs.get("enable_thinking", None)
if (thinking is False or enable_thinking is False) and _glm53_always_thinks(
tokenizer
):
logger.warning_once(
"Ignoring enable_thinking/thinking: the GLM-5.3 chat template "
"has no thinking switch, so reasoning is always on and "
"disabling extraction would leak it into the content."
)
thinking = None
enable_thinking = None
self.thinking_enabled = (
True
if thinking is None and enable_thinking is None