Repository navigation
[Bugfix][Parser] GLM-4.7: stray arg tags must not leak into tool-call argument keys - #54971
pctablet505 wants to merge 7 commits into
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
f7ac7b1 to
7fec1f3
Compare
Manny7717
left a comment
There was a problem hiding this comment.
Verified locally on head 7fec1f39 vs base 41848ca — fix is correct and regression-proven.
Bug confirmed on base: both stray-tag shapes reproduce the reported leak exactly. _ARG_KEY replaced .*? (which backtracks across its own </arg_key> delimiter to reach a following <arg_value>) with a possessive non-<-crossing key group:
<arg_key>path</arg_key></arg_key><arg_value>/tmp/x</arg_value>→ base produced keypath</arg_key>(the exact production failure in the PR body); head →{"path": "/tmp/x"}.<arg_key><arg_key>path</arg_key><arg_value>...→ base produced key<arg_key>path; head correct.
Regression-proven: all 5 new tests FAIL on base (4 param converter tests + the streaming e2e test), PASS on head. Full file: 20/20 pass on head vs 5 failed/15 passed on base (teardown RuntimeError: Cannot access accelerator device errors are the known CUDA-less noise, identical on both).
No regressions on adjacent shapes — probed base-vs-head over 22 inputs: well-formed single/multi-arg, \s* whitespace variants, keys containing < (e.g. a<b), arg_key-like markup inside values, empty keys/values, newlines, trailing partial values, junk prefix. Byte-identical output everywhere except the corrupt-key cases. One deliberate delta on the malformed two-keys-one-value input: base merged a</arg_key><arg_key>b into one corrupt key; head drops the value-less a and keeps {"b": "1"} — cleaner fail behavior, not a regression.
Non-blocking note: vllm/tool_parsers/poolside_v1_tool_parser.py:82 uses the same <arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value> shape with stdlib re — same leak exists there (findall on <arg_key>path</arg_key></arg_key><arg_value>/tmp/x</arg_value> returns [('path</arg_key>', '/tmp/x')]). If poolside models can emit stray arg tags too, the same [^<]*+(?:<(?!/?arg_key>)[^<]*+)*+ key pattern would apply. Out of scope for this PR, just flagging the sibling.
Nice work — clear root-cause writeup, the _ARG_KEY comment explains the backtracking subtlety precisely, and the streaming e2e test is exactly the right level to prove the engine path.
7fec1f3 to
ddf8c87
Compare
ddf8c87 to
eabb55f
Compare
|
Thanks for the catch, @Manny7717 — |
80be8f8 to
8f54775
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
ac193c7 to
4d6d0a0
Compare
… argument keys
`_glm47_arg_converter` matched the argument key with an unbounded `.*?`
before `</arg_key>`. Because the group is non-greedy but unbounded, the
regex engine backtracks *past* the first `</arg_key>` whenever that is
what it takes to reach the following `<arg_value>` -- so a repeated or
stray tag in the model stream is absorbed into the key rather than
ignored.
Observed in production against a GLM server run with
`--tool-call-parser glm47`: an argument object came back as
`{"path</arg_key>": "/Users/user/Desktop/project"}`. The client rejected
the call ("must have required property 'path'"), and the model then
retried with an invented path.
Match runs of non-`<` text instead, so the key group cannot cross its own
delimiter, and accept a run of closing tags so the intended key survives.
Well-formed input is unaffected. The unrolled possessive form also avoids
the backtracking a tempered dot would add on the streaming path, which
re-runs the converter over the whole accumulated buffer on every chunk:
on an unterminated 20 KB buffer it is 1251 us -> 10 us.
Covers both `_ARG_RE` and `_PARTIAL_ARG_RE`, with a streaming test that
fails if only one of the two is fixed.
Signed-off-by: pctablet505 <55033230+pctablet505@users.noreply.github.com>
d1abda4 to
b242272
Compare
…-call argument keys
PoolsideV1ToolParser.func_arg_regex captures the argument key with a plain
`.*?`:
r"<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>"
A non-greedy `.*?` only guarantees the *shortest match starting from the
left*, not that it stops at the first literal occurrence of `</arg_key>`
once the rest of the pattern is considered: if matching `\s*<arg_value>`
fails right after the first `</arg_key>`, the engine backtracks and lets
`.*?` cross that boundary to reach a later one, absorbing a stray
`<arg_key>`/`</arg_key>` tag into the key. Same defect class, same fix
shape, as the GLM-4.7 leak fixed in vllm-project#54971: bound the key to runs of
non-`<` text so the capture cannot cross its own delimiter.
r"<arg_key>([^<]*+(?:<(?!/?arg_key>)[^<]*+)*+)(?:</arg_key>\s*)+"
r"<arg_value>(.*?)</arg_value>"
Not a duplicate of vllm-project#54678, which fixes catastrophic backtracking (a timing
defect) in this file's `func_detail_regex` and adds a regex-timeout guard
around all three patterns in `extract_tool_calls`, including this one's
`.findall` call site -- but it leaves `func_arg_regex`'s pattern itself
unchanged, so the key-leak this PR fixes is still present after vllm-project#54678.
Signed-off-by: pctablet505 <55033230+pctablet505@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: pctablet505 <55033230+pctablet505@users.noreply.github.com>
Keep the one comment a reader needs; the bug narrative lives in the PR description. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: pctablet505 <55033230+pctablet505@users.noreply.github.com>
|
Brought this branch up to date with
I also trimmed the bug-narrative comments and test docstrings down to what a reader needs (the explanation stays in the description), and refreshed the related-PR list (#60842 and #58418 are new neighbours; neither changes the key pattern). @sfeng33, you last touched |
|
Synced this branch with latest |
Signed-off-by: pctablet505 <55033230+pctablet505@users.noreply.github.com>
Overview
With
--tool-call-parser glm47, a duplicated<arg_key>or</arg_key>tag in the model output ends up inside the argument name. Seen in production as{"path</arg_key>": "/Users/user/Desktop/project"}, which the client rejects withmust have required property 'path'. The key pattern is now bounded so it cannot cross anarg_keytag, in both the complete-call and the streaming regex.Sibling fix for the Poolside parser: #56093.
Claims
arg_keytags no longer reach the key, for complete calls (_ARG_RE) and streaming (_PARTIAL_ARG_RE).Validation
CPU only (macOS, Python 3.12, torch 2.14.1). Run on 2026-10-14 against
eca1f5743(mainat7115b70c0, includes #61253). The commits after it only mergemain.glm47_moe.pytaken frompytest tests/tool_parsers/test_glm47_moe_tool_parser.pymain_PARTIAL_ARG_RErevertedNew tests:
test_arg_converter_ignores_stray_arg_tagsandtest_partial_arg_converter_ignores_stray_arg_tagscover a repeated closing tag, a repeated opening tag, and a well-formed control. The first goes through_ARG_RE; the second leaves out</arg_value>, so it goes through_PARTIAL_ARG_RE.TestGlm47Streaming::test_stray_arg_key_tagruns end to end throughextract_tool_calls_streaming.The 5 failures on
mainare the four malformed converter cases plus the streaming test. The last row shows that the streaming half is tested separately.Also:
tests/reasoning/test_glm4_moe_reasoning_parser.py,tests/tool_parsers/test_glm4_moe_tool_parser.py,tests/parser/engine/test_glm47_moe.py): 46 passed.ruff checkandruff format --checkpass on the changed files.mainwith this branch on 22 inputs: whitespace variants, keys containing<, tag-like text inside values, empty keys or values, and partial values. Output was identical except for the corrupt-key cases.Local-only workaround: vLLM's autouse teardown calls
torch.accelerator.empty_host_cache(), which segfaults on CPU-only macOS torch. For these runs it was stubbed out with a local pytest plugin. Nothing in the repo was changed for this.Details
Root cause. Both regexes matched the key with
(?P<key>.*?)</arg_key>\s*<arg_value>. The lazy.*?still runs past the first</arg_key>when the next text is not<arg_value>, so a duplicated closing tag gets absorbed into the key. With a duplicated opening tag, the match simply starts at the first<arg_key>.main<arg_key>path</arg_key></arg_key><arg_value>/tmp/x</arg_value>{"path</arg_key>": "/tmp/x"}{"path": "/tmp/x"}<arg_key><arg_key>path</arg_key><arg_value>/tmp/x</arg_value>{"<arg_key>path": "/tmp/x"}{"path": "/tmp/x"}Fix. Both regexes now share one key pattern:
<, but not anarg_keytag, so it stops at the first</arg_key>. Then(?:</arg_key>\s*)+consumes any duplicated closing tags.<arg_key>a</arg_key><arg_key>b</arg_key><arg_value>1</arg_value>) used to produce a single corrupt key,a</arg_key><arg_key>b. It now produces{"b": "1"}.Overlapping PRs.
_glm47_parse_args, which still uses_ARG_RE/_PARTIAL_ARG_RE. Its author also tested it with these patterns applied.AI assistance: used for this change. I reviewed every changed line and ran the tests above.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.