Skip to content

[Frontend] glm47 tool parser: required-first schema order in prompt rendering and strict structural tags - #55558

Draft
JaredforReal wants to merge 2 commits into
vllm-project:mainfrom
JaredforReal:glm47-tool-schema-required-first
Draft

JaredforReal wants to merge 2 commits into
vllm-project:mainfrom
JaredforReal:glm47-tool-schema-required-first

Conversation

@JaredforReal

@JaredforReal JaredforReal commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Purpose

Fix a tool-calling failure mode of the glm47 parser family (GLM-4.5/4.6/4.7/5.x, incl. GLM-5.3-Flash): the model omits a required property of a nested object when a bulky optional property precedes it in the schema.

A request (a collect_preferences tool whose items[] objects require prompt + tag, with an optional choices array declared between them) fails 50-66% of the time on vLLM with GLM-5.3-Flash: the emitted items[] objects are missing tag. The same failure reproduces on sglang at the same rate (46-60%), so this is a model-behavior boundary issue, not a stack bug.

Mechanism (measured with prefix logprob probes on /v1/completions):

  • GLM renders tool schemas verbatim into the prompt and tends to emit argument keys following the schema's property order.
  • The first key of each item object is a near-tie between the content fields (p(choices)=0.62 vs p(prompt)=0.38, p(tag)~=0 — the model never opens an object with the label field). Once it starts with the optional choices array, the object "feels complete" after prompt and tag is skipped (p(tag) after choices ≈ 0.0002; after prompt ≈ 0.9998). At temp=0 the outcome is still non-deterministic — kernel noise flips the near-tie.
  • The coupling is stack-independent (vLLM: prompt-first → 65/65 keep tag, choices-first → 78% lose it; sglang: 11/11 vs 77%).

