[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:
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user