Repository navigation
[Frontend] Constrain non-strict GLM-4.7 tool calls with a shallow structural tag - #56403
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. 🚀 |
f470348 to
ec6602a
Compare
There was a problem hiding this comment.
One high level suggestion before getting into the details:
we can extend the function and register in vllm/tool_parsers/structural_tag_registry.py, like k3 and hy4(it's using the same XML pattern as GLM), without manually adding functions and detailed rules in vllm/tool_parsers/glm47_moe_tool_parser.py
and following the registry way, the code diff can be smaller and avoid many potential bugs along the way
Also, IMO it would be much easier to maintain
above all this, we need to update the PR description and related unit tests as well
if any performance stats can be shown in PR description would be great
PTAL @Dovis01 thanks~
Register a glm_4_7 builder in the structural tag registry (schema-exact mode delegates to the xgrammar builtin) and attach a non-strict structural tag from the glm47 parser when no tool is strict and strict tool calling is disabled. This keeps auto/required/named tool choices emitting well-formed GLM XML through the existing structural-tag plumbing. Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
ec6602a to
7f0d801
Compare
PLS check again. |
|
taking a look, add some related tests and update PR description please @Dovis01 |
Cover the glm_4_7 registry builder (non-strict grammar accepts omitted/ repeated/reordered args with typed and enum values, auto/required/forced choice shapes, reasoning gate, xgrammar-builtin delegation and protocol rebuild) and the Glm47MoeModelToolParser.adjust_request non-strict attach path with all of its fallback guards. Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Separate the non-strict builder from the strict glm_4_7 key so the schema-exact path keeps dispatching to the xgrammar builtin untouched, dropping the protocol-reconstruction delegation. Update the registry tests accordingly. Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Done |
Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Hi, @sfeng33 Thanks for the clarification. I agree that #47512 addresses the actual correctness bug in the generic JSON fallback path, so I’ll rebase and remove that part from the scope of this PR. The remaining intent here is only the non-strict fallback behavior we are syncing from the official internal implementation to both SGLang and vLLM. SGLang has already landed the equivalent behavior in sgl-project/sglang#38890. This fallback is intentionally a bit stronger than xgrammar’s current glm_4_7 strict:false envelope-only behavior: it keeps lightweight/shallow constraints such as known argument names, basic types, and enums, while still relaxing required/nested/full-schema validation. So I think the main question is whether upstream vLLM wants to preserve this shared fallback semantics, or prefer the simpler envelope-only behavior from xgrammar. I’m happy to simplify the implementation accordingly after rebasing on the latest main. |
|
Thanks @Dovis01. Keeping But in terms of how it gets attached: the Suggested change:
|
Appreciate! @sfeng33 Updated accordingly. Could you pls check again? Thx! |
3849804 to
4b3ab87
Compare
…l-tag dispatch Replaces the parser-level adjust_request override with a base strict-level framework: parsers can set default_tool_strict_level (GLM defaults to FUNCTION so auto tool calls are constrained unless the operator sets an explicit --tool-strict-level), and the glm_4_7 registry builder dispatches per tool after resolve_tool_strictness — strict tools keep the xgrammar builtin's schema-pinned content byte-identically, while non-strict tools get the shallow tag, which pins argument keys and basic value types and excludes structural markers from values. VLLM_ENFORCE_STRICT_TOOL_CALLING=0 still disables structural tags entirely. Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
4b3ab87 to
4952702
Compare
|
Thanks @Dovis01, the wiring now matches the flags and the per-tool dispatch looks good. One regression to fix before merge, plus a few small items. Blocking: tools without a plain top-level
Since GLM now defaults to Fix: in |
…hemas The shallow tag now applies only when the tool schema is a plain object with declared properties and without additionalProperties: true; other shapes (root $ref or compositors, patternProperties, free-form objects) use the builtin's non-strict envelope content so arguments are never dropped. Adds a parametrized test covering the reported schema shapes. Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Thanks for pointing this out. Updated the fallback as suggested: shallow constraints are now only used for plain properties schemas without additionalProperties: true; non-plain/open schemas fall back to the builtin non-strict GLM envelope content. Also added the requested parametrized coverage for the reported schema shapes. Could you please take another look? Thanks! |
|
✅ @Dovis01, CI is now available for this PR.
|
|
/ci run |
|
✅ Triggered Buildkite CI #92557 for commit |
…ag without function tools Since vllm-project#56403 every GLM-4.7 request with tools gets the shallow structural tag. A Responses request whose tools are all builtin (mcp, web_search, ...) has no function tool to build a tag for, so the builder emitted TriggeredTagsFormat(tags=[]), which xgrammar rejects, and the request failed with "Invalid structural tag specification". Mirror the hermes tag and the xgrammar builtin: with no tags under tool_choice=auto, constrain to free text that excludes the GLM control tokens. Signed-off-by: Rongjie Jiang <j1870831863@outlook.com>
Purpose
Fix GLM-4.7-family tool calling when strict tool calling is disabled.
When
VLLM_ENFORCE_STRICT_TOOL_CALLING=0and no tool is markedstrict: true, the glm47 parser currently falls back to unconstrained generation. This meansauto,required, and named tool choices can emit malformed GLM tool-call XML or arguments that do not follow the provided tool schema.This PR adds a non-strict structural tag for the non-strict GLM tool-calling path. The constraint is built and registered in
vllm/tool_parsers/structural_tag_registry.py(likekimi_k3andhy_v4, which use the same XML pattern), instead of hand-written grammar rules in the parser file.The tag constrains the post-reasoning assistant output rather than only the tool arguments. It covers:
<tool_call>blocks;For example, instead of allowing the model to freely generate the native GLM tool syntax, the parser installs a structural tag that constrains the output shape while still allowing normal reasoning and text before tool calls. The thinking prefix stays owned by the engine-side reasoning gate, matching the contract of the existing strict structural-tag path.
Request handling
Glm47MoeModelToolParser.adjust_request()now attaches this tag when:tool_choice != "none";VLLM_ENFORCE_STRICT_TOOL_CALLINGis disabled;strict: true;Existing strict-tool behavior is unchanged and continues to use the xgrammar
glm_4_7builtin.Named tool choice is narrowed to the selected tool (via
normalize_tool_choice), whileautoandrequireduse the available request tools. Both Chat Completions and Responses API tools are supported, including Responses API namespace tools.Structural tag registry builder
get_glm_4_7_structural_tagis registered under"glm_4_7"invllm/tool_parsers/structural_tag_registry.py:non_strict=False, the existing strict-tool path) delegates to the xgrammarglm_4_7builtin. The delegation rebuilds the builtin's input protocol from the normalized tools (normalize is idempotent and has already filtered"forced"down to the named tool) and is verified byte-identical to the previous dispatch across {auto, required, named} x {strict, non-strict} tool sets (6/6 cases).non_strict=True) builds shallow tags from the same composable formats the other vLLM builders use (TagFormat/TriggeredTagsFormat/TagsWithSeparatorFormat/StarFormat/OrFormat/AnyTextFormat): tool names are constrained, argument keys may be omitted / repeated / emitted in any order, values are typed shallowly (strings are raw text excluding GLM structural tokens, other types are bare JSON scalars, enums enumerate), and free text cannot contain GLM structural tokens.Because the constraint is a regular structural tag, the existing plumbing is reused end-to-end (xgrammar compile path, reasoning gate, spec-decode rollback, bitmask application). No Responses API protocol or serving changes are needed; the diff stays inside the parser and the registry.
Scope
The change only affects the glm47 parser family when strict tool calling is disabled, plus a mechanical signature extension (
non_strict: bool = False) for the builders registered in the structural tag registry and the harmony builder.The existing paths remain unchanged for:
VLLM_ENFORCE_STRICT_TOOL_CALLING=1;strict: true;tool_choice="none".There are no model, scheduler, kernel, sampling, or tokenizer changes.
Performance
A/B on a GLM-4.7-format model (TP=8 H100, MTP
num_speculative_tokens=5,VLLM_ENFORCE_STRICT_TOOL_CALLING=0), 12 sequential requests per case; "valid" = structuredtool_callsreturned with no markup leakage:required, valid tool callscontent)required, latency medianauto, valid tool callsauto, latency medianauto, decode throughputrequiredstreaming TTFT / total (median)Constraint overhead measured offline with the
zai-org/GLM-4.7tokenizer: tag build ~0.8-10 ms; cold xgrammar compile 12-20 ms (compiled asynchronously in the engine); cached re-compile ~0.1 ms; compiled spec 2.6-15 KiB for 2-12 tools. No measurable latency or throughput regression on the auto path; the required path additionally goes from unusable to fully functional.Test Plan
Unit tests (run locally):
pytest tests/entrypoints/openai/responses/ -q # passedpytest tests/tool_parsers/test_glm47_moe_tool_parser.py \ tests/tool_parsers/test_glm4_moe_tool_parser.py \ tests/tool_parsers/test_structural_tag_registry.py \ tests/tool_parsers/test_utils.py -q # 315 passed pytest tests/parser -q \ --ignore=tests/parser/mistral \ --ignore=tests/parser/engine/test_delegating_replay.py \ --ignore=tests/parser/engine/test_replay.py # 786 passed pytest tests/v1/structured_output -q \ --ignore=tests/v1/structured_output/test_mistral_common_tokenizer.py # 72 passedStructural-tag validation with the tokenizer via
GrammarCompiler.compile_structural_tag+GrammarMatcher: valid turns (text + tool calls) are accepted;<tool_call>markup inside text, wrongly typed argument values (e.g. a string for anintegerparam), invalid enum values, and unknown tool names are rejected; omitted / reordered / repeated arguments are accepted (non-strict semantics); named choice narrows to the selected tool and rejects others; EOS and turn-taking stop tokens stay selectable.End-to-end (GLM-4.7-format model served with
--tool-call-parser glm47 --enable-auto-tool-choice --reasoning-parser glm45, TP=8 H100, MTP speculative decodingnum_speculative_tokens=5,VLLM_ENFORCE_STRICT_TOOL_CALLING=0), 8/8 scenarios pass:tool_choice="auto"non-strict toolstool_calls,finish_reason="tool_calls", no markup leakagetool_choice="required"tool_calls; e.g.{"city": "Beijing", "days": 3}with correct integer typingfinish_reason="stop"per OpenAI semantics"{}"enable_thinking=false<think>/<tool_call>leakageNo "Failed to advance FSM" errors appeared in the server logs across all runs.
Note: this change is output-format constrained decoding only (grammar-guided); model weights and sampling defaults are untouched, so no accuracy evaluation delta is expected beyond the format guarantees above.