Fix (two commits on this branch):

  1. reorder_tool_schema_required_first: render each object schema's properties with required fields first (declaration order), recursing into nested object/array schemas. JSON Schema property order is not semantically meaningful, so validation and clients are unaffected. Opt-in per tool parser; enabled for the glm47 parser family. The reorder runs in OnlineRenderer right after the per-request tool dicts are built — the parser's adjust_request hook runs after chat-template rendering, which is too late to affect the prompt.
  2. Strict-mode alignment: the structural tag for guided tool calling is now built from the same reordered schema. xgrammar's builtin glm_4_7 tag pins object keys to the schema's declared order (any_order=False), so previously the prompt and the grammar disagreed: after the model committed to a required key that the prompt placed early, every optional key declared before it became unreachable and was silently dropped (and we observed a ~1-2% runaway-to-max_tokens pathology under concurrency, consistent with the grammar fighting the model's key-order prior). any_order=True is not an alternative: per GrammarCompiler.compile_json_schema it only bounds the number of entries and does not check key presence or uniqueness.

Not a duplicate: the only in-flight GLM-5.3-Flash PRs we found (#55219 KV layout, #55358 indexer move) are orthogonal; no open PR touches tool-schema rendering order or the glm47 structural tag.

Test Plan

Unit:

pytest tests/tool_parsers/test_utils.py -k ReorderPropertiesRequiredFirst   # 5 passed
pytest tests/tool_parsers/test_glm47_moe_tool_parser.py                     # incl. new -k StructuralTag (9 passed)

E2E on 4x GB300, zai-org/GLM-5.3-Flash (native FP8, TP4, --tool-call-parser glm47 --reasoning-parser glm45), the request verbatim, serial non-streaming runs:

Test Result

Schema-compliance failure rate (items[] missing the required tag), before → after:

configuration baseline with this PR
GLM-5.3-Flash, original client payload 64.5% (129/200; earlier arms 50%/66% at n=30/100) 14.0% (28/200)
GLM-5.3-Flash, same reorder applied client-side 3% (1/30)
GLM-5.3-Flash, this PR + strict: true 0% (0/144)
GLM-5.3-NVFP4 (GlmMoeDsa, same parser/template) 12% (6/50) 0% (0/50)
sglang (xinyuan/glm-5.3-flash-support), same payload, for reference 46.5% (93/200)

Residual failures all start the item object with the optional choices key first (the J1 coin flip can still land wrong; only guided decoding or a model-side fix eliminates it).

Strict mode eliminates the residual failures entirely: "strict": true on the tool definition with tool_choice: "auto" (free text and reasoning stay unconstrained; once the model starts a <tool_call>, its arguments are grammar-enforced to match the schema). No server-side flag needed beyond the parser flags above.

Performance

Prompt-side reorder cost (reorder_properties_required_first + the per-request model_dump copy it operates on): ~48 µs per request for the original schema (9 µs for the reorder walk alone; the deepcopy dominates and model_dump was already happening). Negligible against ~1s request latencies.

Strict-mode guided decoding overhead, measured on the same 4x GB300 GLM-5.3-Flash setup with the request (~180 completion tokens, almost all inside the grammar-constrained <arg_value> body — a near-worst-case constrained fraction):

concurrency (n) baseline strict (this PR)
1 (32) 1.21s p50, 143.9 tok/s/req 1.09s p50, 143.9 tok/s/req
16 (48) 2.89s p50, 6.45s p99 2.46s p50, 7.66s p99
32 (64) 3.74s p50, 7.78s p99 3.51s p50, 7.75s p99
64 (128) 4.90s p50, 1677 tok/s agg, 9.81 req/s 4.84s p50, 1768 tok/s agg, 9.94 req/s

Strict is never slower (slightly faster in aggregate: constrained outputs are shorter and more regular). TTFT is unchanged once the grammar is compiled; the first request with a new schema pays the ~0.5s compile (cached per schema afterwards).

Per-token grammar cost (isolated microbenchmark of the xgrammar matcher on this schema): fill_next_token_bitmask 82.5 µs/tok + accept_token 1.0 µs/tok ≈ 83.5 µs per constrained token, i.e. ~8.4% of one CPU core per 1000 constrained tok/s of aggregate traffic. The glm_4_7 structural tag is trigger-gated: only tokens inside <tool_call> blocks pay this; reasoning and free text are unconstrained.

Behavioral scope note: the reorder changes the rendered prompt for every model served with --tool-call-parser glm47/glm45 (GLM-4.5/4.6/4.7/5.x). On the GLM-5.3 family it strictly helps (table above); other glm47-family models share the same emission-order prior.

docs/features/tool_calling.md updated for the GLM-5.x entry.

This change was implemented with AI assistance and reviewed by JaredforReal.

…first

GLM-4.7/5.x renders tool schemas verbatim into the prompt, and the
model tends to emit argument keys in the rendered property order. When
a bulky optional field precedes a small required one (e.g. a "choices"
array before a required "tag" label), the model often never returns to
the required field once the object feels complete. Reproduced on
GLM-5.3-Flash with a customer-reported request: 50-66% of
collect_preferences calls omitted the required "tag" (temp=1; ~50%
even at temp=0 - the first key of each item object is a near-tie
between the content fields, p(tag-first) ~= 0).

Reordering each object schema's "properties" so fields declared in
"required" come first (in declaration order) changes the trajectory:
with prompt -> tag -> choices the model commits to the required label
right after the question (measured p(tag | prompt written) ~ 0.9998,
vs ~0.0002 after a "choices"-first start). JSON Schema property order
is not semantically meaningful, so validation and clients are
unaffected.

Opt in per tool parser via the reorder_tool_schema_required_first
class attribute; enabled for the glm47 parser family. The reorder runs
in OnlineRenderer right after the per-request tool dicts are built -
the parser's adjust_request hook runs after chat-template rendering,
which is too late to affect the prompt.

Tested on zai-org/GLM-5.3-Flash (4x GB300, TP4, native FP8,
--tool-call-parser glm47 --reasoning-parser glm45), customer-reported
request, n=30 serial non-streaming runs per configuration:
- baseline (original schema order): 50% (n=30) / 66% (n=100) failures
- this change, original client payload: 13% (4/30)
- same reorder applied client-side: 3% (1/30)
Residual failures all start the item object with the optional
"choices" key; temp=0 and streaming behave the same, and MTP spec
decode on/off does not change the rate.

Unit tests:
pytest tests/tool_parsers/test_utils.py -k ReorderPropertiesRequiredFirst
pytest tests/tool_parsers/test_glm47_moe_tool_parser.py

This change was implemented with AI assistance (Kimi Code CLI).

Co-authored-by: Kimi Code CLI <noreply@moonshot.cn>
Signed-off-by: Jared Wen <w13431838023@gmail.com>
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JaredforReal JaredforReal changed the title [Frontend] glm47 tool parser: render tool schema properties required-… [Frontend] glm47 tool parser: render tool schema properties Sep 6, 2026
…he required-first schema

The glm47 parser renders tool schemas into the prompt with required
properties first (reorder_tool_schema_required_first), but the strict-mode
structural tag was still built from the client's original schema order.
xgrammar's builtin glm_4_7 tag pins object keys to the schema's declared
order (any_order=False), so the prompt and the grammar disagreed: after the
model committed to a required key that the prompt placed early, every
optional key declared before it became unreachable and was silently dropped.

Declare reorder_tool_schema_required_first on ToolParser and apply the same
reorder to per-request copies of the tools before get_model_structural_tag,
so the grammar enforces exactly the order the model was shown.
request.tools is left untouched.

xgrammar's any_order=True is not an alternative: as documented in
GrammarCompiler.compile_json_schema, it only bounds the number of entries and
does not check key presence or uniqueness, so required keys can still be
missing (verified: {"a":1,"c":3} is accepted for required=["a","b"]).

Tests:
  pytest tests/tool_parsers/test_glm47_moe_tool_parser.py -k StructuralTag  (9 passed)
  pytest tests/tool_parsers/test_utils.py -k Reorder                          (5 passed)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jared Wen <jaredwen@inferact.ai>
@mergify

mergify Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--55558.org.readthedocs.build/en/55558/

@mergify mergify Bot added the documentation Improvements or additions to documentation label Sep 7, 2026
@JaredforReal JaredforReal changed the title [Frontend] glm47 tool parser: render tool schema properties [Frontend] glm47 tool parser: required-first schema order in prompt rendering and strict structural tags Sep 7, 2026
@gaby

gaby commented Sep 12, 2026

Copy link
Copy Markdown

@JaredforReal

Copy link
Copy Markdown
Contributor Author

thanks gaby, and no, this is for tool call related argument tag loss @gaby

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation glm tool-calling

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants