From 42e1589db4a5760ab9257ac5a5c8162325788d0d Mon Sep 17 00:00:00 2001 From: Mikhail Kostryukov Date: Tue, 21 Jul 2026 00:51:18 +0300 Subject: [PATCH] [Bugfix] glm47 parser: tolerate missing opening tag Under concurrent load, GLM-4.7-family models occasionally skip the opening tag when emitting tool calls, producing keyvalue with the value fully intact. The argument regexes required the opening tag, so such calls were silently parsed as empty arguments ({}), failing downstream tool validation. Make the opening tag optional in both the full and partial argument regexes; the closing still delimits the value. Streaming is unaffected for well-formed output: the incremental lexer buffers ambiguous tag prefixes, so a partially streamed tag never leaks into the value. Signed-off-by: Mikhail Kostryukov --- .../test_glm47_moe_tool_parser.py | 63 +++++++++++++++++++ vllm/parser/glm47_moe.py | 8 ++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/tests/tool_parsers/test_glm47_moe_tool_parser.py b/tests/tool_parsers/test_glm47_moe_tool_parser.py index dc4b726a5383..d2aa072b8111 100644 --- a/tests/tool_parsers/test_glm47_moe_tool_parser.py +++ b/tests/tool_parsers/test_glm47_moe_tool_parser.py @@ -163,6 +163,34 @@ def test_whitespace_preserved_in_arg_values(self, glm47_tool_parser, mock_reques assert r.tools_called assert json.loads(r.tool_calls[0].function.arguments) == {"city": " Beijing "} + def test_missing_arg_value_open_tag(self, glm47_tool_parser, mock_request): + # Under load GLM-4.7-family models occasionally skip the opening + # tag while still emitting the value and closing tag. + out = ( + "get_weathercity" + "Beijing" + ) + r = glm47_tool_parser.extract_tool_calls(out, request=mock_request) + assert r.tools_called + assert r.tool_calls[0].function.name == "get_weather" + assert json.loads(r.tool_calls[0].function.arguments) == {"city": "Beijing"} + + def test_missing_arg_value_open_tag_multiple_args( + self, glm47_tool_parser, mock_request + ): + out = ( + "get_weather" + "cityBeijing" + "date2026-07-21" + "" + ) + r = glm47_tool_parser.extract_tool_calls(out, request=mock_request) + assert r.tools_called + assert json.loads(r.tool_calls[0].function.arguments) == { + "city": "Beijing", + "date": "2026-07-21", + } + def test_content_before(self, glm47_tool_parser, mock_request): out = "Checking.get_current_date" r = glm47_tool_parser.extract_tool_calls(out, request=mock_request) @@ -266,3 +294,38 @@ def test_with_args(self, glm47_tool_parser, mock_request): ] args = json.loads("".join(arguments)) assert args["city"] == "Beijing" + + def test_missing_arg_value_open_tag(self, glm47_tool_parser, mock_request): + _reset(glm47_tool_parser) + chunks = [ + "", + "get_weather\n", + "city", + "Bei", + "jing", + "", + "", + ] + current_text = "" + deltas = [] + for chunk in chunks: + current_text += chunk + delta = glm47_tool_parser.extract_tool_calls_streaming( + previous_text="", + current_text=current_text, + delta_text=chunk, + previous_token_ids=[], + current_token_ids=[], + delta_token_ids=[], + request=mock_request, + ) + if delta: + deltas.append(delta) + arguments = [ + tool_call.function.arguments + for delta in deltas + for tool_call in (delta.tool_calls or []) + if tool_call.function and tool_call.function.arguments + ] + args = json.loads("".join(arguments)) + assert args["city"] == "Beijing" diff --git a/vllm/parser/glm47_moe.py b/vllm/parser/glm47_moe.py index 00e517f2ea68..1afabbfef312 100644 --- a/vllm/parser/glm47_moe.py +++ b/vllm/parser/glm47_moe.py @@ -48,14 +48,18 @@ ("<|system|>", "<|user|>", "<|assistant|>", "<|observation|>") ) +# The opening tag is optional: under load GLM-4.7-family models +# occasionally skip it, emitting +# ``keyvalue`` with the value fully intact. +# Requiring the tag would silently drop such arguments (yielding ``{}``). _ARG_RE = re.compile( r"(?P.*?)\s*" - r"(?P.*?)", + r"(?:)?(?P.*?)", re.DOTALL, ) _PARTIAL_ARG_RE = re.compile( r"(?P.*?)\s*" - r"(?P.*)$", + r"(?:)?(?P.*)$", re.DOTALL, )