Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
85 changes: 85 additions & 0 deletions tests/tool_parsers/test_glm47_moe_tool_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.entrypoints.openai.responses.utils import build_response_output_items
from vllm.parser.glm47_moe import _glm47_arg_converter
from vllm.tokenizers import get_tokenizer
from vllm.tool_parsers.glm47_moe_tool_parser import Glm47MoeModelToolParser

Expand Down Expand Up @@ -266,3 +267,87 @@ def test_with_args(self, glm47_tool_parser, mock_request):
]
args = json.loads("".join(arguments))
assert args["city"] == "Beijing"

def test_stray_arg_key_tag(self, glm47_tool_parser, mock_request):
"""A duplicated `</arg_key>` in the stream must not reach the key."""
_reset(glm47_tool_parser)
chunks = [
"<tool_call>",
"get_weather\n",
"<arg_key>city</arg_key>",
"</arg_key>",
"<arg_value>",
"Beijing",
"</arg_value>",
"</tool_call>",
]
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
]
joined = "".join(arguments)
# Checked before json.loads: a partial fix truncates the stream, and
# the JSONDecodeError would hide which half regressed.
assert "</arg_key>" not in joined
assert json.loads(joined) == {"city": "Beijing"}


@pytest.mark.parametrize(
"raw_args",
[
pytest.param(
"<arg_key>path</arg_key></arg_key><arg_value>/tmp/x</arg_value>",
id="repeated-closing-tag",
),
pytest.param(
"<arg_key><arg_key>path</arg_key><arg_value>/tmp/x</arg_value>",
id="repeated-opening-tag",
),
pytest.param(
"<arg_key>path</arg_key><arg_value>/tmp/x</arg_value>",
id="well-formed",
),
],
)
def test_arg_converter_ignores_stray_arg_tags(raw_args: str):
"""Stray `arg_key` tags must not leak into the argument key."""
assert json.loads(_glm47_arg_converter(raw_args, False)) == {"path": "/tmp/x"}


@pytest.mark.parametrize(
"raw_args",
[
pytest.param(
"<arg_key>path</arg_key></arg_key><arg_value>/tmp/x",
id="repeated-closing-tag",
),
pytest.param(
"<arg_key><arg_key>path</arg_key><arg_value>/tmp/x",
id="repeated-opening-tag",
),
pytest.param(
"<arg_key>path</arg_key><arg_value>/tmp/x",
id="well-formed",
),
],
)
def test_partial_arg_converter_ignores_stray_arg_tags(raw_args: str):
"""Without `</arg_value>` the input reaches `_PARTIAL_ARG_RE`, not `_ARG_RE`."""
assert json.loads(_glm47_arg_converter(raw_args, True)) == {"path": "/tmp/x"}
9 changes: 5 additions & 4 deletions vllm/parser/glm47_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@
("<|system|>", "<|user|>", "<|assistant|>", "<|observation|>")
)

# The key group cannot cross ``</arg_key>``, so a stray tag never leaks into it.
_ARG_KEY = r"<arg_key>(?P<key>[^<]*+(?:<(?!/?arg_key>)[^<]*+)*+)(?:</arg_key>\s*)+"

_ARG_RE = re.compile(
r"<arg_key>(?P<key>.*?)</arg_key>\s*"
r"<arg_value>(?P<value>.*?)</arg_value>",
_ARG_KEY + r"<arg_value>(?P<value>.*?)</arg_value>",
re.DOTALL,
)
_PARTIAL_ARG_RE = re.compile(
r"<arg_key>(?P<key>.*?)</arg_key>\s*"
r"<arg_value>(?P<value>.*)$",
_ARG_KEY + r"<arg_value>(?P<value>.*)$",
re.DOTALL,
)

Expand Down
Loading