Repository navigation
[Rust Frontend] Replay roundtrip output grammars through XGrammar - #59143
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
|
/ci run --allow-stale |
|
This pull request has merge conflicts that must be resolved before it can be |
537445f to
2571c36
Compare
d2f732d to
e4e6f7f
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
e4e6f7f to
52c6aa3
Compare
52c6aa3 to
58ae5aa
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #92000 for commit |
|
Hi @BugenZhao, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
Export the output grammar built by the initialized roundtrip parser, with the rendered completion as its generation, as checked-in replay cases. A PEP 723 uv script compiles every case with the real XGrammar and replays token-zero cases through the matcher, and a nextest wrapper runs it in the Rust CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Run the roundtrip tool-call fixture once per tool-choice variant: `auto` with content before the calls, and `required` with tool calls only. Each model's variants share one replay file with model-level vocab size and stop set. Kimi K3 and MiMo V2.6 run `auto` only: the K3 renderer appends a tool-choice system message after the history, and MiMo renders compact tool calls that its Qwen3-Coder builder tags do not match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Add a named `get_weather` variant to the roundtrip tool-call fixture, with a single call and no content, and replay its grammar alongside the `auto` and `required` variants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
With the MiMo structural-tag builder, MiMo's grammar tags match the compact `<tool_call><function=...>` calls its template renders, so run every tool-choice variant for MiMo. Regenerate the replay files for the `excludes` field that `xgrammar-structural-tag` 0.4.0 serializes on JSON schema formats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
…essages Kimi K3 renders request-scoped control messages, such as the `tool_choice=required` instruction, after the whole history. The generation prompt is then `A + C + G` while the closed history is `A + G + B + C`, so the prompt is no longer a prefix of the full render. Locate the completion `B` by finding the unique split of `C` and `G`, falling back to the prefix cut when `C` is empty. This lets Kimi K3 run the `required` variant, and every model now runs every tool-choice variant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
58ae5aa to
e6ce31d
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #92066 for commit |
Purpose
Rust-built output grammars are currently checked only as structural-tag JSON. Nothing compiles them with the real XGrammar or verifies that the matcher accepts a real generation from token zero, which is the meaning of
reasoning_ended = Some(true)since #57340.Step 2.5 of the parser-owned output grammar rollout, following the merged #55269 and #57340. Stacked on #59148, which switches Rust MiMo to the MiMo structural-tag builder in
xgrammar-structural-tag0.4.1; only the last five commits belong to this PR. It adds test coverage only; production behavior is unchanged. The open-PR search found no other XGrammar replay harness for the Rust frontend.--tool-strict-level parameterso every builder-backed parser emits a grammar; the strict level affects grammar inputs only, not parsing.tool_call_mixruns once per tool-choice variant for every model:auto, with content before the tool calls;required, with tool calls only, the only shape builder grammars admit underrequired;get_weather, with a single call and no content.rust/src/chat/tests/grammar_replay/<model>.jsonviaexpect_file!. The file records the model, model vocab size, and lowered stop set once. Each variant case records theBuiltOutputGrammar, the completion text, and the generation token IDs ending with a stop token. A stale file for a parser that no longer builds a grammar fails the roundtrip.grammar_replay.pyis a PEP 723 uv script pinned to thexgrammarversion inrequirements/common.txt, with CPU torch. It buildsTokenizerInfoand compiles each case the same way asbackend_xgrammar.py. Forfrom_token_zerocases it replays every token, checking both the bitmask andaccept_token, and requires the stop token to terminate the matcher withoverride_stop_tokens.final_output_onlycases are compiled only. It also requires the HF tokenizer to encode the completion to the recorded IDs, so tokenizer drift fails loudly.grammar_replay.rswraps the script as a nextest test, so the existing Rust Frontend Cargo CI step runs it. That step already installs uv.BuiltOutputGrammarandGrammarCoveragederiveSerialize. serde_json'sraw_valueis enabled for chat dev-dependencies only, to keep token ID arrays on one line.Kimi K3 renders request-scoped control messages, such as the
tool_choice=requiredinstruction, after the whole history, as the officialencoding_k3.pydoes. The generation prompt is thenA + C + Gwhile the closed history isA + G + B + C, so the prompt is no longer a prefix of the full render. The roundtrip now locates the completionBas the unique split ofCandG. It falls back to the prefix cut whenCis empty, which is the case for every other template. K3's grammar remainsfinal_output_onlyhere; it becomes a token-zero replay case with its native builder in Step 3.The replay found the MiMo V2.6 grammar mismatch that #59148 fixes. MiMo renders compact
<tool_call><function=...>calls, but on main the Rust MiMo parser reuses the Qwen3-Coder builder, whose tags are newline-delimited. Underrequiredthe replay rejected the call. Underautothe trigger never fired, so the grammar left the calls unconstrained. With #59148, MiMo runs all three variants, and itsautotrigger and tags are compact. #58019 is the Python counterpart.Test Plan
Negative controls: I copied the cases, mutated them, and ran the script on the copies.
Test Result
xgrammar-structural-tag0.4.1 output replayed with XGrammar 0.2.7, the version pinned inrequirements/common.txt. XGrammar 0.2.7 ignores the newexcludesfield. The runner pin moves together withrequirements/common.txt.auto,required, and named, replayed from token zero and terminated: Qwen3, Qwen3.5, MiMo V2.6, MiniMax M2.5, DeepSeek V3.2/V4/V4.1, GLM-4.7/5.2, Hy3, and Kimi K2.5.auto,required, and named,final_output_only, compiled.from_token_zero: rejectsthinkafter<|close|>.<think>at token 0.<|im_end|>.requiredgeneration rejects the second call right after the first closes, for Qwen3, DeepSeek V4, and GLM-4.7.vllm-parser+vllm-chat: 860 tests passed, including a unit test for the completion split. Workspace Clippy,cargo sort, and pre-commit passed.trust_remote_code=Trueis needed only for the Kimi K3 tokenizer. It is kept for all cases because production deployments of these models load their tokenizers with it.AI assistance: implementation and validation assisted by Claude Code.
🤖 Generated with Claude Code