[Rust Frontend] Replay roundtrip output grammars through XGrammar (#59143)
Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bugen Zhao committed
Sep 30, 2026 at 15:29 UTC
87228cff01d7a56988e572c9a689444728f26568
17 files changed
+4486
-92
rust/src/chat/Cargo.toml
+1
@@ -46,6 +46,7 @@ clap.workspace = true
46
expect-test.workspace = true
47
paste.workspace = true
48
rmp-serde.workspace = true
49
+serde_json = { workspace = true, features = ["raw_value"] }
50
serial_test.workspace = true
51
tempfile.workspace = true
52
tokio.workspace = true
rust/src/chat/tests/grammar_replay.py
new
+113
@@ -0,0 +1,113 @@
1
+#!/usr/bin/env -S uv run --script
2
+# SPDX-License-Identifier: Apache-2.0
3
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4
+# /// script
5
+# requires-python = ">=3.11"
6
+# dependencies = [
7
+# "tiktoken>=0.6.0",
8
+# # xgrammar imports torch unconditionally; only the CPU build is needed.
9
+# "torch",
10
+# "transformers>=5.10.4",
11
+# # Keep in sync with requirements/common.txt.
12
+# "xgrammar==0.2.7",
13
+# ]
14
+#
15
+# [[tool.uv.index]]
16
+# name = "pytorch-cpu"
17
+# url = "https://download.pytorch.org/whl/cpu"
18
+# explicit = true
19
+#
20
+# [tool.uv.sources]
21
+# torch = { index = "pytorch-cpu" }
22
+# ///
23
+"""Replay the grammar cases exported by the roundtrip tests through XGrammar.
24
+
25
+Each file holds one model's cases, keyed by tool-choice variant. Every case is
26
+compiled against the model tokenizer the same way as
27
+`vllm/v1/structured_output/backend_xgrammar.py`. Token-zero cases are then
28
+replayed from the first generated token: each token must be allowed by the
29
+bitmask and accepted by the matcher, and the final stop token must terminate it.
30
+"""
31
+
32
+import json
33
+import sys
34
+from functools import cache
35
+from pathlib import Path
36
+
37
+import xgrammar as xgr
38
+from transformers import AutoTokenizer
39
+
40
+CASES_DIR = Path(__file__).parent / "grammar_replay"
41
+
42
+
43
+@cache
44
+def load_tokenizer(model_id: str, vocab_size: int):
45
+ tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
46
+ tokenizer_info = xgr.TokenizerInfo.from_huggingface(
47
+ tokenizer, vocab_size=vocab_size
48
+ )
49
+ return tokenizer, xgr.GrammarCompiler(tokenizer_info)
50
+
51
+
52
+def replay(model: dict, case: dict) -> str:
53
+ """Check one case of a model and return a summary of what was checked."""
54
+ tokenizer, compiler = load_tokenizer(model["model_id"], model["vocab_size"])
55
+ generation = case["generation_token_ids"]
56
+
57
+ # The case token IDs come from the Rust tokenizer; the runner must load the
58
+ # same vocabulary for the grammar to mean the same thing.
59
+ encoded = tokenizer.encode(case["completion"], add_special_tokens=False)
60
+ if encoded != generation[:-1]:
61
+ raise AssertionError(
62
+ "tokenizer mismatch: the completion encodes to "
63
+ f"{encoded}, but the case records {generation[:-1]}"
64
+ )
65
+
66
+ grammar = case["grammar"]
67
+ structural_tag = {"type": "structural_tag", "format": grammar["format"]}
68
+ compiled = compiler.compile_structural_tag(json.dumps(structural_tag))
69
+ if grammar["coverage"] != "from_token_zero":
70
+ return f"compiled only ({grammar['coverage']} is gated by the engine)"
71
+
72
+ matcher = xgr.GrammarMatcher(
73
+ compiled, override_stop_tokens=model["all_stop_token_ids"]
74
+ )
75
+ bitmask = xgr.allocate_token_bitmask(1, model["vocab_size"])
76
+ for index, token_id in enumerate(generation):
77
+ matcher.fill_next_token_bitmask(bitmask)
78
+ allowed = (bitmask[0, token_id // 32].item() >> (token_id % 32)) & 1
79
+ if not allowed or not matcher.accept_token(token_id):
80
+ prefix = tokenizer.decode(generation[:index])
81
+ raise AssertionError(
82
+ f"token {index} ({token_id}, {tokenizer.decode([token_id])!r}) "
83
+ f"rejected after {prefix!r}"
84
+ )
85
+ if not matcher.is_terminated():
86
+ raise AssertionError("matcher not terminated after the stop token")
87
+ return f"replayed {len(generation)} tokens from token zero"
88
+
89
+
90
+def main() -> int:
91
+ paths = sorted(CASES_DIR.glob("*.json"))
92
+ if not paths:
93
+ print(f"no grammar replay cases in {CASES_DIR}", file=sys.stderr)
94
+ return 1
95
+
96
+ failures = 0
97
+ for path in paths:
98
+ model = json.loads(path.read_text())
99
+ for name, case in model["cases"].items():
100
+ try:
101
+ summary = replay(model, case)
102
+ except Exception as error:
103
+ failures += 1
104
+ coverage = case["grammar"]["coverage"]
105
+ print(f"FAIL {path.name} {name} [{coverage}]: {error}", file=sys.stderr)
106
+ else:
107
+ print(f"PASS {path.name} {name}: {summary}")
108
+
109
+ return 1 if failures else 0
110
+
111
+
112
+if __name__ == "__main__":
113
+ sys.exit(main())
rust/src/chat/tests/grammar_replay.rs
new
+24
@@ -0,0 +1,24 @@
1
+// SPDX-License-Identifier: Apache-2.0
2
+// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+//! Replays the grammar cases exported by the roundtrip tests through the real
5
+//! XGrammar, so that parser-built output grammars are checked against the
6
+//! engine's compiler and matcher.
7
+
8
+use std::path::PathBuf;
9
+use std::process::Command;
10
+
11
+#[test]
12
+fn xgrammar_accepts_roundtrip_generations() {
13
+ let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/grammar_replay.py");
14
+ let output = Command::new(&script)
15
+ .output()
16
+ .unwrap_or_else(|error| panic!("failed to execute {:?}: {error}", script));
17
+ assert!(
18
+ output.status.success(),
19
+ "grammar replay failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
20
+ output.status.code(),
21
+ String::from_utf8_lossy(&output.stdout),
22
+ String::from_utf8_lossy(&output.stderr),
23
+ );
24
+}
rust/src/chat/tests/grammar_replay/DeepSeek-V3.2-Exp.json
new
+274
@@ -0,0 +1,274 @@
1
+{
2
+ "model_id": "deepseek-ai/DeepSeek-V3.2-Exp",
3
+ "vocab_size": 129280,
4
+ "all_stop_token_ids": [1],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<|DSML|function_calls>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<|DSML|function_calls>\n",
31
+ "content": {
32
+ "type": "tags_with_separator",
33
+ "tags": [
34
+ {
35
+ "type": "tag",
36
+ "begin": "<|DSML|invoke name=\"get_weather\">\n",
37
+ "content": {
38
+ "type": "json_schema",
39
+ "json_schema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "location": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "required": [
47
+ "location"
48
+ ]
49
+ },
50
+ "style": "deepseek_xml",
51
+ "any_order": false,
52
+ "max_whitespace_cnt": null,
53
+ "excludes": []
54
+ },
55
+ "end": "</|DSML|invoke>\n"
56
+ },
57
+ {
58
+ "type": "tag",
59
+ "begin": "<|DSML|invoke name=\"add\">\n",
60
+ "content": {
61
+ "type": "json_schema",
62
+ "json_schema": {
63
+ "type": "object",
64
+ "properties": {
65
+ "y": {
66
+ "type": "number"
67
+ },
68
+ "x": {
69
+ "type": "number"
70
+ },
71
+ "items": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "string"
75
+ }
76
+ }
77
+ },
78
+ "required": [
79
+ "y",
80
+ "x",
81
+ "items"
82
+ ]
83
+ },
84
+ "style": "deepseek_xml",
85
+ "any_order": false,
86
+ "max_whitespace_cnt": null,
87
+ "excludes": []
88
+ },
89
+ "end": "</|DSML|invoke>\n"
90
+ }
91
+ ],
92
+ "separator": "",
93
+ "at_least_one": true,
94
+ "stop_after_first": false
95
+ },
96
+ "end": "</|DSML|function_calls>"
97
+ }
98
+ ],
99
+ "at_least_one": false,
100
+ "stop_after_first": false,
101
+ "excludes": [
102
+ "<think>",
103
+ "</think>"
104
+ ]
105
+ }
106
+ ]
107
+ },
108
+ "coverage": "from_token_zero"
109
+ },
110
+ "completion": "Need call the weather and add tools.</think>I will call the tools.\n\n<|DSML|function_calls>\n<|DSML|invoke name=\"get_weather\">\n<|DSML|parameter name=\"location\" string=\"true\">Shanghai</|DSML|parameter>\n</|DSML|invoke>\n<|DSML|invoke name=\"add\">\n<|DSML|parameter name=\"y\" string=\"false\">1.0</|DSML|parameter>\n<|DSML|parameter name=\"x\" string=\"false\">2</|DSML|parameter>\n<|DSML|parameter name=\"items\" string=\"false\">[\"left\", \"right\"]</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_calls>",
111
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128799, 43, 759, 1926, 270, 6704, 339, 30, 28217, 10525, 7398, 28217, 8701, 4941, 12548, 1018, 30, 28217, 10525, 7398, 28217, 40148, 5406, 2329, 1281, 1133, 65, 50219, 3816, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 1718, 28217, 10525, 7398, 28217, 40148, 5406, 1018, 30, 28217, 10525, 7398, 28217, 40148, 5406, 2329, 1281, 3327, 3816, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 91, 4, 3418, 1281, 19836, 3320, 19, 16, 18, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 90, 4, 3418, 1281, 19836, 3320, 20, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 17173, 4, 3418, 1281, 19836, 3320, 10931, 4589, 1760, 582, 2459, 13747, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 1718, 28217, 10525, 7398, 28217, 40148, 5406, 1018, 1718, 28217, 10525, 7398, 28217, 8701, 4941, 12548, 32, 1]
112
+ },
113
+ "required": {
114
+ "grammar": {
115
+ "format": {
116
+ "type": "sequence",
117
+ "elements": [
118
+ {
119
+ "type": "tag",
120
+ "begin": "",
121
+ "content": {
122
+ "type": "any_text",
123
+ "excludes": [],
124
+ "max_tokens": null,
125
+ "max_chars": null
126
+ },
127
+ "end": "</think>"
128
+ },
129
+ {
130
+ "type": "sequence",
131
+ "elements": [
132
+ {
133
+ "type": "const_string",
134
+ "value": "\n\n<|DSML|function_calls>\n"
135
+ },
136
+ {
137
+ "type": "tags_with_separator",
138
+ "tags": [
139
+ {
140
+ "type": "tag",
141
+ "begin": "<|DSML|invoke name=\"get_weather\">\n",
142
+ "content": {
143
+ "type": "json_schema",
144
+ "json_schema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "location": {
148
+ "type": "string"
149
+ }
150
+ },
151
+ "required": [
152
+ "location"
153
+ ]
154
+ },
155
+ "style": "deepseek_xml",
156
+ "any_order": false,
157
+ "max_whitespace_cnt": null,
158
+ "excludes": []
159
+ },
160
+ "end": "</|DSML|invoke>\n"
161
+ },
162
+ {
163
+ "type": "tag",
164
+ "begin": "<|DSML|invoke name=\"add\">\n",
165
+ "content": {
166
+ "type": "json_schema",
167
+ "json_schema": {
168
+ "type": "object",
169
+ "properties": {
170
+ "y": {
171
+ "type": "number"
172
+ },
173
+ "x": {
174
+ "type": "number"
175
+ },
176
+ "items": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "string"
180
+ }
181
+ }
182
+ },
183
+ "required": [
184
+ "y",
185
+ "x",
186
+ "items"
187
+ ]
188
+ },
189
+ "style": "deepseek_xml",
190
+ "any_order": false,
191
+ "max_whitespace_cnt": null,
192
+ "excludes": []
193
+ },
194
+ "end": "</|DSML|invoke>\n"
195
+ }
196
+ ],
197
+ "separator": "",
198
+ "at_least_one": true,
199
+ "stop_after_first": false
200
+ },
201
+ {
202
+ "type": "const_string",
203
+ "value": "</|DSML|function_calls>"
204
+ }
205
+ ]
206
+ }
207
+ ]
208
+ },
209
+ "coverage": "from_token_zero"
210
+ },
211
+ "completion": "Need call the weather and add tools.</think>\n\n<|DSML|function_calls>\n<|DSML|invoke name=\"get_weather\">\n<|DSML|parameter name=\"location\" string=\"true\">Shanghai</|DSML|parameter>\n</|DSML|invoke>\n<|DSML|invoke name=\"add\">\n<|DSML|parameter name=\"y\" string=\"false\">1.0</|DSML|parameter>\n<|DSML|parameter name=\"x\" string=\"false\">2</|DSML|parameter>\n<|DSML|parameter name=\"items\" string=\"false\">[\"left\", \"right\"]</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_calls>",
212
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128799, 271, 30, 28217, 10525, 7398, 28217, 8701, 4941, 12548, 1018, 30, 28217, 10525, 7398, 28217, 40148, 5406, 2329, 1281, 1133, 65, 50219, 3816, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 1718, 28217, 10525, 7398, 28217, 40148, 5406, 1018, 30, 28217, 10525, 7398, 28217, 40148, 5406, 2329, 1281, 3327, 3816, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 91, 4, 3418, 1281, 19836, 3320, 19, 16, 18, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 90, 4, 3418, 1281, 19836, 3320, 20, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 17173, 4, 3418, 1281, 19836, 3320, 10931, 4589, 1760, 582, 2459, 13747, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 1718, 28217, 10525, 7398, 28217, 40148, 5406, 1018, 1718, 28217, 10525, 7398, 28217, 8701, 4941, 12548, 32, 1]
213
+ },
214
+ "named": {
215
+ "grammar": {
216
+ "format": {
217
+ "type": "sequence",
218
+ "elements": [
219
+ {
220
+ "type": "tag",
221
+ "begin": "",
222
+ "content": {
223
+ "type": "any_text",
224
+ "excludes": [],
225
+ "max_tokens": null,
226
+ "max_chars": null
227
+ },
228
+ "end": "</think>"
229
+ },
230
+ {
231
+ "type": "sequence",
232
+ "elements": [
233
+ {
234
+ "type": "const_string",
235
+ "value": "\n\n<|DSML|function_calls>\n"
236
+ },
237
+ {
238
+ "type": "tag",
239
+ "begin": "<|DSML|invoke name=\"get_weather\">\n",
240
+ "content": {
241
+ "type": "json_schema",
242
+ "json_schema": {
243
+ "type": "object",
244
+ "properties": {
245
+ "location": {
246
+ "type": "string"
247
+ }
248
+ },
249
+ "required": [
250
+ "location"
251
+ ]
252
+ },
253
+ "style": "deepseek_xml",
254
+ "any_order": false,
255
+ "max_whitespace_cnt": null,
256
+ "excludes": []
257
+ },
258
+ "end": "</|DSML|invoke>\n"
259
+ },
260
+ {
261
+ "type": "const_string",
262
+ "value": "</|DSML|function_calls>"
263
+ }
264
+ ]
265
+ }
266
+ ]
267
+ },
268
+ "coverage": "from_token_zero"
269
+ },
270
+ "completion": "Need call the weather and add tools.</think>\n\n<|DSML|function_calls>\n<|DSML|invoke name=\"get_weather\">\n<|DSML|parameter name=\"location\" string=\"true\">Shanghai</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_calls>",
271
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128799, 271, 30, 28217, 10525, 7398, 28217, 8701, 4941, 12548, 1018, 30, 28217, 10525, 7398, 28217, 40148, 5406, 2329, 1281, 1133, 65, 50219, 3816, 30, 28217, 10525, 7398, 28217, 41523, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 28217, 10525, 7398, 28217, 41523, 1018, 1718, 28217, 10525, 7398, 28217, 40148, 5406, 1018, 1718, 28217, 10525, 7398, 28217, 8701, 4941, 12548, 32, 1]
272
+ }
273
+ }
274
+}
rust/src/chat/tests/grammar_replay/DeepSeek-V4-Flash.json
new
+274
@@ -0,0 +1,274 @@
1
+{
2
+ "model_id": "deepseek-ai/DeepSeek-V4-Flash",
3
+ "vocab_size": 129280,
4
+ "all_stop_token_ids": [1],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<|DSML|tool_calls>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<|DSML|tool_calls>\n",
31
+ "content": {
32
+ "type": "tags_with_separator",
33
+ "tags": [
34
+ {
35
+ "type": "tag",
36
+ "begin": "<|DSML|invoke name=\"get_weather\">\n",
37
+ "content": {
38
+ "type": "json_schema",
39
+ "json_schema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "location": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "required": [
47
+ "location"
48
+ ]
49
+ },
50
+ "style": "deepseek_xml",
51
+ "any_order": false,
52
+ "max_whitespace_cnt": null,
53
+ "excludes": []
54
+ },
55
+ "end": "</|DSML|invoke>\n"
56
+ },
57
+ {
58
+ "type": "tag",
59
+ "begin": "<|DSML|invoke name=\"add\">\n",
60
+ "content": {
61
+ "type": "json_schema",
62
+ "json_schema": {
63
+ "type": "object",
64
+ "properties": {
65
+ "y": {
66
+ "type": "number"
67
+ },
68
+ "x": {
69
+ "type": "number"
70
+ },
71
+ "items": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "string"
75
+ }
76
+ }
77
+ },
78
+ "required": [
79
+ "y",
80
+ "x",
81
+ "items"
82
+ ]
83
+ },
84
+ "style": "deepseek_xml",
85
+ "any_order": false,
86
+ "max_whitespace_cnt": null,
87
+ "excludes": []
88
+ },
89
+ "end": "</|DSML|invoke>\n"
90
+ }
91
+ ],
92
+ "separator": "",
93
+ "at_least_one": true,
94
+ "stop_after_first": false
95
+ },
96
+ "end": "</|DSML|tool_calls>"
97
+ }
98
+ ],
99
+ "at_least_one": false,
100
+ "stop_after_first": false,
101
+ "excludes": [
102
+ "<think>",
103
+ "</think>"
104
+ ]
105
+ }
106
+ ]
107
+ },
108
+ "coverage": "from_token_zero"
109
+ },
110
+ "completion": "Need call the weather and add tools.</think>I will call the tools.\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"get_weather\">\n<|DSML|parameter name=\"location\" string=\"true\">Shanghai</|DSML|parameter>\n</|DSML|invoke>\n<|DSML|invoke name=\"add\">\n<|DSML|parameter name=\"y\" string=\"false\">1.0</|DSML|parameter>\n<|DSML|parameter name=\"x\" string=\"false\">2</|DSML|parameter>\n<|DSML|parameter name=\"items\" string=\"false\">[\"left\", \"right\"]</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>",
111
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128822, 43, 759, 1926, 270, 6704, 339, 30, 128825, 72461, 4941, 12548, 1018, 30, 128825, 40148, 5406, 2329, 1281, 1133, 65, 50219, 3816, 30, 128825, 41523, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 128825, 41523, 1018, 1718, 128825, 40148, 5406, 1018, 30, 128825, 40148, 5406, 2329, 1281, 3327, 3816, 30, 128825, 41523, 2329, 1281, 91, 4, 3418, 1281, 19836, 3320, 19, 16, 18, 1718, 128825, 41523, 1018, 30, 128825, 41523, 2329, 1281, 90, 4, 3418, 1281, 19836, 3320, 20, 1718, 128825, 41523, 1018, 30, 128825, 41523, 2329, 1281, 17173, 4, 3418, 1281, 19836, 3320, 10931, 4589, 1760, 582, 2459, 13747, 1718, 128825, 41523, 1018, 1718, 128825, 40148, 5406, 1018, 1718, 128825, 72461, 4941, 12548, 32, 1]
112
+ },
113
+ "required": {
114
+ "grammar": {
115
+ "format": {
116
+ "type": "sequence",
117
+ "elements": [
118
+ {
119
+ "type": "tag",
120
+ "begin": "",
121
+ "content": {
122
+ "type": "any_text",
123
+ "excludes": [],
124
+ "max_tokens": null,
125
+ "max_chars": null
126
+ },
127
+ "end": "</think>"
128
+ },
129
+ {
130
+ "type": "sequence",
131
+ "elements": [
132
+ {
133
+ "type": "const_string",
134
+ "value": "\n\n<|DSML|tool_calls>\n"
135
+ },
136
+ {
137
+ "type": "tags_with_separator",
138
+ "tags": [
139
+ {
140
+ "type": "tag",
141
+ "begin": "<|DSML|invoke name=\"get_weather\">\n",
142
+ "content": {
143
+ "type": "json_schema",
144
+ "json_schema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "location": {
148
+ "type": "string"
149
+ }
150
+ },
151
+ "required": [
152
+ "location"
153
+ ]
154
+ },
155
+ "style": "deepseek_xml",
156
+ "any_order": false,
157
+ "max_whitespace_cnt": null,
158
+ "excludes": []
159
+ },
160
+ "end": "</|DSML|invoke>\n"
161
+ },
162
+ {
163
+ "type": "tag",
164
+ "begin": "<|DSML|invoke name=\"add\">\n",
165
+ "content": {
166
+ "type": "json_schema",
167
+ "json_schema": {
168
+ "type": "object",
169
+ "properties": {
170
+ "y": {
171
+ "type": "number"
172
+ },
173
+ "x": {
174
+ "type": "number"
175
+ },
176
+ "items": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "string"
180
+ }
181
+ }
182
+ },
183
+ "required": [
184
+ "y",
185
+ "x",
186
+ "items"
187
+ ]
188
+ },
189
+ "style": "deepseek_xml",
190
+ "any_order": false,
191
+ "max_whitespace_cnt": null,
192
+ "excludes": []
193
+ },
194
+ "end": "</|DSML|invoke>\n"
195
+ }
196
+ ],
197
+ "separator": "",
198
+ "at_least_one": true,
199
+ "stop_after_first": false
200
+ },
201
+ {
202
+ "type": "const_string",
203
+ "value": "</|DSML|tool_calls>"
204
+ }
205
+ ]
206
+ }
207
+ ]
208
+ },
209
+ "coverage": "from_token_zero"
210
+ },
211
+ "completion": "Need call the weather and add tools.</think>\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"get_weather\">\n<|DSML|parameter name=\"location\" string=\"true\">Shanghai</|DSML|parameter>\n</|DSML|invoke>\n<|DSML|invoke name=\"add\">\n<|DSML|parameter name=\"y\" string=\"false\">1.0</|DSML|parameter>\n<|DSML|parameter name=\"x\" string=\"false\">2</|DSML|parameter>\n<|DSML|parameter name=\"items\" string=\"false\">[\"left\", \"right\"]</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>",
212
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128822, 271, 30, 128825, 72461, 4941, 12548, 1018, 30, 128825, 40148, 5406, 2329, 1281, 1133, 65, 50219, 3816, 30, 128825, 41523, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 128825, 41523, 1018, 1718, 128825, 40148, 5406, 1018, 30, 128825, 40148, 5406, 2329, 1281, 3327, 3816, 30, 128825, 41523, 2329, 1281, 91, 4, 3418, 1281, 19836, 3320, 19, 16, 18, 1718, 128825, 41523, 1018, 30, 128825, 41523, 2329, 1281, 90, 4, 3418, 1281, 19836, 3320, 20, 1718, 128825, 41523, 1018, 30, 128825, 41523, 2329, 1281, 17173, 4, 3418, 1281, 19836, 3320, 10931, 4589, 1760, 582, 2459, 13747, 1718, 128825, 41523, 1018, 1718, 128825, 40148, 5406, 1018, 1718, 128825, 72461, 4941, 12548, 32, 1]
213
+ },
214
+ "named": {
215
+ "grammar": {
216
+ "format": {
217
+ "type": "sequence",
218
+ "elements": [
219
+ {
220
+ "type": "tag",
221
+ "begin": "",
222
+ "content": {
223
+ "type": "any_text",
224
+ "excludes": [],
225
+ "max_tokens": null,
226
+ "max_chars": null
227
+ },
228
+ "end": "</think>"
229
+ },
230
+ {
231
+ "type": "sequence",
232
+ "elements": [
233
+ {
234
+ "type": "const_string",
235
+ "value": "\n\n<|DSML|tool_calls>\n"
236
+ },
237
+ {
238
+ "type": "tag",
239
+ "begin": "<|DSML|invoke name=\"get_weather\">\n",
240
+ "content": {
241
+ "type": "json_schema",
242
+ "json_schema": {
243
+ "type": "object",
244
+ "properties": {
245
+ "location": {
246
+ "type": "string"
247
+ }
248
+ },
249
+ "required": [
250
+ "location"
251
+ ]
252
+ },
253
+ "style": "deepseek_xml",
254
+ "any_order": false,
255
+ "max_whitespace_cnt": null,
256
+ "excludes": []
257
+ },
258
+ "end": "</|DSML|invoke>\n"
259
+ },
260
+ {
261
+ "type": "const_string",
262
+ "value": "</|DSML|tool_calls>"
263
+ }
264
+ ]
265
+ }
266
+ ]
267
+ },
268
+ "coverage": "from_token_zero"
269
+ },
270
+ "completion": "Need call the weather and add tools.</think>\n\n<|DSML|tool_calls>\n<|DSML|invoke name=\"get_weather\">\n<|DSML|parameter name=\"location\" string=\"true\">Shanghai</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>",
271
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128822, 271, 30, 128825, 72461, 4941, 12548, 1018, 30, 128825, 40148, 5406, 2329, 1281, 1133, 65, 50219, 3816, 30, 128825, 41523, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 128825, 41523, 1018, 1718, 128825, 40148, 5406, 1018, 1718, 128825, 72461, 4941, 12548, 32, 1]
272
+ }
273
+ }
274
+}
rust/src/chat/tests/grammar_replay/DeepSeek-V4.1-Flash.json
new
+274
@@ -0,0 +1,274 @@
1
+{
2
+ "model_id": "deepseek-ai/DeepSeek-V4.1-Flash",
3
+ "vocab_size": 129280,
4
+ "all_stop_token_ids": [1],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<|DSML| calls>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<|DSML| calls>\n",
31
+ "content": {
32
+ "type": "tags_with_separator",
33
+ "tags": [
34
+ {
35
+ "type": "tag",
36
+ "begin": "<|DSML| invoke name=\"get_weather\">\n",
37
+ "content": {
38
+ "type": "json_schema",
39
+ "json_schema": {
40
+ "type": "object",
41
+ "properties": {
42
+ "location": {
43
+ "type": "string"
44
+ }
45
+ },
46
+ "required": [
47
+ "location"
48
+ ]
49
+ },
50
+ "style": "deepseek_v4_1_xml",
51
+ "any_order": false,
52
+ "max_whitespace_cnt": null,
53
+ "excludes": []
54
+ },
55
+ "end": "</|DSML| invoke>\n"
56
+ },
57
+ {
58
+ "type": "tag",
59
+ "begin": "<|DSML| invoke name=\"add\">\n",
60
+ "content": {
61
+ "type": "json_schema",
62
+ "json_schema": {
63
+ "type": "object",
64
+ "properties": {
65
+ "y": {
66
+ "type": "number"
67
+ },
68
+ "x": {
69
+ "type": "number"
70
+ },
71
+ "items": {
72
+ "type": "array",
73
+ "items": {
74
+ "type": "string"
75
+ }
76
+ }
77
+ },
78
+ "required": [
79
+ "y",
80
+ "x",
81
+ "items"
82
+ ]
83
+ },
84
+ "style": "deepseek_v4_1_xml",
85
+ "any_order": false,
86
+ "max_whitespace_cnt": null,
87
+ "excludes": []
88
+ },
89
+ "end": "</|DSML| invoke>\n"
90
+ }
91
+ ],
92
+ "separator": "",
93
+ "at_least_one": true,
94
+ "stop_after_first": false
95
+ },
96
+ "end": "</|DSML| calls>"
97
+ }
98
+ ],
99
+ "at_least_one": false,
100
+ "stop_after_first": false,
101
+ "excludes": [
102
+ "<think>",
103
+ "</think>"
104
+ ]
105
+ }
106
+ ]
107
+ },
108
+ "coverage": "from_token_zero"
109
+ },
110
+ "completion": "Need call the weather and add tools.</think>I will call the tools.\n\n<|DSML| calls>\n<|DSML| invoke name=\"get_weather\">\n<|DSML| parameter name=\"location\" string=\"true\">Shanghai</|DSML| parameter>\n</|DSML| invoke>\n<|DSML| invoke name=\"add\">\n<|DSML| parameter name=\"y\" string=\"false\">1.0</|DSML| parameter>\n<|DSML| parameter name=\"x\" string=\"false\">2</|DSML| parameter>\n<|DSML| parameter name=\"items\" string=\"false\">[\"left\", \"right\"]</|DSML| parameter>\n</|DSML| invoke>\n</|DSML| calls>",
111
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128822, 43, 759, 1926, 270, 6704, 339, 30, 128825, 10699, 1018, 30, 128825, 34756, 2329, 1281, 1133, 65, 50219, 3816, 30, 128825, 10767, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 128825, 10767, 1018, 1718, 128825, 34756, 1018, 30, 128825, 34756, 2329, 1281, 3327, 3816, 30, 128825, 10767, 2329, 1281, 91, 4, 3418, 1281, 19836, 3320, 19, 16, 18, 1718, 128825, 10767, 1018, 30, 128825, 10767, 2329, 1281, 90, 4, 3418, 1281, 19836, 3320, 20, 1718, 128825, 10767, 1018, 30, 128825, 10767, 2329, 1281, 17173, 4, 3418, 1281, 19836, 3320, 10931, 4589, 1760, 582, 2459, 13747, 1718, 128825, 10767, 1018, 1718, 128825, 34756, 1018, 1718, 128825, 10699, 32, 1]
112
+ },
113
+ "required": {
114
+ "grammar": {
115
+ "format": {
116
+ "type": "sequence",
117
+ "elements": [
118
+ {
119
+ "type": "tag",
120
+ "begin": "",
121
+ "content": {
122
+ "type": "any_text",
123
+ "excludes": [],
124
+ "max_tokens": null,
125
+ "max_chars": null
126
+ },
127
+ "end": "</think>"
128
+ },
129
+ {
130
+ "type": "sequence",
131
+ "elements": [
132
+ {
133
+ "type": "const_string",
134
+ "value": "\n\n<|DSML| calls>\n"
135
+ },
136
+ {
137
+ "type": "tags_with_separator",
138
+ "tags": [
139
+ {
140
+ "type": "tag",
141
+ "begin": "<|DSML| invoke name=\"get_weather\">\n",
142
+ "content": {
143
+ "type": "json_schema",
144
+ "json_schema": {
145
+ "type": "object",
146
+ "properties": {
147
+ "location": {
148
+ "type": "string"
149
+ }
150
+ },
151
+ "required": [
152
+ "location"
153
+ ]
154
+ },
155
+ "style": "deepseek_v4_1_xml",
156
+ "any_order": false,
157
+ "max_whitespace_cnt": null,
158
+ "excludes": []
159
+ },
160
+ "end": "</|DSML| invoke>\n"
161
+ },
162
+ {
163
+ "type": "tag",
164
+ "begin": "<|DSML| invoke name=\"add\">\n",
165
+ "content": {
166
+ "type": "json_schema",
167
+ "json_schema": {
168
+ "type": "object",
169
+ "properties": {
170
+ "y": {
171
+ "type": "number"
172
+ },
173
+ "x": {
174
+ "type": "number"
175
+ },
176
+ "items": {
177
+ "type": "array",
178
+ "items": {
179
+ "type": "string"
180
+ }
181
+ }
182
+ },
183
+ "required": [
184
+ "y",
185
+ "x",
186
+ "items"
187
+ ]
188
+ },
189
+ "style": "deepseek_v4_1_xml",
190
+ "any_order": false,
191
+ "max_whitespace_cnt": null,
192
+ "excludes": []
193
+ },
194
+ "end": "</|DSML| invoke>\n"
195
+ }
196
+ ],
197
+ "separator": "",
198
+ "at_least_one": true,
199
+ "stop_after_first": false
200
+ },
201
+ {
202
+ "type": "const_string",
203
+ "value": "</|DSML| calls>"
204
+ }
205
+ ]
206
+ }
207
+ ]
208
+ },
209
+ "coverage": "from_token_zero"
210
+ },
211
+ "completion": "Need call the weather and add tools.</think>\n\n<|DSML| calls>\n<|DSML| invoke name=\"get_weather\">\n<|DSML| parameter name=\"location\" string=\"true\">Shanghai</|DSML| parameter>\n</|DSML| invoke>\n<|DSML| invoke name=\"add\">\n<|DSML| parameter name=\"y\" string=\"false\">1.0</|DSML| parameter>\n<|DSML| parameter name=\"x\" string=\"false\">2</|DSML| parameter>\n<|DSML| parameter name=\"items\" string=\"false\">[\"left\", \"right\"]</|DSML| parameter>\n</|DSML| invoke>\n</|DSML| calls>",
212
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128822, 271, 30, 128825, 10699, 1018, 30, 128825, 34756, 2329, 1281, 1133, 65, 50219, 3816, 30, 128825, 10767, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 128825, 10767, 1018, 1718, 128825, 34756, 1018, 30, 128825, 34756, 2329, 1281, 3327, 3816, 30, 128825, 10767, 2329, 1281, 91, 4, 3418, 1281, 19836, 3320, 19, 16, 18, 1718, 128825, 10767, 1018, 30, 128825, 10767, 2329, 1281, 90, 4, 3418, 1281, 19836, 3320, 20, 1718, 128825, 10767, 1018, 30, 128825, 10767, 2329, 1281, 17173, 4, 3418, 1281, 19836, 3320, 10931, 4589, 1760, 582, 2459, 13747, 1718, 128825, 10767, 1018, 1718, 128825, 34756, 1018, 1718, 128825, 10699, 32, 1]
213
+ },
214
+ "named": {
215
+ "grammar": {
216
+ "format": {
217
+ "type": "sequence",
218
+ "elements": [
219
+ {
220
+ "type": "tag",
221
+ "begin": "",
222
+ "content": {
223
+ "type": "any_text",
224
+ "excludes": [],
225
+ "max_tokens": null,
226
+ "max_chars": null
227
+ },
228
+ "end": "</think>"
229
+ },
230
+ {
231
+ "type": "sequence",
232
+ "elements": [
233
+ {
234
+ "type": "const_string",
235
+ "value": "\n\n<|DSML| calls>\n"
236
+ },
237
+ {
238
+ "type": "tag",
239
+ "begin": "<|DSML| invoke name=\"get_weather\">\n",
240
+ "content": {
241
+ "type": "json_schema",
242
+ "json_schema": {
243
+ "type": "object",
244
+ "properties": {
245
+ "location": {
246
+ "type": "string"
247
+ }
248
+ },
249
+ "required": [
250
+ "location"
251
+ ]
252
+ },
253
+ "style": "deepseek_v4_1_xml",
254
+ "any_order": false,
255
+ "max_whitespace_cnt": null,
256
+ "excludes": []
257
+ },
258
+ "end": "</|DSML| invoke>\n"
259
+ },
260
+ {
261
+ "type": "const_string",
262
+ "value": "</|DSML| calls>"
263
+ }
264
+ ]
265
+ }
266
+ ]
267
+ },
268
+ "coverage": "from_token_zero"
269
+ },
270
+ "completion": "Need call the weather and add tools.</think>\n\n<|DSML| calls>\n<|DSML| invoke name=\"get_weather\">\n<|DSML| parameter name=\"location\" string=\"true\">Shanghai</|DSML| parameter>\n</|DSML| invoke>\n</|DSML| calls>",
271
+ "generation_token_ids": [50249, 1926, 270, 9670, 305, 1258, 6704, 16, 128822, 271, 30, 128825, 10699, 1018, 30, 128825, 34756, 2329, 1281, 1133, 65, 50219, 3816, 30, 128825, 10767, 2329, 1281, 33182, 4, 3418, 1281, 11476, 3320, 119606, 28011, 1718, 128825, 10767, 1018, 1718, 128825, 34756, 1018, 1718, 128825, 10699, 32, 1]
272
+ }
273
+ }
274
+}
rust/src/chat/tests/grammar_replay/GLM-4.7-Flash.json
new
+251
@@ -0,0 +1,251 @@
1
+{
2
+ "model_id": "zai-org/GLM-4.7-Flash",
3
+ "vocab_size": 154880,
4
+ "all_stop_token_ids": [154820, 154827, 154829],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<tool_call>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<tool_call>get_weather",
31
+ "content": {
32
+ "type": "json_schema",
33
+ "json_schema": {
34
+ "type": "object",
35
+ "properties": {
36
+ "location": {
37
+ "type": "string"
38
+ }
39
+ },
40
+ "required": [
41
+ "location"
42
+ ]
43
+ },
44
+ "style": "glm_xml",
45
+ "any_order": false,
46
+ "max_whitespace_cnt": null,
47
+ "excludes": []
48
+ },
49
+ "end": "</tool_call>"
50
+ },
51
+ {
52
+ "type": "tag",
53
+ "begin": "<tool_call>add",
54
+ "content": {
55
+ "type": "json_schema",
56
+ "json_schema": {
57
+ "type": "object",
58
+ "properties": {
59
+ "y": {
60
+ "type": "number"
61
+ },
62
+ "x": {
63
+ "type": "number"
64
+ },
65
+ "items": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ }
70
+ }
71
+ },
72
+ "required": [
73
+ "y",
74
+ "x",
75
+ "items"
76
+ ]
77
+ },
78
+ "style": "glm_xml",
79
+ "any_order": false,
80
+ "max_whitespace_cnt": null,
81
+ "excludes": []
82
+ },
83
+ "end": "</tool_call>"
84
+ }
85
+ ],
86
+ "at_least_one": false,
87
+ "stop_after_first": false,
88
+ "excludes": [
89
+ "<think>",
90
+ "</think>",
91
+ "</tool_call>",
92
+ "<arg_key>",
93
+ "</arg_key>",
94
+ "<arg_value>",
95
+ "</arg_value>"
96
+ ]
97
+ }
98
+ ]
99
+ },
100
+ "coverage": "from_token_zero"
101
+ },
102
+ "completion": "Need call the weather and add tools.</think>I will call the tools.<tool_call>get_weather<arg_key>location</arg_key><arg_value>Shanghai</arg_value></tool_call><tool_call>add<arg_key>y</arg_key><arg_value>1.0</arg_value><arg_key>x</arg_key><arg_value>2</arg_value><arg_key>items</arg_key><arg_value>[\"left\", \"right\"]</arg_value></tool_call>",
103
+ "generation_token_ids": [23591, 1618, 279, 9101, 323, 912, 7375, 13, 154842, 40, 686, 1618, 279, 7375, 13, 154843, 455, 68852, 154847, 2527, 154848, 154849, 2016, 29953, 154850, 154844, 154843, 718, 154847, 88, 154848, 154849, 16, 13, 15, 154850, 154847, 87, 154848, 154849, 17, 154850, 154847, 3615, 154848, 154849, 1183, 2359, 497, 330, 1291, 1341, 154850, 154844, 154820]
104
+ },
105
+ "required": {
106
+ "grammar": {
107
+ "format": {
108
+ "type": "sequence",
109
+ "elements": [
110
+ {
111
+ "type": "tag",
112
+ "begin": "",
113
+ "content": {
114
+ "type": "any_text",
115
+ "excludes": [],
116
+ "max_tokens": null,
117
+ "max_chars": null
118
+ },
119
+ "end": "</think>"
120
+ },
121
+ {
122
+ "type": "triggered_tags",
123
+ "triggers": [
124
+ "<tool_call>"
125
+ ],
126
+ "tags": [
127
+ {
128
+ "type": "tag",
129
+ "begin": "<tool_call>get_weather",
130
+ "content": {
131
+ "type": "json_schema",
132
+ "json_schema": {
133
+ "type": "object",
134
+ "properties": {
135
+ "location": {
136
+ "type": "string"
137
+ }
138
+ },
139
+ "required": [
140
+ "location"
141
+ ]
142
+ },
143
+ "style": "glm_xml",
144
+ "any_order": false,
145
+ "max_whitespace_cnt": null,
146
+ "excludes": []
147
+ },
148
+ "end": "</tool_call>"
149
+ },
150
+ {
151
+ "type": "tag",
152
+ "begin": "<tool_call>add",
153
+ "content": {
154
+ "type": "json_schema",
155
+ "json_schema": {
156
+ "type": "object",
157
+ "properties": {
158
+ "y": {
159
+ "type": "number"
160
+ },
161
+ "x": {
162
+ "type": "number"
163
+ },
164
+ "items": {
165
+ "type": "array",
166
+ "items": {
167
+ "type": "string"
168
+ }
169
+ }
170
+ },
171
+ "required": [
172
+ "y",
173
+ "x",
174
+ "items"
175
+ ]
176
+ },
177
+ "style": "glm_xml",
178
+ "any_order": false,
179
+ "max_whitespace_cnt": null,
180
+ "excludes": []
181
+ },
182
+ "end": "</tool_call>"
183
+ }
184
+ ],
185
+ "at_least_one": true,
186
+ "stop_after_first": false,
187
+ "excludes": [
188
+ "<think>",
189
+ "</think>",
190
+ "</tool_call>",
191
+ "<arg_key>",
192
+ "</arg_key>",
193
+ "<arg_value>",
194
+ "</arg_value>"
195
+ ]
196
+ }
197
+ ]
198
+ },
199
+ "coverage": "from_token_zero"
200
+ },
201
+ "completion": "Need call the weather and add tools.</think><tool_call>get_weather<arg_key>location</arg_key><arg_value>Shanghai</arg_value></tool_call><tool_call>add<arg_key>y</arg_key><arg_value>1.0</arg_value><arg_key>x</arg_key><arg_value>2</arg_value><arg_key>items</arg_key><arg_value>[\"left\", \"right\"]</arg_value></tool_call>",
202
+ "generation_token_ids": [23591, 1618, 279, 9101, 323, 912, 7375, 13, 154842, 154843, 455, 68852, 154847, 2527, 154848, 154849, 2016, 29953, 154850, 154844, 154843, 718, 154847, 88, 154848, 154849, 16, 13, 15, 154850, 154847, 87, 154848, 154849, 17, 154850, 154847, 3615, 154848, 154849, 1183, 2359, 497, 330, 1291, 1341, 154850, 154844, 154820]
203
+ },
204
+ "named": {
205
+ "grammar": {
206
+ "format": {
207
+ "type": "sequence",
208
+ "elements": [
209
+ {
210
+ "type": "tag",
211
+ "begin": "",
212
+ "content": {
213
+ "type": "any_text",
214
+ "excludes": [],
215
+ "max_tokens": null,
216
+ "max_chars": null
217
+ },
218
+ "end": "</think>"
219
+ },
220
+ {
221
+ "type": "tag",
222
+ "begin": "<tool_call>get_weather",
223
+ "content": {
224
+ "type": "json_schema",
225
+ "json_schema": {
226
+ "type": "object",
227
+ "properties": {
228
+ "location": {
229
+ "type": "string"
230
+ }
231
+ },
232
+ "required": [
233
+ "location"
234
+ ]
235
+ },
236
+ "style": "glm_xml",
237
+ "any_order": false,
238
+ "max_whitespace_cnt": null,
239
+ "excludes": []
240
+ },
241
+ "end": "</tool_call>"
242
+ }
243
+ ]
244
+ },
245
+ "coverage": "from_token_zero"
246
+ },
247
+ "completion": "Need call the weather and add tools.</think><tool_call>get_weather<arg_key>location</arg_key><arg_value>Shanghai</arg_value></tool_call>",
248
+ "generation_token_ids": [23591, 1618, 279, 9101, 323, 912, 7375, 13, 154842, 154843, 455, 68852, 154847, 2527, 154848, 154849, 2016, 29953, 154850, 154844, 154820]
249
+ }
250
+ }
251
+}
rust/src/chat/tests/grammar_replay/GLM-5.2-FP8.json
new
+251
@@ -0,0 +1,251 @@
1
+{
2
+ "model_id": "zai-org/GLM-5.2-FP8",
3
+ "vocab_size": 154880,
4
+ "all_stop_token_ids": [154820, 154827, 154829],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<tool_call>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<tool_call>get_weather",
31
+ "content": {
32
+ "type": "json_schema",
33
+ "json_schema": {
34
+ "type": "object",
35
+ "properties": {
36
+ "location": {
37
+ "type": "string"
38
+ }
39
+ },
40
+ "required": [
41
+ "location"
42
+ ]
43
+ },
44
+ "style": "glm_xml",
45
+ "any_order": false,
46
+ "max_whitespace_cnt": null,
47
+ "excludes": []
48
+ },
49
+ "end": "</tool_call>"
50
+ },
51
+ {
52
+ "type": "tag",
53
+ "begin": "<tool_call>add",
54
+ "content": {
55
+ "type": "json_schema",
56
+ "json_schema": {
57
+ "type": "object",
58
+ "properties": {
59
+ "y": {
60
+ "type": "number"
61
+ },
62
+ "x": {
63
+ "type": "number"
64
+ },
65
+ "items": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ }
70
+ }
71
+ },
72
+ "required": [
73
+ "y",
74
+ "x",
75
+ "items"
76
+ ]
77
+ },
78
+ "style": "glm_xml",
79
+ "any_order": false,
80
+ "max_whitespace_cnt": null,
81
+ "excludes": []
82
+ },
83
+ "end": "</tool_call>"
84
+ }
85
+ ],
86
+ "at_least_one": false,
87
+ "stop_after_first": false,
88
+ "excludes": [
89
+ "<think>",
90
+ "</think>",
91
+ "</tool_call>",
92
+ "<arg_key>",
93
+ "</arg_key>",
94
+ "<arg_value>",
95
+ "</arg_value>"
96
+ ]
97
+ }
98
+ ]
99
+ },
100
+ "coverage": "from_token_zero"
101
+ },
102
+ "completion": "Need call the weather and add tools.</think>I will call the tools.<tool_call>get_weather<arg_key>location</arg_key><arg_value>Shanghai</arg_value></tool_call><tool_call>add<arg_key>y</arg_key><arg_value>1.0</arg_value><arg_key>x</arg_key><arg_value>2</arg_value><arg_key>items</arg_key><arg_value>[\"left\", \"right\"]</arg_value></tool_call>",
103
+ "generation_token_ids": [23591, 1618, 279, 9101, 323, 912, 7375, 13, 154842, 40, 686, 1618, 279, 7375, 13, 154843, 455, 68852, 154847, 2527, 154848, 154849, 2016, 29953, 154850, 154844, 154843, 718, 154847, 88, 154848, 154849, 16, 13, 15, 154850, 154847, 87, 154848, 154849, 17, 154850, 154847, 3615, 154848, 154849, 1183, 2359, 497, 330, 1291, 1341, 154850, 154844, 154820]
104
+ },
105
+ "required": {
106
+ "grammar": {
107
+ "format": {
108
+ "type": "sequence",
109
+ "elements": [
110
+ {
111
+ "type": "tag",
112
+ "begin": "",
113
+ "content": {
114
+ "type": "any_text",
115
+ "excludes": [],
116
+ "max_tokens": null,
117
+ "max_chars": null
118
+ },
119
+ "end": "</think>"
120
+ },
121
+ {
122
+ "type": "triggered_tags",
123
+ "triggers": [
124
+ "<tool_call>"
125
+ ],
126
+ "tags": [
127
+ {
128
+ "type": "tag",
129
+ "begin": "<tool_call>get_weather",
130
+ "content": {
131
+ "type": "json_schema",
132
+ "json_schema": {
133
+ "type": "object",
134
+ "properties": {
135
+ "location": {
136
+ "type": "string"
137
+ }
138
+ },
139
+ "required": [
140
+ "location"
141
+ ]
142
+ },
143
+ "style": "glm_xml",
144
+ "any_order": false,
145
+ "max_whitespace_cnt": null,
146
+ "excludes": []
147
+ },
148
+ "end": "</tool_call>"
149
+ },
150
+ {
151
+ "type": "tag",
152
+ "begin": "<tool_call>add",
153
+ "content": {
154
+ "type": "json_schema",
155
+ "json_schema": {
156
+ "type": "object",
157
+ "properties": {
158
+ "y": {
159
+ "type": "number"
160
+ },
161
+ "x": {
162
+ "type": "number"
163
+ },
164
+ "items": {
165
+ "type": "array",
166
+ "items": {
167
+ "type": "string"
168
+ }
169
+ }
170
+ },
171
+ "required": [
172
+ "y",
173
+ "x",
174
+ "items"
175
+ ]
176
+ },
177
+ "style": "glm_xml",
178
+ "any_order": false,
179
+ "max_whitespace_cnt": null,
180
+ "excludes": []
181
+ },
182
+ "end": "</tool_call>"
183
+ }
184
+ ],
185
+ "at_least_one": true,
186
+ "stop_after_first": false,
187
+ "excludes": [
188
+ "<think>",
189
+ "</think>",
190
+ "</tool_call>",
191
+ "<arg_key>",
192
+ "</arg_key>",
193
+ "<arg_value>",
194
+ "</arg_value>"
195
+ ]
196
+ }
197
+ ]
198
+ },
199
+ "coverage": "from_token_zero"
200
+ },
201
+ "completion": "Need call the weather and add tools.</think><tool_call>get_weather<arg_key>location</arg_key><arg_value>Shanghai</arg_value></tool_call><tool_call>add<arg_key>y</arg_key><arg_value>1.0</arg_value><arg_key>x</arg_key><arg_value>2</arg_value><arg_key>items</arg_key><arg_value>[\"left\", \"right\"]</arg_value></tool_call>",
202
+ "generation_token_ids": [23591, 1618, 279, 9101, 323, 912, 7375, 13, 154842, 154843, 455, 68852, 154847, 2527, 154848, 154849, 2016, 29953, 154850, 154844, 154843, 718, 154847, 88, 154848, 154849, 16, 13, 15, 154850, 154847, 87, 154848, 154849, 17, 154850, 154847, 3615, 154848, 154849, 1183, 2359, 497, 330, 1291, 1341, 154850, 154844, 154820]
203
+ },
204
+ "named": {
205
+ "grammar": {
206
+ "format": {
207
+ "type": "sequence",
208
+ "elements": [
209
+ {
210
+ "type": "tag",
211
+ "begin": "",
212
+ "content": {
213
+ "type": "any_text",
214
+ "excludes": [],
215
+ "max_tokens": null,
216
+ "max_chars": null
217
+ },
218
+ "end": "</think>"
219
+ },
220
+ {
221
+ "type": "tag",
222
+ "begin": "<tool_call>get_weather",
223
+ "content": {
224
+ "type": "json_schema",
225
+ "json_schema": {
226
+ "type": "object",
227
+ "properties": {
228
+ "location": {
229
+ "type": "string"
230
+ }
231
+ },
232
+ "required": [
233
+ "location"
234
+ ]
235
+ },
236
+ "style": "glm_xml",
237
+ "any_order": false,
238
+ "max_whitespace_cnt": null,
239
+ "excludes": []
240
+ },
241
+ "end": "</tool_call>"
242
+ }
243
+ ]
244
+ },
245
+ "coverage": "from_token_zero"
246
+ },
247
+ "completion": "Need call the weather and add tools.</think><tool_call>get_weather<arg_key>location</arg_key><arg_value>Shanghai</arg_value></tool_call>",
248
+ "generation_token_ids": [23591, 1618, 279, 9101, 323, 912, 7375, 13, 154842, 154843, 455, 68852, 154847, 2527, 154848, 154849, 2016, 29953, 154850, 154844, 154820]
249
+ }
250
+ }
251
+}
rust/src/chat/tests/grammar_replay/Hy3.json
new
+625
@@ -0,0 +1,625 @@
1
+{
2
+ "model_id": "tencent/Hy3",
3
+ "vocab_size": 120832,
4
+ "all_stop_token_ids": [120025],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think:opensource>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<tool_calls:opensource>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<tool_calls:opensource>\n",
31
+ "content": {
32
+ "type": "tags_with_separator",
33
+ "tags": [
34
+ {
35
+ "type": "tag",
36
+ "begin": "<tool_call:opensource>get_weather<tool_sep:opensource>\n",
37
+ "content": {
38
+ "type": "sequence",
39
+ "elements": [
40
+ {
41
+ "type": "sequence",
42
+ "elements": [
43
+ {
44
+ "type": "const_string",
45
+ "value": "<arg_key:opensource>"
46
+ },
47
+ {
48
+ "type": "const_string",
49
+ "value": "location"
50
+ },
51
+ {
52
+ "type": "const_string",
53
+ "value": "</arg_key:opensource>"
54
+ },
55
+ {
56
+ "type": "const_string",
57
+ "value": "\n"
58
+ },
59
+ {
60
+ "type": "const_string",
61
+ "value": "<arg_value:opensource>"
62
+ },
63
+ {
64
+ "type": "any_text",
65
+ "excludes": [
66
+ "<tool_calls:opensource>",
67
+ "</tool_calls:opensource>",
68
+ "<tool_call:opensource>",
69
+ "</tool_call:opensource>",
70
+ "<tool_sep:opensource>",
71
+ "<arg_key:opensource>",
72
+ "</arg_key:opensource>",
73
+ "<arg_value:opensource>",
74
+ "</arg_value:opensource>"
75
+ ],
76
+ "max_tokens": null,
77
+ "max_chars": null
78
+ },
79
+ {
80
+ "type": "const_string",
81
+ "value": "</arg_value:opensource>"
82
+ },
83
+ {
84
+ "type": "const_string",
85
+ "value": "\n"
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+ },
91
+ "end": "</tool_call:opensource>"
92
+ },
93
+ {
94
+ "type": "tag",
95
+ "begin": "<tool_call:opensource>add<tool_sep:opensource>\n",
96
+ "content": {
97
+ "type": "sequence",
98
+ "elements": [
99
+ {
100
+ "type": "sequence",
101
+ "elements": [
102
+ {
103
+ "type": "const_string",
104
+ "value": "<arg_key:opensource>"
105
+ },
106
+ {
107
+ "type": "const_string",
108
+ "value": "y"
109
+ },
110
+ {
111
+ "type": "const_string",
112
+ "value": "</arg_key:opensource>"
113
+ },
114
+ {
115
+ "type": "const_string",
116
+ "value": "\n"
117
+ },
118
+ {
119
+ "type": "const_string",
120
+ "value": "<arg_value:opensource>"
121
+ },
122
+ {
123
+ "type": "any_text",
124
+ "excludes": [
125
+ "<tool_calls:opensource>",
126
+ "</tool_calls:opensource>",
127
+ "<tool_call:opensource>",
128
+ "</tool_call:opensource>",
129
+ "<tool_sep:opensource>",
130
+ "<arg_key:opensource>",
131
+ "</arg_key:opensource>",
132
+ "<arg_value:opensource>",
133
+ "</arg_value:opensource>"
134
+ ],
135
+ "max_tokens": null,
136
+ "max_chars": null
137
+ },
138
+ {
139
+ "type": "const_string",
140
+ "value": "</arg_value:opensource>"
141
+ },
142
+ {
143
+ "type": "const_string",
144
+ "value": "\n"
145
+ }
146
+ ]
147
+ },
148
+ {
149
+ "type": "sequence",
150
+ "elements": [
151
+ {
152
+ "type": "const_string",
153
+ "value": "<arg_key:opensource>"
154
+ },
155
+ {
156
+ "type": "const_string",
157
+ "value": "x"
158
+ },
159
+ {
160
+ "type": "const_string",
161
+ "value": "</arg_key:opensource>"
162
+ },
163
+ {
164
+ "type": "const_string",
165
+ "value": "\n"
166
+ },
167
+ {
168
+ "type": "const_string",
169
+ "value": "<arg_value:opensource>"
170
+ },
171
+ {
172
+ "type": "any_text",
173
+ "excludes": [
174
+ "<tool_calls:opensource>",
175
+ "</tool_calls:opensource>",
176
+ "<tool_call:opensource>",
177
+ "</tool_call:opensource>",
178
+ "<tool_sep:opensource>",
179
+ "<arg_key:opensource>",
180
+ "</arg_key:opensource>",
181
+ "<arg_value:opensource>",
182
+ "</arg_value:opensource>"
183
+ ],
184
+ "max_tokens": null,
185
+ "max_chars": null
186
+ },
187
+ {
188
+ "type": "const_string",
189
+ "value": "</arg_value:opensource>"
190
+ },
191
+ {
192
+ "type": "const_string",
193
+ "value": "\n"
194
+ }
195
+ ]
196
+ },
197
+ {
198
+ "type": "sequence",
199
+ "elements": [
200
+ {
201
+ "type": "const_string",
202
+ "value": "<arg_key:opensource>"
203
+ },
204
+ {
205
+ "type": "const_string",
206
+ "value": "items"
207
+ },
208
+ {
209
+ "type": "const_string",
210
+ "value": "</arg_key:opensource>"
211
+ },
212
+ {
213
+ "type": "const_string",
214
+ "value": "\n"
215
+ },
216
+ {
217
+ "type": "const_string",
218
+ "value": "<arg_value:opensource>"
219
+ },
220
+ {
221
+ "type": "any_text",
222
+ "excludes": [
223
+ "<tool_calls:opensource>",
224
+ "</tool_calls:opensource>",
225
+ "<tool_call:opensource>",
226
+ "</tool_call:opensource>",
227
+ "<tool_sep:opensource>",
228
+ "<arg_key:opensource>",
229
+ "</arg_key:opensource>",
230
+ "<arg_value:opensource>",
231
+ "</arg_value:opensource>"
232
+ ],
233
+ "max_tokens": null,
234
+ "max_chars": null
235
+ },
236
+ {
237
+ "type": "const_string",
238
+ "value": "</arg_value:opensource>"
239
+ },
240
+ {
241
+ "type": "const_string",
242
+ "value": "\n"
243
+ }
244
+ ]
245
+ }
246
+ ]
247
+ },
248
+ "end": "</tool_call:opensource>"
249
+ }
250
+ ],
251
+ "separator": "\n",
252
+ "at_least_one": true,
253
+ "stop_after_first": false
254
+ },
255
+ "end": "\n</tool_calls:opensource>"
256
+ }
257
+ ],
258
+ "at_least_one": false,
259
+ "stop_after_first": false,
260
+ "excludes": []
261
+ }
262
+ ]
263
+ },
264
+ "coverage": "from_token_zero"
265
+ },
266
+ "completion": "Need call the weather and add tools.</think:opensource>I will call the tools.<tool_calls:opensource>\n<tool_call:opensource>get_weather<tool_sep:opensource>\n<arg_key:opensource>location</arg_key:opensource>\n<arg_value:opensource>Shanghai</arg_value:opensource>\n</tool_call:opensource>\n<tool_call:opensource>add<tool_sep:opensource>\n<arg_key:opensource>y</arg_key:opensource>\n<arg_value:opensource>1.0</arg_value:opensource>\n<arg_key:opensource>x</arg_key:opensource>\n<arg_value:opensource>2</arg_value:opensource>\n<arg_key:opensource>items</arg_key:opensource>\n<arg_value:opensource>[\"left\", \"right\"]</arg_value:opensource>\n</tool_call:opensource>\n</tool_calls:opensource>",
267
+ "generation_token_ids": [47893, 1609, 252, 9969, 289, 1189, 7106, 13, 120030, 40, 823, 1609, 252, 7106, 13, 120035, 185, 120033, 874, 62, 39899, 120043, 185, 120066, 24806, 120067, 185, 120068, 4169, 768, 32381, 120069, 185, 120034, 185, 120033, 2176, 120043, 185, 120066, 88, 120067, 185, 120068, 16, 13, 15, 120069, 185, 120066, 87, 120067, 185, 120068, 17, 120069, 185, 120066, 12792, 120067, 185, 120068, 5831, 1639, 1221, 478, 1331, 7029, 120069, 185, 120034, 185, 120036, 120025]
268
+ },
269
+ "required": {
270
+ "grammar": {
271
+ "format": {
272
+ "type": "sequence",
273
+ "elements": [
274
+ {
275
+ "type": "tag",
276
+ "begin": "",
277
+ "content": {
278
+ "type": "any_text",
279
+ "excludes": [],
280
+ "max_tokens": null,
281
+ "max_chars": null
282
+ },
283
+ "end": "</think:opensource>"
284
+ },
285
+ {
286
+ "type": "sequence",
287
+ "elements": [
288
+ {
289
+ "type": "const_string",
290
+ "value": "<tool_calls:opensource>\n"
291
+ },
292
+ {
293
+ "type": "tags_with_separator",
294
+ "tags": [
295
+ {
296
+ "type": "tag",
297
+ "begin": "<tool_call:opensource>get_weather<tool_sep:opensource>\n",
298
+ "content": {
299
+ "type": "sequence",
300
+ "elements": [
301
+ {
302
+ "type": "sequence",
303
+ "elements": [
304
+ {
305
+ "type": "const_string",
306
+ "value": "<arg_key:opensource>"
307
+ },
308
+ {
309
+ "type": "const_string",
310
+ "value": "location"
311
+ },
312
+ {
313
+ "type": "const_string",
314
+ "value": "</arg_key:opensource>"
315
+ },
316
+ {
317
+ "type": "const_string",
318
+ "value": "\n"
319
+ },
320
+ {
321
+ "type": "const_string",
322
+ "value": "<arg_value:opensource>"
323
+ },
324
+ {
325
+ "type": "any_text",
326
+ "excludes": [
327
+ "<tool_calls:opensource>",
328
+ "</tool_calls:opensource>",
329
+ "<tool_call:opensource>",
330
+ "</tool_call:opensource>",
331
+ "<tool_sep:opensource>",
332
+ "<arg_key:opensource>",
333
+ "</arg_key:opensource>",
334
+ "<arg_value:opensource>",
335
+ "</arg_value:opensource>"
336
+ ],
337
+ "max_tokens": null,
338
+ "max_chars": null
339
+ },
340
+ {
341
+ "type": "const_string",
342
+ "value": "</arg_value:opensource>"
343
+ },
344
+ {
345
+ "type": "const_string",
346
+ "value": "\n"
347
+ }
348
+ ]
349
+ }
350
+ ]
351
+ },
352
+ "end": "</tool_call:opensource>"
353
+ },
354
+ {
355
+ "type": "tag",
356
+ "begin": "<tool_call:opensource>add<tool_sep:opensource>\n",
357
+ "content": {
358
+ "type": "sequence",
359
+ "elements": [
360
+ {
361
+ "type": "sequence",
362
+ "elements": [
363
+ {
364
+ "type": "const_string",
365
+ "value": "<arg_key:opensource>"
366
+ },
367
+ {
368
+ "type": "const_string",
369
+ "value": "y"
370
+ },
371
+ {
372
+ "type": "const_string",
373
+ "value": "</arg_key:opensource>"
374
+ },
375
+ {
376
+ "type": "const_string",
377
+ "value": "\n"
378
+ },
379
+ {
380
+ "type": "const_string",
381
+ "value": "<arg_value:opensource>"
382
+ },
383
+ {
384
+ "type": "any_text",
385
+ "excludes": [
386
+ "<tool_calls:opensource>",
387
+ "</tool_calls:opensource>",
388
+ "<tool_call:opensource>",
389
+ "</tool_call:opensource>",
390
+ "<tool_sep:opensource>",
391
+ "<arg_key:opensource>",
392
+ "</arg_key:opensource>",
393
+ "<arg_value:opensource>",
394
+ "</arg_value:opensource>"
395
+ ],
396
+ "max_tokens": null,
397
+ "max_chars": null
398
+ },
399
+ {
400
+ "type": "const_string",
401
+ "value": "</arg_value:opensource>"
402
+ },
403
+ {
404
+ "type": "const_string",
405
+ "value": "\n"
406
+ }
407
+ ]
408
+ },
409
+ {
410
+ "type": "sequence",
411
+ "elements": [
412
+ {
413
+ "type": "const_string",
414
+ "value": "<arg_key:opensource>"
415
+ },
416
+ {
417
+ "type": "const_string",
418
+ "value": "x"
419
+ },
420
+ {
421
+ "type": "const_string",
422
+ "value": "</arg_key:opensource>"
423
+ },
424
+ {
425
+ "type": "const_string",
426
+ "value": "\n"
427
+ },
428
+ {
429
+ "type": "const_string",
430
+ "value": "<arg_value:opensource>"
431
+ },
432
+ {
433
+ "type": "any_text",
434
+ "excludes": [
435
+ "<tool_calls:opensource>",
436
+ "</tool_calls:opensource>",
437
+ "<tool_call:opensource>",
438
+ "</tool_call:opensource>",
439
+ "<tool_sep:opensource>",
440
+ "<arg_key:opensource>",
441
+ "</arg_key:opensource>",
442
+ "<arg_value:opensource>",
443
+ "</arg_value:opensource>"
444
+ ],
445
+ "max_tokens": null,
446
+ "max_chars": null
447
+ },
448
+ {
449
+ "type": "const_string",
450
+ "value": "</arg_value:opensource>"
451
+ },
452
+ {
453
+ "type": "const_string",
454
+ "value": "\n"
455
+ }
456
+ ]
457
+ },
458
+ {
459
+ "type": "sequence",
460
+ "elements": [
461
+ {
462
+ "type": "const_string",
463
+ "value": "<arg_key:opensource>"
464
+ },
465
+ {
466
+ "type": "const_string",
467
+ "value": "items"
468
+ },
469
+ {
470
+ "type": "const_string",
471
+ "value": "</arg_key:opensource>"
472
+ },
473
+ {
474
+ "type": "const_string",
475
+ "value": "\n"
476
+ },
477
+ {
478
+ "type": "const_string",
479
+ "value": "<arg_value:opensource>"
480
+ },
481
+ {
482
+ "type": "any_text",
483
+ "excludes": [
484
+ "<tool_calls:opensource>",
485
+ "</tool_calls:opensource>",
486
+ "<tool_call:opensource>",
487
+ "</tool_call:opensource>",
488
+ "<tool_sep:opensource>",
489
+ "<arg_key:opensource>",
490
+ "</arg_key:opensource>",
491
+ "<arg_value:opensource>",
492
+ "</arg_value:opensource>"
493
+ ],
494
+ "max_tokens": null,
495
+ "max_chars": null
496
+ },
497
+ {
498
+ "type": "const_string",
499
+ "value": "</arg_value:opensource>"
500
+ },
501
+ {
502
+ "type": "const_string",
503
+ "value": "\n"
504
+ }
505
+ ]
506
+ }
507
+ ]
508
+ },
509
+ "end": "</tool_call:opensource>"
510
+ }
511
+ ],
512
+ "separator": "\n",
513
+ "at_least_one": true,
514
+ "stop_after_first": false
515
+ },
516
+ {
517
+ "type": "const_string",
518
+ "value": "\n</tool_calls:opensource>"
519
+ }
520
+ ]
521
+ }
522
+ ]
523
+ },
524
+ "coverage": "from_token_zero"
525
+ },
526
+ "completion": "Need call the weather and add tools.</think:opensource><tool_calls:opensource>\n<tool_call:opensource>get_weather<tool_sep:opensource>\n<arg_key:opensource>location</arg_key:opensource>\n<arg_value:opensource>Shanghai</arg_value:opensource>\n</tool_call:opensource>\n<tool_call:opensource>add<tool_sep:opensource>\n<arg_key:opensource>y</arg_key:opensource>\n<arg_value:opensource>1.0</arg_value:opensource>\n<arg_key:opensource>x</arg_key:opensource>\n<arg_value:opensource>2</arg_value:opensource>\n<arg_key:opensource>items</arg_key:opensource>\n<arg_value:opensource>[\"left\", \"right\"]</arg_value:opensource>\n</tool_call:opensource>\n</tool_calls:opensource>",
527
+ "generation_token_ids": [47893, 1609, 252, 9969, 289, 1189, 7106, 13, 120030, 120035, 185, 120033, 874, 62, 39899, 120043, 185, 120066, 24806, 120067, 185, 120068, 4169, 768, 32381, 120069, 185, 120034, 185, 120033, 2176, 120043, 185, 120066, 88, 120067, 185, 120068, 16, 13, 15, 120069, 185, 120066, 87, 120067, 185, 120068, 17, 120069, 185, 120066, 12792, 120067, 185, 120068, 5831, 1639, 1221, 478, 1331, 7029, 120069, 185, 120034, 185, 120036, 120025]
528
+ },
529
+ "named": {
530
+ "grammar": {
531
+ "format": {
532
+ "type": "sequence",
533
+ "elements": [
534
+ {
535
+ "type": "tag",
536
+ "begin": "",
537
+ "content": {
538
+ "type": "any_text",
539
+ "excludes": [],
540
+ "max_tokens": null,
541
+ "max_chars": null
542
+ },
543
+ "end": "</think:opensource>"
544
+ },
545
+ {
546
+ "type": "sequence",
547
+ "elements": [
548
+ {
549
+ "type": "const_string",
550
+ "value": "<tool_calls:opensource>\n"
551
+ },
552
+ {
553
+ "type": "tag",
554
+ "begin": "<tool_call:opensource>get_weather<tool_sep:opensource>\n",
555
+ "content": {
556
+ "type": "sequence",
557
+ "elements": [
558
+ {
559
+ "type": "sequence",
560
+ "elements": [
561
+ {
562
+ "type": "const_string",
563
+ "value": "<arg_key:opensource>"
564
+ },
565
+ {
566
+ "type": "const_string",
567
+ "value": "location"
568
+ },
569
+ {
570
+ "type": "const_string",
571
+ "value": "</arg_key:opensource>"
572
+ },
573
+ {
574
+ "type": "const_string",
575
+ "value": "\n"
576
+ },
577
+ {
578
+ "type": "const_string",
579
+ "value": "<arg_value:opensource>"
580
+ },
581
+ {
582
+ "type": "any_text",
583
+ "excludes": [
584
+ "<tool_calls:opensource>",
585
+ "</tool_calls:opensource>",
586
+ "<tool_call:opensource>",
587
+ "</tool_call:opensource>",
588
+ "<tool_sep:opensource>",
589
+ "<arg_key:opensource>",
590
+ "</arg_key:opensource>",
591
+ "<arg_value:opensource>",
592
+ "</arg_value:opensource>"
593
+ ],
594
+ "max_tokens": null,
595
+ "max_chars": null
596
+ },
597
+ {
598
+ "type": "const_string",
599
+ "value": "</arg_value:opensource>"
600
+ },
601
+ {
602
+ "type": "const_string",
603
+ "value": "\n"
604
+ }
605
+ ]
606
+ }
607
+ ]
608
+ },
609
+ "end": "</tool_call:opensource>"
610
+ },
611
+ {
612
+ "type": "const_string",
613
+ "value": "\n</tool_calls:opensource>"
614
+ }
615
+ ]
616
+ }
617
+ ]
618
+ },
619
+ "coverage": "from_token_zero"
620
+ },
621
+ "completion": "Need call the weather and add tools.</think:opensource><tool_calls:opensource>\n<tool_call:opensource>get_weather<tool_sep:opensource>\n<arg_key:opensource>location</arg_key:opensource>\n<arg_value:opensource>Shanghai</arg_value:opensource>\n</tool_call:opensource>\n</tool_calls:opensource>",
622
+ "generation_token_ids": [47893, 1609, 252, 9969, 289, 1189, 7106, 13, 120030, 120035, 185, 120033, 874, 62, 39899, 120043, 185, 120066, 24806, 120067, 185, 120068, 4169, 768, 32381, 120069, 185, 120034, 185, 120036, 120025]
623
+ }
624
+ }
625
+}
rust/src/chat/tests/grammar_replay/Kimi-K2.5.json
new
+340
@@ -0,0 +1,340 @@
1
+{
2
+ "model_id": "moonshotai/Kimi-K2.5",
3
+ "vocab_size": 163840,
4
+ "all_stop_token_ids": [163585, 163586],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "tag",
13
+ "begin": "",
14
+ "content": {
15
+ "type": "any_text",
16
+ "excludes": [],
17
+ "max_tokens": null,
18
+ "max_chars": null
19
+ },
20
+ "end": "</think>"
21
+ },
22
+ {
23
+ "type": "triggered_tags",
24
+ "triggers": [
25
+ "<|tool_calls_section_begin|>"
26
+ ],
27
+ "tags": [
28
+ {
29
+ "type": "tag",
30
+ "begin": "<|tool_calls_section_begin|>",
31
+ "content": {
32
+ "type": "tags_with_separator",
33
+ "tags": [
34
+ {
35
+ "type": "tag",
36
+ "begin": "<|tool_call_begin|>functions.get_weather:",
37
+ "content": {
38
+ "type": "sequence",
39
+ "elements": [
40
+ {
41
+ "type": "regex",
42
+ "pattern": "\\d+"
43
+ },
44
+ {
45
+ "type": "const_string",
46
+ "value": "<|tool_call_argument_begin|>"
47
+ },
48
+ {
49
+ "type": "json_schema",
50
+ "json_schema": {
51
+ "type": "object",
52
+ "properties": {
53
+ "location": {
54
+ "type": "string"
55
+ }
56
+ },
57
+ "required": [
58
+ "location"
59
+ ]
60
+ },
61
+ "style": "json",
62
+ "any_order": false,
63
+ "max_whitespace_cnt": null,
64
+ "excludes": []
65
+ }
66
+ ]
67
+ },
68
+ "end": "<|tool_call_end|>"
69
+ },
70
+ {
71
+ "type": "tag",
72
+ "begin": "<|tool_call_begin|>functions.add:",
73
+ "content": {
74
+ "type": "sequence",
75
+ "elements": [
76
+ {
77
+ "type": "regex",
78
+ "pattern": "\\d+"
79
+ },
80
+ {
81
+ "type": "const_string",
82
+ "value": "<|tool_call_argument_begin|>"
83
+ },
84
+ {
85
+ "type": "json_schema",
86
+ "json_schema": {
87
+ "type": "object",
88
+ "properties": {
89
+ "y": {
90
+ "type": "number"
91
+ },
92
+ "x": {
93
+ "type": "number"
94
+ },
95
+ "items": {
96
+ "type": "array",
97
+ "items": {
98
+ "type": "string"
99
+ }
100
+ }
101
+ },
102
+ "required": [
103
+ "y",
104
+ "x",
105
+ "items"
106
+ ]
107
+ },
108
+ "style": "json",
109
+ "any_order": false,
110
+ "max_whitespace_cnt": null,
111
+ "excludes": []
112
+ }
113
+ ]
114
+ },
115
+ "end": "<|tool_call_end|>"
116
+ }
117
+ ],
118
+ "separator": "",
119
+ "at_least_one": true,
120
+ "stop_after_first": false
121
+ },
122
+ "end": "<|tool_calls_section_end|>"
123
+ }
124
+ ],
125
+ "at_least_one": false,
126
+ "stop_after_first": false,
127
+ "excludes": [
128
+ "<think>",
129
+ "</think>",
130
+ "<|tool_call_begin|>"
131
+ ]
132
+ }
133
+ ]
134
+ },
135
+ "coverage": "from_token_zero"
136
+ },
137
+ "completion": "Need call the weather and add tools.</think>I will call the tools.<|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{\"location\": \"Shanghai\"}<|tool_call_end|><|tool_call_begin|>functions.add:1<|tool_call_argument_begin|>{\"y\": 1.0, \"x\": 2, \"items\": [\"left\", \"right\"]}<|tool_call_end|><|tool_calls_section_end|>",
138
+ "generation_token_ids": [20082, 2390, 276, 10666, 316, 1239, 7697, 13, 163607, 40, 748, 2390, 276, 7697, 13, 163595, 163597, 41937, 1150, 21055, 2800, 25, 15, 163598, 8264, 5791, 1289, 414, 2797, 31231, 16934, 163599, 163597, 41937, 3149, 25, 16, 163598, 8264, 88, 1289, 220, 16, 13, 15, 11, 414, 87, 1289, 220, 17, 11, 414, 7989, 1289, 8681, 3794, 665, 414, 2277, 3956, 92, 163599, 163596, 163586]
139
+ },
140
+ "required": {
141
+ "grammar": {
142
+ "format": {
143
+ "type": "sequence",
144
+ "elements": [
145
+ {
146
+ "type": "tag",
147
+ "begin": "",
148
+ "content": {
149
+ "type": "any_text",
150
+ "excludes": [],
151
+ "max_tokens": null,
152
+ "max_chars": null
153
+ },
154
+ "end": "</think>"
155
+ },
156
+ {
157
+ "type": "sequence",
158
+ "elements": [
159
+ {
160
+ "type": "const_string",
161
+ "value": "<|tool_calls_section_begin|>"
162
+ },
163
+ {
164
+ "type": "tags_with_separator",
165
+ "tags": [
166
+ {
167
+ "type": "tag",
168
+ "begin": "<|tool_call_begin|>functions.get_weather:",
169
+ "content": {
170
+ "type": "sequence",
171
+ "elements": [
172
+ {
173
+ "type": "regex",
174
+ "pattern": "\\d+"
175
+ },
176
+ {
177
+ "type": "const_string",
178
+ "value": "<|tool_call_argument_begin|>"
179
+ },
180
+ {
181
+ "type": "json_schema",
182
+ "json_schema": {
183
+ "type": "object",
184
+ "properties": {
185
+ "location": {
186
+ "type": "string"
187
+ }
188
+ },
189
+ "required": [
190
+ "location"
191
+ ]
192
+ },
193
+ "style": "json",
194
+ "any_order": false,
195
+ "max_whitespace_cnt": null,
196
+ "excludes": []
197
+ }
198
+ ]
199
+ },
200
+ "end": "<|tool_call_end|>"
201
+ },
202
+ {
203
+ "type": "tag",
204
+ "begin": "<|tool_call_begin|>functions.add:",
205
+ "content": {
206
+ "type": "sequence",
207
+ "elements": [
208
+ {
209
+ "type": "regex",
210
+ "pattern": "\\d+"
211
+ },
212
+ {
213
+ "type": "const_string",
214
+ "value": "<|tool_call_argument_begin|>"
215
+ },
216
+ {
217
+ "type": "json_schema",
218
+ "json_schema": {
219
+ "type": "object",
220
+ "properties": {
221
+ "y": {
222
+ "type": "number"
223
+ },
224
+ "x": {
225
+ "type": "number"
226
+ },
227
+ "items": {
228
+ "type": "array",
229
+ "items": {
230
+ "type": "string"
231
+ }
232
+ }
233
+ },
234
+ "required": [
235
+ "y",
236
+ "x",
237
+ "items"
238
+ ]
239
+ },
240
+ "style": "json",
241
+ "any_order": false,
242
+ "max_whitespace_cnt": null,
243
+ "excludes": []
244
+ }
245
+ ]
246
+ },
247
+ "end": "<|tool_call_end|>"
248
+ }
249
+ ],
250
+ "separator": "",
251
+ "at_least_one": true,
252
+ "stop_after_first": false
253
+ },
254
+ {
255
+ "type": "const_string",
256
+ "value": "<|tool_calls_section_end|>"
257
+ }
258
+ ]
259
+ }
260
+ ]
261
+ },
262
+ "coverage": "from_token_zero"
263
+ },
264
+ "completion": "Need call the weather and add tools.</think><|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{\"location\": \"Shanghai\"}<|tool_call_end|><|tool_call_begin|>functions.add:1<|tool_call_argument_begin|>{\"y\": 1.0, \"x\": 2, \"items\": [\"left\", \"right\"]}<|tool_call_end|><|tool_calls_section_end|>",
265
+ "generation_token_ids": [20082, 2390, 276, 10666, 316, 1239, 7697, 13, 163607, 163595, 163597, 41937, 1150, 21055, 2800, 25, 15, 163598, 8264, 5791, 1289, 414, 2797, 31231, 16934, 163599, 163597, 41937, 3149, 25, 16, 163598, 8264, 88, 1289, 220, 16, 13, 15, 11, 414, 87, 1289, 220, 17, 11, 414, 7989, 1289, 8681, 3794, 665, 414, 2277, 3956, 92, 163599, 163596, 163586]
266
+ },
267
+ "named": {
268
+ "grammar": {
269
+ "format": {
270
+ "type": "sequence",
271
+ "elements": [
272
+ {
273
+ "type": "tag",
274
+ "begin": "",
275
+ "content": {
276
+ "type": "any_text",
277
+ "excludes": [],
278
+ "max_tokens": null,
279
+ "max_chars": null
280
+ },
281
+ "end": "</think>"
282
+ },
283
+ {
284
+ "type": "sequence",
285
+ "elements": [
286
+ {
287
+ "type": "const_string",
288
+ "value": "<|tool_calls_section_begin|>"
289
+ },
290
+ {
291
+ "type": "tag",
292
+ "begin": "<|tool_call_begin|>functions.get_weather:",
293
+ "content": {
294
+ "type": "sequence",
295
+ "elements": [
296
+ {
297
+ "type": "regex",
298
+ "pattern": "\\d+"
299
+ },
300
+ {
301
+ "type": "const_string",
302
+ "value": "<|tool_call_argument_begin|>"
303
+ },
304
+ {
305
+ "type": "json_schema",
306
+ "json_schema": {
307
+ "type": "object",
308
+ "properties": {
309
+ "location": {
310
+ "type": "string"
311
+ }
312
+ },
313
+ "required": [
314
+ "location"
315
+ ]
316
+ },
317
+ "style": "json",
318
+ "any_order": false,
319
+ "max_whitespace_cnt": null,
320
+ "excludes": []
321
+ }
322
+ ]
323
+ },
324
+ "end": "<|tool_call_end|>"
325
+ },
326
+ {
327
+ "type": "const_string",
328
+ "value": "<|tool_calls_section_end|>"
329
+ }
330
+ ]
331
+ }
332
+ ]
333
+ },
334
+ "coverage": "from_token_zero"
335
+ },
336
+ "completion": "Need call the weather and add tools.</think><|tool_calls_section_begin|><|tool_call_begin|>functions.get_weather:0<|tool_call_argument_begin|>{\"location\": \"Shanghai\"}<|tool_call_end|><|tool_calls_section_end|>",
337
+ "generation_token_ids": [20082, 2390, 276, 10666, 316, 1239, 7697, 13, 163607, 163595, 163597, 41937, 1150, 21055, 2800, 25, 15, 163598, 8264, 5791, 1289, 414, 2797, 31231, 16934, 163599, 163596, 163586]
338
+ }
339
+ }
340
+}
rust/src/chat/tests/grammar_replay/Kimi-K3.json
new
+587
@@ -0,0 +1,587 @@
1
+{
2
+ "model_id": "moonshotai/Kimi-K3",
3
+ "vocab_size": 163840,
4
+ "all_stop_token_ids": [163585, 163586],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "optional",
13
+ "content": {
14
+ "type": "const_string",
15
+ "value": "<|open|>response<|sep|>"
16
+ }
17
+ },
18
+ {
19
+ "type": "tag",
20
+ "begin": "",
21
+ "content": {
22
+ "type": "any_text",
23
+ "excludes": [
24
+ "<|open|>",
25
+ "<|close|>",
26
+ "<|end_of_msg|>"
27
+ ],
28
+ "max_tokens": null,
29
+ "max_chars": null
30
+ },
31
+ "end": "<|close|>response<|sep|>"
32
+ },
33
+ {
34
+ "type": "optional",
35
+ "content": {
36
+ "type": "tag",
37
+ "begin": "<|open|>tools<|sep|>",
38
+ "content": {
39
+ "type": "tags_with_separator",
40
+ "tags": [
41
+ {
42
+ "type": "tag",
43
+ "begin": "<|open|>call tool=\"get_weather\" index=\"",
44
+ "content": {
45
+ "type": "sequence",
46
+ "elements": [
47
+ {
48
+ "type": "regex",
49
+ "pattern": "[1-9][0-9]*"
50
+ },
51
+ {
52
+ "type": "const_string",
53
+ "value": "\"<|sep|>"
54
+ },
55
+ {
56
+ "type": "or",
57
+ "elements": [
58
+ {
59
+ "type": "plus",
60
+ "content": {
61
+ "type": "tag",
62
+ "begin": "<|open|>argument key=\"location\" type=\"string\"<|sep|>",
63
+ "content": {
64
+ "type": "any_text",
65
+ "excludes": [
66
+ "<|close|>argument<|sep|>",
67
+ "<|close|>call<|sep|>"
68
+ ],
69
+ "max_tokens": null,
70
+ "max_chars": null
71
+ },
72
+ "end": "<|close|>argument<|sep|>"
73
+ }
74
+ },
75
+ {
76
+ "type": "tag",
77
+ "begin": "<|open|>json type=\"object\"<|sep|>",
78
+ "content": {
79
+ "type": "json_schema",
80
+ "json_schema": {
81
+ "type": "object",
82
+ "properties": {
83
+ "location": {
84
+ "type": "string"
85
+ }
86
+ },
87
+ "required": [
88
+ "location"
89
+ ]
90
+ },
91
+ "style": "json",
92
+ "any_order": false,
93
+ "max_whitespace_cnt": null,
94
+ "excludes": []
95
+ },
96
+ "end": "<|close|>json<|sep|>"
97
+ }
98
+ ]
99
+ }
100
+ ]
101
+ },
102
+ "end": "<|close|>call<|sep|>"
103
+ },
104
+ {
105
+ "type": "tag",
106
+ "begin": "<|open|>call tool=\"add\" index=\"",
107
+ "content": {
108
+ "type": "sequence",
109
+ "elements": [
110
+ {
111
+ "type": "regex",
112
+ "pattern": "[1-9][0-9]*"
113
+ },
114
+ {
115
+ "type": "const_string",
116
+ "value": "\"<|sep|>"
117
+ },
118
+ {
119
+ "type": "or",
120
+ "elements": [
121
+ {
122
+ "type": "plus",
123
+ "content": {
124
+ "type": "or",
125
+ "elements": [
126
+ {
127
+ "type": "tag",
128
+ "begin": "<|open|>argument key=\"y\" type=\"number\"<|sep|>",
129
+ "content": {
130
+ "type": "json_schema",
131
+ "json_schema": {
132
+ "type": "number"
133
+ },
134
+ "style": "json",
135
+ "any_order": false,
136
+ "max_whitespace_cnt": null,
137
+ "excludes": []
138
+ },
139
+ "end": "<|close|>argument<|sep|>"
140
+ },
141
+ {
142
+ "type": "tag",
143
+ "begin": "<|open|>argument key=\"x\" type=\"number\"<|sep|>",
144
+ "content": {
145
+ "type": "json_schema",
146
+ "json_schema": {
147
+ "type": "number"
148
+ },
149
+ "style": "json",
150
+ "any_order": false,
151
+ "max_whitespace_cnt": null,
152
+ "excludes": []
153
+ },
154
+ "end": "<|close|>argument<|sep|>"
155
+ },
156
+ {
157
+ "type": "tag",
158
+ "begin": "<|open|>argument key=\"items\" type=\"array\"<|sep|>",
159
+ "content": {
160
+ "type": "json_schema",
161
+ "json_schema": {
162
+ "type": "array",
163
+ "items": {
164
+ "type": "string"
165
+ }
166
+ },
167
+ "style": "json",
168
+ "any_order": false,
169
+ "max_whitespace_cnt": null,
170
+ "excludes": []
171
+ },
172
+ "end": "<|close|>argument<|sep|>"
173
+ }
174
+ ]
175
+ }
176
+ },
177
+ {
178
+ "type": "tag",
179
+ "begin": "<|open|>json type=\"object\"<|sep|>",
180
+ "content": {
181
+ "type": "json_schema",
182
+ "json_schema": {
183
+ "type": "object",
184
+ "properties": {
185
+ "y": {
186
+ "type": "number"
187
+ },
188
+ "x": {
189
+ "type": "number"
190
+ },
191
+ "items": {
192
+ "type": "array",
193
+ "items": {
194
+ "type": "string"
195
+ }
196
+ }
197
+ },
198
+ "required": [
199
+ "y",
200
+ "x",
201
+ "items"
202
+ ]
203
+ },
204
+ "style": "json",
205
+ "any_order": false,
206
+ "max_whitespace_cnt": null,
207
+ "excludes": []
208
+ },
209
+ "end": "<|close|>json<|sep|>"
210
+ }
211
+ ]
212
+ }
213
+ ]
214
+ },
215
+ "end": "<|close|>call<|sep|>"
216
+ }
217
+ ],
218
+ "separator": "",
219
+ "at_least_one": true,
220
+ "stop_after_first": false
221
+ },
222
+ "end": "<|close|>tools<|sep|>"
223
+ }
224
+ },
225
+ {
226
+ "type": "optional",
227
+ "content": {
228
+ "type": "const_string",
229
+ "value": "<|close|>message<|sep|>"
230
+ }
231
+ }
232
+ ]
233
+ },
234
+ "coverage": "final_output_only"
235
+ },
236
+ "completion": "Need call the weather and add tools.<|close|>think<|sep|><|open|>response<|sep|>I will call the tools.<|close|>response<|sep|><|open|>tools<|sep|><|open|>call tool=\"get_weather\" index=\"1\"<|sep|><|open|>argument key=\"location\" type=\"string\"<|sep|>Shanghai<|close|>argument<|sep|><|close|>call<|sep|><|open|>call tool=\"add\" index=\"2\"<|sep|><|open|>argument key=\"y\" type=\"number\"<|sep|>1.0<|close|>argument<|sep|><|open|>argument key=\"x\" type=\"number\"<|sep|>2<|close|>argument<|sep|><|open|>argument key=\"items\" type=\"array\"<|sep|>[\"left\",\"right\"]<|close|>argument<|sep|><|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>",
237
+ "generation_token_ids": [20082, 2390, 276, 10666, 316, 1239, 7697, 13, 163588, 39964, 163589, 163587, 12092, 163589, 40, 748, 2390, 276, 7697, 13, 163588, 12092, 163589, 163587, 25385, 163589, 163587, 10257, 4453, 878, 618, 21055, 2800, 1, 4002, 878, 16, 1, 163589, 163587, 47185, 2355, 878, 5791, 1, 1798, 878, 2033, 1, 163589, 2797, 31231, 163588, 47185, 163589, 163588, 10257, 163589, 163587, 10257, 4453, 878, 1210, 1, 4002, 878, 17, 1, 163589, 163587, 47185, 2355, 878, 88, 1, 1798, 878, 10006, 1, 163589, 16, 13, 15, 163588, 47185, 163589, 163587, 47185, 2355, 878, 87, 1, 1798, 878, 10006, 1, 163589, 17, 163588, 47185, 163589, 163587, 47185, 2355, 878, 7989, 1, 1798, 878, 4392, 1, 163589, 3530, 3794, 3923, 2277, 3956, 163588, 47185, 163589, 163588, 10257, 163589, 163588, 25385, 163589, 163588, 2778, 163589, 163586]
238
+ },
239
+ "required": {
240
+ "grammar": {
241
+ "format": {
242
+ "type": "sequence",
243
+ "elements": [
244
+ {
245
+ "type": "optional",
246
+ "content": {
247
+ "type": "const_string",
248
+ "value": "<|open|>response<|sep|>"
249
+ }
250
+ },
251
+ {
252
+ "type": "tag",
253
+ "begin": "",
254
+ "content": {
255
+ "type": "any_text",
256
+ "excludes": [
257
+ "<|open|>",
258
+ "<|close|>",
259
+ "<|end_of_msg|>"
260
+ ],
261
+ "max_tokens": null,
262
+ "max_chars": null
263
+ },
264
+ "end": "<|close|>response<|sep|>"
265
+ },
266
+ {
267
+ "type": "tag",
268
+ "begin": "<|open|>tools<|sep|>",
269
+ "content": {
270
+ "type": "tags_with_separator",
271
+ "tags": [
272
+ {
273
+ "type": "tag",
274
+ "begin": "<|open|>call tool=\"get_weather\" index=\"",
275
+ "content": {
276
+ "type": "sequence",
277
+ "elements": [
278
+ {
279
+ "type": "regex",
280
+ "pattern": "[1-9][0-9]*"
281
+ },
282
+ {
283
+ "type": "const_string",
284
+ "value": "\"<|sep|>"
285
+ },
286
+ {
287
+ "type": "or",
288
+ "elements": [
289
+ {
290
+ "type": "plus",
291
+ "content": {
292
+ "type": "tag",
293
+ "begin": "<|open|>argument key=\"location\" type=\"string\"<|sep|>",
294
+ "content": {
295
+ "type": "any_text",
296
+ "excludes": [
297
+ "<|close|>argument<|sep|>",
298
+ "<|close|>call<|sep|>"
299
+ ],
300
+ "max_tokens": null,
301
+ "max_chars": null
302
+ },
303
+ "end": "<|close|>argument<|sep|>"
304
+ }
305
+ },
306
+ {
307
+ "type": "tag",
308
+ "begin": "<|open|>json type=\"object\"<|sep|>",
309
+ "content": {
310
+ "type": "json_schema",
311
+ "json_schema": {
312
+ "type": "object",
313
+ "properties": {
314
+ "location": {
315
+ "type": "string"
316
+ }
317
+ },
318
+ "required": [
319
+ "location"
320
+ ]
321
+ },
322
+ "style": "json",
323
+ "any_order": false,
324
+ "max_whitespace_cnt": null,
325
+ "excludes": []
326
+ },
327
+ "end": "<|close|>json<|sep|>"
328
+ }
329
+ ]
330
+ }
331
+ ]
332
+ },
333
+ "end": "<|close|>call<|sep|>"
334
+ },
335
+ {
336
+ "type": "tag",
337
+ "begin": "<|open|>call tool=\"add\" index=\"",
338
+ "content": {
339
+ "type": "sequence",
340
+ "elements": [
341
+ {
342
+ "type": "regex",
343
+ "pattern": "[1-9][0-9]*"
344
+ },
345
+ {
346
+ "type": "const_string",
347
+ "value": "\"<|sep|>"
348
+ },
349
+ {
350
+ "type": "or",
351
+ "elements": [
352
+ {
353
+ "type": "plus",
354
+ "content": {
355
+ "type": "or",
356
+ "elements": [
357
+ {
358
+ "type": "tag",
359
+ "begin": "<|open|>argument key=\"y\" type=\"number\"<|sep|>",
360
+ "content": {
361
+ "type": "json_schema",
362
+ "json_schema": {
363
+ "type": "number"
364
+ },
365
+ "style": "json",
366
+ "any_order": false,
367
+ "max_whitespace_cnt": null,
368
+ "excludes": []
369
+ },
370
+ "end": "<|close|>argument<|sep|>"
371
+ },
372
+ {
373
+ "type": "tag",
374
+ "begin": "<|open|>argument key=\"x\" type=\"number\"<|sep|>",
375
+ "content": {
376
+ "type": "json_schema",
377
+ "json_schema": {
378
+ "type": "number"
379
+ },
380
+ "style": "json",
381
+ "any_order": false,
382
+ "max_whitespace_cnt": null,
383
+ "excludes": []
384
+ },
385
+ "end": "<|close|>argument<|sep|>"
386
+ },
387
+ {
388
+ "type": "tag",
389
+ "begin": "<|open|>argument key=\"items\" type=\"array\"<|sep|>",
390
+ "content": {
391
+ "type": "json_schema",
392
+ "json_schema": {
393
+ "type": "array",
394
+ "items": {
395
+ "type": "string"
396
+ }
397
+ },
398
+ "style": "json",
399
+ "any_order": false,
400
+ "max_whitespace_cnt": null,
401
+ "excludes": []
402
+ },
403
+ "end": "<|close|>argument<|sep|>"
404
+ }
405
+ ]
406
+ }
407
+ },
408
+ {
409
+ "type": "tag",
410
+ "begin": "<|open|>json type=\"object\"<|sep|>",
411
+ "content": {
412
+ "type": "json_schema",
413
+ "json_schema": {
414
+ "type": "object",
415
+ "properties": {
416
+ "y": {
417
+ "type": "number"
418
+ },
419
+ "x": {
420
+ "type": "number"
421
+ },
422
+ "items": {
423
+ "type": "array",
424
+ "items": {
425
+ "type": "string"
426
+ }
427
+ }
428
+ },
429
+ "required": [
430
+ "y",
431
+ "x",
432
+ "items"
433
+ ]
434
+ },
435
+ "style": "json",
436
+ "any_order": false,
437
+ "max_whitespace_cnt": null,
438
+ "excludes": []
439
+ },
440
+ "end": "<|close|>json<|sep|>"
441
+ }
442
+ ]
443
+ }
444
+ ]
445
+ },
446
+ "end": "<|close|>call<|sep|>"
447
+ }
448
+ ],
449
+ "separator": "",
450
+ "at_least_one": true,
451
+ "stop_after_first": false
452
+ },
453
+ "end": "<|close|>tools<|sep|>"
454
+ },
455
+ {
456
+ "type": "optional",
457
+ "content": {
458
+ "type": "const_string",
459
+ "value": "<|close|>message<|sep|>"
460
+ }
461
+ }
462
+ ]
463
+ },
464
+ "coverage": "final_output_only"
465
+ },
466
+ "completion": "Need call the weather and add tools.<|close|>think<|sep|><|open|>response<|sep|><|close|>response<|sep|><|open|>tools<|sep|><|open|>call tool=\"get_weather\" index=\"1\"<|sep|><|open|>argument key=\"location\" type=\"string\"<|sep|>Shanghai<|close|>argument<|sep|><|close|>call<|sep|><|open|>call tool=\"add\" index=\"2\"<|sep|><|open|>argument key=\"y\" type=\"number\"<|sep|>1.0<|close|>argument<|sep|><|open|>argument key=\"x\" type=\"number\"<|sep|>2<|close|>argument<|sep|><|open|>argument key=\"items\" type=\"array\"<|sep|>[\"left\",\"right\"]<|close|>argument<|sep|><|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>",
467
+ "generation_token_ids": [20082, 2390, 276, 10666, 316, 1239, 7697, 13, 163588, 39964, 163589, 163587, 12092, 163589, 163588, 12092, 163589, 163587, 25385, 163589, 163587, 10257, 4453, 878, 618, 21055, 2800, 1, 4002, 878, 16, 1, 163589, 163587, 47185, 2355, 878, 5791, 1, 1798, 878, 2033, 1, 163589, 2797, 31231, 163588, 47185, 163589, 163588, 10257, 163589, 163587, 10257, 4453, 878, 1210, 1, 4002, 878, 17, 1, 163589, 163587, 47185, 2355, 878, 88, 1, 1798, 878, 10006, 1, 163589, 16, 13, 15, 163588, 47185, 163589, 163587, 47185, 2355, 878, 87, 1, 1798, 878, 10006, 1, 163589, 17, 163588, 47185, 163589, 163587, 47185, 2355, 878, 7989, 1, 1798, 878, 4392, 1, 163589, 3530, 3794, 3923, 2277, 3956, 163588, 47185, 163589, 163588, 10257, 163589, 163588, 25385, 163589, 163588, 2778, 163589, 163586]
468
+ },
469
+ "named": {
470
+ "grammar": {
471
+ "format": {
472
+ "type": "sequence",
473
+ "elements": [
474
+ {
475
+ "type": "optional",
476
+ "content": {
477
+ "type": "const_string",
478
+ "value": "<|open|>response<|sep|>"
479
+ }
480
+ },
481
+ {
482
+ "type": "tag",
483
+ "begin": "",
484
+ "content": {
485
+ "type": "any_text",
486
+ "excludes": [
487
+ "<|open|>",
488
+ "<|close|>",
489
+ "<|end_of_msg|>"
490
+ ],
491
+ "max_tokens": null,
492
+ "max_chars": null
493
+ },
494
+ "end": "<|close|>response<|sep|>"
495
+ },
496
+ {
497
+ "type": "tag",
498
+ "begin": "<|open|>tools<|sep|>",
499
+ "content": {
500
+ "type": "tags_with_separator",
501
+ "tags": [
502
+ {
503
+ "type": "tag",
504
+ "begin": "<|open|>call tool=\"get_weather\" index=\"",
505
+ "content": {
506
+ "type": "sequence",
507
+ "elements": [
508
+ {
509
+ "type": "regex",
510
+ "pattern": "[1-9][0-9]*"
511
+ },
512
+ {
513
+ "type": "const_string",
514
+ "value": "\"<|sep|>"
515
+ },
516
+ {
517
+ "type": "or",
518
+ "elements": [
519
+ {
520
+ "type": "plus",
521
+ "content": {
522
+ "type": "tag",
523
+ "begin": "<|open|>argument key=\"location\" type=\"string\"<|sep|>",
524
+ "content": {
525
+ "type": "any_text",
526
+ "excludes": [
527
+ "<|close|>argument<|sep|>",
528
+ "<|close|>call<|sep|>"
529
+ ],
530
+ "max_tokens": null,
531
+ "max_chars": null
532
+ },
533
+ "end": "<|close|>argument<|sep|>"
534
+ }
535
+ },
536
+ {
537
+ "type": "tag",
538
+ "begin": "<|open|>json type=\"object\"<|sep|>",
539
+ "content": {
540
+ "type": "json_schema",
541
+ "json_schema": {
542
+ "type": "object",
543
+ "properties": {
544
+ "location": {
545
+ "type": "string"
546
+ }
547
+ },
548
+ "required": [
549
+ "location"
550
+ ]
551
+ },
552
+ "style": "json",
553
+ "any_order": false,
554
+ "max_whitespace_cnt": null,
555
+ "excludes": []
556
+ },
557
+ "end": "<|close|>json<|sep|>"
558
+ }
559
+ ]
560
+ }
561
+ ]
562
+ },
563
+ "end": "<|close|>call<|sep|>"
564
+ }
565
+ ],
566
+ "separator": "",
567
+ "at_least_one": true,
568
+ "stop_after_first": false
569
+ },
570
+ "end": "<|close|>tools<|sep|>"
571
+ },
572
+ {
573
+ "type": "optional",
574
+ "content": {
575
+ "type": "const_string",
576
+ "value": "<|close|>message<|sep|>"
577
+ }
578
+ }
579
+ ]
580
+ },
581
+ "coverage": "final_output_only"
582
+ },
583
+ "completion": "Need call the weather and add tools.<|close|>think<|sep|><|open|>response<|sep|><|close|>response<|sep|><|open|>tools<|sep|><|open|>call tool=\"get_weather\" index=\"1\"<|sep|><|open|>argument key=\"location\" type=\"string\"<|sep|>Shanghai<|close|>argument<|sep|><|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>",
584
+ "generation_token_ids": [20082, 2390, 276, 10666, 316, 1239, 7697, 13, 163588, 39964, 163589, 163587, 12092, 163589, 163588, 12092, 163589, 163587, 25385, 163589, 163587, 10257, 4453, 878, 618, 21055, 2800, 1, 4002, 878, 16, 1, 163589, 163587, 47185, 2355, 878, 5791, 1, 1798, 878, 2033, 1, 163589, 2797, 31231, 163588, 47185, 163589, 163588, 10257, 163589, 163588, 25385, 163589, 163588, 2778, 163589, 163586]
585
+ }
586
+ }
587
+}
rust/src/chat/tests/grammar_replay/MiMo-V2.6-Flash-RL.json
new
+254
@@ -0,0 +1,254 @@
1
+{
2
+ "model_id": "XiaomiMiMo/MiMo-V2.6-Flash-RL",
3
+ "vocab_size": 152576,
4
+ "all_stop_token_ids": [151643, 151645, 151672],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "optional",
13
+ "content": {
14
+ "type": "tag",
15
+ "begin": "<think>",
16
+ "content": {
17
+ "type": "any_text",
18
+ "excludes": [],
19
+ "max_tokens": null,
20
+ "max_chars": null
21
+ },
22
+ "end": "</think>"
23
+ }
24
+ },
25
+ {
26
+ "type": "triggered_tags",
27
+ "triggers": [
28
+ "<tool_call>"
29
+ ],
30
+ "tags": [
31
+ {
32
+ "type": "tag",
33
+ "begin": "<tool_call><function=get_weather>",
34
+ "content": {
35
+ "type": "json_schema",
36
+ "json_schema": {
37
+ "type": "object",
38
+ "properties": {
39
+ "location": {
40
+ "type": "string"
41
+ }
42
+ },
43
+ "required": [
44
+ "location"
45
+ ]
46
+ },
47
+ "style": "qwen_xml",
48
+ "any_order": false,
49
+ "max_whitespace_cnt": null,
50
+ "excludes": []
51
+ },
52
+ "end": "</function></tool_call>"
53
+ },
54
+ {
55
+ "type": "tag",
56
+ "begin": "<tool_call><function=add>",
57
+ "content": {
58
+ "type": "json_schema",
59
+ "json_schema": {
60
+ "type": "object",
61
+ "properties": {
62
+ "y": {
63
+ "type": "number"
64
+ },
65
+ "x": {
66
+ "type": "number"
67
+ },
68
+ "items": {
69
+ "type": "array",
70
+ "items": {
71
+ "type": "string"
72
+ }
73
+ }
74
+ },
75
+ "required": [
76
+ "y",
77
+ "x",
78
+ "items"
79
+ ]
80
+ },
81
+ "style": "qwen_xml",
82
+ "any_order": false,
83
+ "max_whitespace_cnt": null,
84
+ "excludes": []
85
+ },
86
+ "end": "</function></tool_call>"
87
+ }
88
+ ],
89
+ "at_least_one": false,
90
+ "stop_after_first": false,
91
+ "excludes": [
92
+ "<think>",
93
+ "</think>",
94
+ "</tool_call>",
95
+ "<function="
96
+ ]
97
+ }
98
+ ]
99
+ },
100
+ "coverage": "from_token_zero"
101
+ },
102
+ "completion": "<think>Need call the weather and add tools.</think>I will call the tools.<tool_call><function=get_weather><parameter=location>Shanghai</parameter></function></tool_call><tool_call><function=add><parameter=y>1.0</parameter><parameter=x>2</parameter><parameter=items>[\"left\", \"right\"]</parameter></function></tool_call>",
103
+ "generation_token_ids": [151667, 23657, 1618, 279, 9104, 323, 912, 7375, 13, 151668, 40, 686, 1618, 279, 7375, 13, 151657, 27, 1688, 28280, 69364, 1784, 16181, 28, 2527, 29, 2016, 30070, 522, 16181, 1472, 1688, 29, 151658, 151657, 27, 1688, 92784, 1784, 16181, 29368, 29, 16, 13, 15, 522, 16181, 1784, 16181, 25387, 29, 17, 522, 16181, 1784, 16181, 28, 3615, 29, 1183, 2359, 497, 330, 1291, 1341, 522, 16181, 1472, 1688, 29, 151658, 151645]
104
+ },
105
+ "required": {
106
+ "grammar": {
107
+ "format": {
108
+ "type": "sequence",
109
+ "elements": [
110
+ {
111
+ "type": "optional",
112
+ "content": {
113
+ "type": "tag",
114
+ "begin": "<think>",
115
+ "content": {
116
+ "type": "any_text",
117
+ "excludes": [],
118
+ "max_tokens": null,
119
+ "max_chars": null
120
+ },
121
+ "end": "</think>"
122
+ }
123
+ },
124
+ {
125
+ "type": "triggered_tags",
126
+ "triggers": [
127
+ "<tool_call>"
128
+ ],
129
+ "tags": [
130
+ {
131
+ "type": "tag",
132
+ "begin": "<tool_call><function=get_weather>",
133
+ "content": {
134
+ "type": "json_schema",
135
+ "json_schema": {
136
+ "type": "object",
137
+ "properties": {
138
+ "location": {
139
+ "type": "string"
140
+ }
141
+ },
142
+ "required": [
143
+ "location"
144
+ ]
145
+ },
146
+ "style": "qwen_xml",
147
+ "any_order": false,
148
+ "max_whitespace_cnt": null,
149
+ "excludes": []
150
+ },
151
+ "end": "</function></tool_call>"
152
+ },
153
+ {
154
+ "type": "tag",
155
+ "begin": "<tool_call><function=add>",
156
+ "content": {
157
+ "type": "json_schema",
158
+ "json_schema": {
159
+ "type": "object",
160
+ "properties": {
161
+ "y": {
162
+ "type": "number"
163
+ },
164
+ "x": {
165
+ "type": "number"
166
+ },
167
+ "items": {
168
+ "type": "array",
169
+ "items": {
170
+ "type": "string"
171
+ }
172
+ }
173
+ },
174
+ "required": [
175
+ "y",
176
+ "x",
177
+ "items"
178
+ ]
179
+ },
180
+ "style": "qwen_xml",
181
+ "any_order": false,
182
+ "max_whitespace_cnt": null,
183
+ "excludes": []
184
+ },
185
+ "end": "</function></tool_call>"
186
+ }
187
+ ],
188
+ "at_least_one": true,
189
+ "stop_after_first": false,
190
+ "excludes": [
191
+ "<think>",
192
+ "</think>",
193
+ "</tool_call>",
194
+ "<function="
195
+ ]
196
+ }
197
+ ]
198
+ },
199
+ "coverage": "from_token_zero"
200
+ },
201
+ "completion": "<think>Need call the weather and add tools.</think><tool_call><function=get_weather><parameter=location>Shanghai</parameter></function></tool_call><tool_call><function=add><parameter=y>1.0</parameter><parameter=x>2</parameter><parameter=items>[\"left\", \"right\"]</parameter></function></tool_call>",
202
+ "generation_token_ids": [151667, 23657, 1618, 279, 9104, 323, 912, 7375, 13, 151668, 151657, 27, 1688, 28280, 69364, 1784, 16181, 28, 2527, 29, 2016, 30070, 522, 16181, 1472, 1688, 29, 151658, 151657, 27, 1688, 92784, 1784, 16181, 29368, 29, 16, 13, 15, 522, 16181, 1784, 16181, 25387, 29, 17, 522, 16181, 1784, 16181, 28, 3615, 29, 1183, 2359, 497, 330, 1291, 1341, 522, 16181, 1472, 1688, 29, 151658, 151645]
203
+ },
204
+ "named": {
205
+ "grammar": {
206
+ "format": {
207
+ "type": "sequence",
208
+ "elements": [
209
+ {
210
+ "type": "optional",
211
+ "content": {
212
+ "type": "tag",
213
+ "begin": "<think>",
214
+ "content": {
215
+ "type": "any_text",
216
+ "excludes": [],
217
+ "max_tokens": null,
218
+ "max_chars": null
219
+ },
220
+ "end": "</think>"
221
+ }
222
+ },
223
+ {
224
+ "type": "tag",
225
+ "begin": "<tool_call><function=get_weather>",
226
+ "content": {
227
+ "type": "json_schema",
228
+ "json_schema": {
229
+ "type": "object",
230
+ "properties": {
231
+ "location": {
232
+ "type": "string"
233
+ }
234
+ },
235
+ "required": [
236
+ "location"
237
+ ]
238
+ },
239
+ "style": "qwen_xml",
240
+ "any_order": false,
241
+ "max_whitespace_cnt": null,
242
+ "excludes": []
243
+ },
244
+ "end": "</function></tool_call>"
245
+ }
246
+ ]
247
+ },
248
+ "coverage": "from_token_zero"
249
+ },
250
+ "completion": "<think>Need call the weather and add tools.</think><tool_call><function=get_weather><parameter=location>Shanghai</parameter></function></tool_call>",
251
+ "generation_token_ids": [151667, 23657, 1618, 279, 9104, 323, 912, 7375, 13, 151668, 151657, 27, 1688, 28280, 69364, 1784, 16181, 28, 2527, 29, 2016, 30070, 522, 16181, 1472, 1688, 29, 151658, 151645]
252
+ }
253
+ }
254
+}
rust/src/chat/tests/grammar_replay/MiniMax-M2.5.json
new
+301
@@ -0,0 +1,301 @@
1
+{
2
+ "model_id": "MiniMaxAI/MiniMax-M2.5",
3
+ "vocab_size": 200064,
4
+ "all_stop_token_ids": [200020],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "sequence",
13
+ "elements": [
14
+ {
15
+ "type": "tag",
16
+ "begin": "",
17
+ "content": {
18
+ "type": "any_text",
19
+ "excludes": [],
20
+ "max_tokens": null,
21
+ "max_chars": null
22
+ },
23
+ "end": "</think>"
24
+ },
25
+ {
26
+ "type": "const_string",
27
+ "value": "\n\n"
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "type": "triggered_tags",
33
+ "triggers": [
34
+ "<minimax:tool_call>"
35
+ ],
36
+ "tags": [
37
+ {
38
+ "type": "tag",
39
+ "begin": "<minimax:tool_call>\n",
40
+ "content": {
41
+ "type": "tags_with_separator",
42
+ "tags": [
43
+ {
44
+ "type": "tag",
45
+ "begin": "<invoke name=\"get_weather\">\n",
46
+ "content": {
47
+ "type": "json_schema",
48
+ "json_schema": {
49
+ "type": "object",
50
+ "properties": {
51
+ "location": {
52
+ "type": "string"
53
+ }
54
+ },
55
+ "required": [
56
+ "location"
57
+ ]
58
+ },
59
+ "style": "minimax_xml",
60
+ "any_order": false,
61
+ "max_whitespace_cnt": null,
62
+ "excludes": []
63
+ },
64
+ "end": "</invoke>\n"
65
+ },
66
+ {
67
+ "type": "tag",
68
+ "begin": "<invoke name=\"add\">\n",
69
+ "content": {
70
+ "type": "json_schema",
71
+ "json_schema": {
72
+ "type": "object",
73
+ "properties": {
74
+ "y": {
75
+ "type": "number"
76
+ },
77
+ "x": {
78
+ "type": "number"
79
+ },
80
+ "items": {
81
+ "type": "array",
82
+ "items": {
83
+ "type": "string"
84
+ }
85
+ }
86
+ },
87
+ "required": [
88
+ "y",
89
+ "x",
90
+ "items"
91
+ ]
92
+ },
93
+ "style": "minimax_xml",
94
+ "any_order": false,
95
+ "max_whitespace_cnt": null,
96
+ "excludes": []
97
+ },
98
+ "end": "</invoke>\n"
99
+ }
100
+ ],
101
+ "separator": "",
102
+ "at_least_one": true,
103
+ "stop_after_first": false
104
+ },
105
+ "end": "</minimax:tool_call>"
106
+ }
107
+ ],
108
+ "at_least_one": false,
109
+ "stop_after_first": false,
110
+ "excludes": [
111
+ "<think>",
112
+ "</think>"
113
+ ]
114
+ }
115
+ ]
116
+ },
117
+ "coverage": "from_token_zero"
118
+ },
119
+ "completion": "Need call the weather and add tools.\n</think>\n\nI will call the tools.\n<minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Shanghai</parameter>\n</invoke>\n<invoke name=\"add\">\n<parameter name=\"y\">1.0</parameter>\n<parameter name=\"x\">2</parameter>\n<parameter name=\"items\">[\"left\", \"right\"]</parameter>\n</invoke>\n</minimax:tool_call>",
120
+ "generation_token_ids": [42478, 1728, 275, 12562, 306, 1187, 6629, 320, 200051, 367, 73, 715, 1728, 275, 6629, 320, 200052, 10, 60, 88638, 1925, 1139, 848, 95, 51872, 6143, 60, 24662, 1925, 1139, 28415, 3361, 165806, 1579, 24662, 1100, 1579, 88638, 1100, 60, 88638, 1925, 1139, 2132, 6143, 60, 24662, 1925, 1139, 121, 3361, 49, 46, 48, 1579, 24662, 1100, 60, 24662, 1925, 1139, 120, 3361, 50, 1579, 24662, 1100, 60, 24662, 1925, 1139, 12405, 3361, 7795, 2038, 1164, 494, 1581, 8760, 1579, 24662, 1100, 1579, 88638, 1100, 200053, 200020]
121
+ },
122
+ "required": {
123
+ "grammar": {
124
+ "format": {
125
+ "type": "sequence",
126
+ "elements": [
127
+ {
128
+ "type": "sequence",
129
+ "elements": [
130
+ {
131
+ "type": "tag",
132
+ "begin": "",
133
+ "content": {
134
+ "type": "any_text",
135
+ "excludes": [],
136
+ "max_tokens": null,
137
+ "max_chars": null
138
+ },
139
+ "end": "</think>"
140
+ },
141
+ {
142
+ "type": "const_string",
143
+ "value": "\n\n"
144
+ }
145
+ ]
146
+ },
147
+ {
148
+ "type": "sequence",
149
+ "elements": [
150
+ {
151
+ "type": "const_string",
152
+ "value": "\n<minimax:tool_call>\n"
153
+ },
154
+ {
155
+ "type": "tags_with_separator",
156
+ "tags": [
157
+ {
158
+ "type": "tag",
159
+ "begin": "<invoke name=\"get_weather\">\n",
160
+ "content": {
161
+ "type": "json_schema",
162
+ "json_schema": {
163
+ "type": "object",
164
+ "properties": {
165
+ "location": {
166
+ "type": "string"
167
+ }
168
+ },
169
+ "required": [
170
+ "location"
171
+ ]
172
+ },
173
+ "style": "minimax_xml",
174
+ "any_order": false,
175
+ "max_whitespace_cnt": null,
176
+ "excludes": []
177
+ },
178
+ "end": "</invoke>\n"
179
+ },
180
+ {
181
+ "type": "tag",
182
+ "begin": "<invoke name=\"add\">\n",
183
+ "content": {
184
+ "type": "json_schema",
185
+ "json_schema": {
186
+ "type": "object",
187
+ "properties": {
188
+ "y": {
189
+ "type": "number"
190
+ },
191
+ "x": {
192
+ "type": "number"
193
+ },
194
+ "items": {
195
+ "type": "array",
196
+ "items": {
197
+ "type": "string"
198
+ }
199
+ }
200
+ },
201
+ "required": [
202
+ "y",
203
+ "x",
204
+ "items"
205
+ ]
206
+ },
207
+ "style": "minimax_xml",
208
+ "any_order": false,
209
+ "max_whitespace_cnt": null,
210
+ "excludes": []
211
+ },
212
+ "end": "</invoke>\n"
213
+ }
214
+ ],
215
+ "separator": "",
216
+ "at_least_one": true,
217
+ "stop_after_first": false
218
+ },
219
+ {
220
+ "type": "const_string",
221
+ "value": "</minimax:tool_call>"
222
+ }
223
+ ]
224
+ }
225
+ ]
226
+ },
227
+ "coverage": "from_token_zero"
228
+ },
229
+ "completion": "Need call the weather and add tools.\n</think>\n\n\n<minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Shanghai</parameter>\n</invoke>\n<invoke name=\"add\">\n<parameter name=\"y\">1.0</parameter>\n<parameter name=\"x\">2</parameter>\n<parameter name=\"items\">[\"left\", \"right\"]</parameter>\n</invoke>\n</minimax:tool_call>",
230
+ "generation_token_ids": [42478, 1728, 275, 12562, 306, 1187, 6629, 320, 200051, 4368, 200052, 10, 60, 88638, 1925, 1139, 848, 95, 51872, 6143, 60, 24662, 1925, 1139, 28415, 3361, 165806, 1579, 24662, 1100, 1579, 88638, 1100, 60, 88638, 1925, 1139, 2132, 6143, 60, 24662, 1925, 1139, 121, 3361, 49, 46, 48, 1579, 24662, 1100, 60, 24662, 1925, 1139, 120, 3361, 50, 1579, 24662, 1100, 60, 24662, 1925, 1139, 12405, 3361, 7795, 2038, 1164, 494, 1581, 8760, 1579, 24662, 1100, 1579, 88638, 1100, 200053, 200020]
231
+ },
232
+ "named": {
233
+ "grammar": {
234
+ "format": {
235
+ "type": "sequence",
236
+ "elements": [
237
+ {
238
+ "type": "sequence",
239
+ "elements": [
240
+ {
241
+ "type": "tag",
242
+ "begin": "",
243
+ "content": {
244
+ "type": "any_text",
245
+ "excludes": [],
246
+ "max_tokens": null,
247
+ "max_chars": null
248
+ },
249
+ "end": "</think>"
250
+ },
251
+ {
252
+ "type": "const_string",
253
+ "value": "\n\n"
254
+ }
255
+ ]
256
+ },
257
+ {
258
+ "type": "sequence",
259
+ "elements": [
260
+ {
261
+ "type": "const_string",
262
+ "value": "\n<minimax:tool_call>\n"
263
+ },
264
+ {
265
+ "type": "tag",
266
+ "begin": "<invoke name=\"get_weather\">\n",
267
+ "content": {
268
+ "type": "json_schema",
269
+ "json_schema": {
270
+ "type": "object",
271
+ "properties": {
272
+ "location": {
273
+ "type": "string"
274
+ }
275
+ },
276
+ "required": [
277
+ "location"
278
+ ]
279
+ },
280
+ "style": "minimax_xml",
281
+ "any_order": false,
282
+ "max_whitespace_cnt": null,
283
+ "excludes": []
284
+ },
285
+ "end": "</invoke>\n"
286
+ },
287
+ {
288
+ "type": "const_string",
289
+ "value": "</minimax:tool_call>"
290
+ }
291
+ ]
292
+ }
293
+ ]
294
+ },
295
+ "coverage": "from_token_zero"
296
+ },
297
+ "completion": "Need call the weather and add tools.\n</think>\n\n\n<minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Shanghai</parameter>\n</invoke>\n</minimax:tool_call>",
298
+ "generation_token_ids": [42478, 1728, 275, 12562, 306, 1187, 6629, 320, 200051, 4368, 200052, 10, 60, 88638, 1925, 1139, 848, 95, 51872, 6143, 60, 24662, 1925, 1139, 28415, 3361, 165806, 1579, 24662, 1100, 1579, 88638, 1100, 200053, 200020]
299
+ }
300
+ }
301
+}
rust/src/chat/tests/grammar_replay/Qwen3-0.6B.json
new
+277
@@ -0,0 +1,277 @@
1
+{
2
+ "model_id": "Qwen/Qwen3-0.6B",
3
+ "vocab_size": 151936,
4
+ "all_stop_token_ids": [151643, 151645],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "optional",
13
+ "content": {
14
+ "type": "sequence",
15
+ "elements": [
16
+ {
17
+ "type": "tag",
18
+ "begin": "<think>",
19
+ "content": {
20
+ "type": "any_text",
21
+ "excludes": [],
22
+ "max_tokens": null,
23
+ "max_chars": null
24
+ },
25
+ "end": "</think>"
26
+ },
27
+ {
28
+ "type": "const_string",
29
+ "value": "\n\n"
30
+ }
31
+ ]
32
+ }
33
+ },
34
+ {
35
+ "type": "triggered_tags",
36
+ "triggers": [
37
+ "<tool_call>"
38
+ ],
39
+ "tags": [
40
+ {
41
+ "type": "tag",
42
+ "begin": "<tool_call>\n{\"name\": \"get_weather\", \"arguments\": ",
43
+ "content": {
44
+ "type": "json_schema",
45
+ "json_schema": {
46
+ "type": "object",
47
+ "properties": {
48
+ "location": {
49
+ "type": "string"
50
+ }
51
+ },
52
+ "required": [
53
+ "location"
54
+ ]
55
+ },
56
+ "style": "json",
57
+ "any_order": false,
58
+ "max_whitespace_cnt": null,
59
+ "excludes": []
60
+ },
61
+ "end": "}\n</tool_call>"
62
+ },
63
+ {
64
+ "type": "tag",
65
+ "begin": "<tool_call>\n{\"name\": \"add\", \"arguments\": ",
66
+ "content": {
67
+ "type": "json_schema",
68
+ "json_schema": {
69
+ "type": "object",
70
+ "properties": {
71
+ "y": {
72
+ "type": "number"
73
+ },
74
+ "x": {
75
+ "type": "number"
76
+ },
77
+ "items": {
78
+ "type": "array",
79
+ "items": {
80
+ "type": "string"
81
+ }
82
+ }
83
+ },
84
+ "required": [
85
+ "y",
86
+ "x",
87
+ "items"
88
+ ]
89
+ },
90
+ "style": "json",
91
+ "any_order": false,
92
+ "max_whitespace_cnt": null,
93
+ "excludes": []
94
+ },
95
+ "end": "}\n</tool_call>"
96
+ }
97
+ ],
98
+ "at_least_one": false,
99
+ "stop_after_first": false,
100
+ "excludes": [
101
+ "<think>",
102
+ "</think>"
103
+ ]
104
+ }
105
+ ]
106
+ },
107
+ "coverage": "from_token_zero"
108
+ },
109
+ "completion": "<think>\nNeed call the weather and add tools.\n</think>\n\nI will call the tools.\n<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Shanghai\"}}\n</tool_call>\n<tool_call>\n{\"name\": \"add\", \"arguments\": {\"y\": 1.0, \"x\": 2, \"items\": [\"left\", \"right\"]}}\n</tool_call>",
110
+ "generation_token_ids": [151667, 198, 23657, 1618, 279, 9104, 323, 912, 7375, 624, 151668, 271, 40, 686, 1618, 279, 7375, 624, 151657, 198, 4913, 606, 788, 330, 455, 69364, 497, 330, 16370, 788, 5212, 2527, 788, 330, 2016, 30070, 95642, 151658, 198, 151657, 198, 4913, 606, 788, 330, 718, 497, 330, 16370, 788, 5212, 88, 788, 220, 16, 13, 15, 11, 330, 87, 788, 220, 17, 11, 330, 3615, 788, 4383, 2359, 497, 330, 1291, 1341, 11248, 151658, 151645]
111
+ },
112
+ "required": {
113
+ "grammar": {
114
+ "format": {
115
+ "type": "sequence",
116
+ "elements": [
117
+ {
118
+ "type": "optional",
119
+ "content": {
120
+ "type": "sequence",
121
+ "elements": [
122
+ {
123
+ "type": "tag",
124
+ "begin": "<think>",
125
+ "content": {
126
+ "type": "any_text",
127
+ "excludes": [],
128
+ "max_tokens": null,
129
+ "max_chars": null
130
+ },
131
+ "end": "</think>"
132
+ },
133
+ {
134
+ "type": "const_string",
135
+ "value": "\n\n"
136
+ }
137
+ ]
138
+ }
139
+ },
140
+ {
141
+ "type": "triggered_tags",
142
+ "triggers": [
143
+ "<tool_call>"
144
+ ],
145
+ "tags": [
146
+ {
147
+ "type": "tag",
148
+ "begin": "<tool_call>\n{\"name\": \"get_weather\", \"arguments\": ",
149
+ "content": {
150
+ "type": "json_schema",
151
+ "json_schema": {
152
+ "type": "object",
153
+ "properties": {
154
+ "location": {
155
+ "type": "string"
156
+ }
157
+ },
158
+ "required": [
159
+ "location"
160
+ ]
161
+ },
162
+ "style": "json",
163
+ "any_order": false,
164
+ "max_whitespace_cnt": null,
165
+ "excludes": []
166
+ },
167
+ "end": "}\n</tool_call>"
168
+ },
169
+ {
170
+ "type": "tag",
171
+ "begin": "<tool_call>\n{\"name\": \"add\", \"arguments\": ",
172
+ "content": {
173
+ "type": "json_schema",
174
+ "json_schema": {
175
+ "type": "object",
176
+ "properties": {
177
+ "y": {
178
+ "type": "number"
179
+ },
180
+ "x": {
181
+ "type": "number"
182
+ },
183
+ "items": {
184
+ "type": "array",
185
+ "items": {
186
+ "type": "string"
187
+ }
188
+ }
189
+ },
190
+ "required": [
191
+ "y",
192
+ "x",
193
+ "items"
194
+ ]
195
+ },
196
+ "style": "json",
197
+ "any_order": false,
198
+ "max_whitespace_cnt": null,
199
+ "excludes": []
200
+ },
201
+ "end": "}\n</tool_call>"
202
+ }
203
+ ],
204
+ "at_least_one": true,
205
+ "stop_after_first": false,
206
+ "excludes": [
207
+ "<think>",
208
+ "</think>"
209
+ ]
210
+ }
211
+ ]
212
+ },
213
+ "coverage": "from_token_zero"
214
+ },
215
+ "completion": "<think>\nNeed call the weather and add tools.\n</think>\n\n<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Shanghai\"}}\n</tool_call>\n<tool_call>\n{\"name\": \"add\", \"arguments\": {\"y\": 1.0, \"x\": 2, \"items\": [\"left\", \"right\"]}}\n</tool_call>",
216
+ "generation_token_ids": [151667, 198, 23657, 1618, 279, 9104, 323, 912, 7375, 624, 151668, 271, 151657, 198, 4913, 606, 788, 330, 455, 69364, 497, 330, 16370, 788, 5212, 2527, 788, 330, 2016, 30070, 95642, 151658, 198, 151657, 198, 4913, 606, 788, 330, 718, 497, 330, 16370, 788, 5212, 88, 788, 220, 16, 13, 15, 11, 330, 87, 788, 220, 17, 11, 330, 3615, 788, 4383, 2359, 497, 330, 1291, 1341, 11248, 151658, 151645]
217
+ },
218
+ "named": {
219
+ "grammar": {
220
+ "format": {
221
+ "type": "sequence",
222
+ "elements": [
223
+ {
224
+ "type": "optional",
225
+ "content": {
226
+ "type": "sequence",
227
+ "elements": [
228
+ {
229
+ "type": "tag",
230
+ "begin": "<think>",
231
+ "content": {
232
+ "type": "any_text",
233
+ "excludes": [],
234
+ "max_tokens": null,
235
+ "max_chars": null
236
+ },
237
+ "end": "</think>"
238
+ },
239
+ {
240
+ "type": "const_string",
241
+ "value": "\n\n"
242
+ }
243
+ ]
244
+ }
245
+ },
246
+ {
247
+ "type": "tag",
248
+ "begin": "<tool_call>\n{\"name\": \"get_weather\", \"arguments\": ",
249
+ "content": {
250
+ "type": "json_schema",
251
+ "json_schema": {
252
+ "type": "object",
253
+ "properties": {
254
+ "location": {
255
+ "type": "string"
256
+ }
257
+ },
258
+ "required": [
259
+ "location"
260
+ ]
261
+ },
262
+ "style": "json",
263
+ "any_order": false,
264
+ "max_whitespace_cnt": null,
265
+ "excludes": []
266
+ },
267
+ "end": "}\n</tool_call>"
268
+ }
269
+ ]
270
+ },
271
+ "coverage": "from_token_zero"
272
+ },
273
+ "completion": "<think>\nNeed call the weather and add tools.\n</think>\n\n<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Shanghai\"}}\n</tool_call>",
274
+ "generation_token_ids": [151667, 198, 23657, 1618, 279, 9104, 323, 912, 7375, 624, 151668, 271, 151657, 198, 4913, 606, 788, 330, 455, 69364, 497, 330, 16370, 788, 5212, 2527, 788, 330, 2016, 30070, 95642, 151658, 151645]
275
+ }
276
+ }
277
+}
rust/src/chat/tests/grammar_replay/Qwen3.5-4B.json
new
+268
@@ -0,0 +1,268 @@
1
+{
2
+ "model_id": "Qwen/Qwen3.5-4B",
3
+ "vocab_size": 248320,
4
+ "all_stop_token_ids": [248044, 248046],
5
+ "cases": {
6
+ "auto": {
7
+ "grammar": {
8
+ "format": {
9
+ "type": "sequence",
10
+ "elements": [
11
+ {
12
+ "type": "sequence",
13
+ "elements": [
14
+ {
15
+ "type": "tag",
16
+ "begin": "",
17
+ "content": {
18
+ "type": "any_text",
19
+ "excludes": [],
20
+ "max_tokens": null,
21
+ "max_chars": null
22
+ },
23
+ "end": "</think>"
24
+ },
25
+ {
26
+ "type": "const_string",
27
+ "value": "\n\n"
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "type": "triggered_tags",
33
+ "triggers": [
34
+ "<tool_call>\n<function="
35
+ ],
36
+ "tags": [
37
+ {
38
+ "type": "tag",
39
+ "begin": "<tool_call>\n<function=get_weather>\n",
40
+ "content": {
41
+ "type": "json_schema",
42
+ "json_schema": {
43
+ "type": "object",
44
+ "properties": {
45
+ "location": {
46
+ "type": "string"
47
+ }
48
+ },
49
+ "required": [
50
+ "location"
51
+ ]
52
+ },
53
+ "style": "qwen_xml",
54
+ "any_order": false,
55
+ "max_whitespace_cnt": null,
56
+ "excludes": []
57
+ },
58
+ "end": "\n</function>\n</tool_call>"
59
+ },
60
+ {
61
+ "type": "tag",
62
+ "begin": "<tool_call>\n<function=add>\n",
63
+ "content": {
64
+ "type": "json_schema",
65
+ "json_schema": {
66
+ "type": "object",
67
+ "properties": {
68
+ "y": {
69
+ "type": "number"
70
+ },
71
+ "x": {
72
+ "type": "number"
73
+ },
74
+ "items": {
75
+ "type": "array",
76
+ "items": {
77
+ "type": "string"
78
+ }
79
+ }
80
+ },
81
+ "required": [
82
+ "y",
83
+ "x",
84
+ "items"
85
+ ]
86
+ },
87
+ "style": "qwen_xml",
88
+ "any_order": false,
89
+ "max_whitespace_cnt": null,
90
+ "excludes": []
91
+ },
92
+ "end": "\n</function>\n</tool_call>"
93
+ }
94
+ ],
95
+ "at_least_one": false,
96
+ "stop_after_first": false,
97
+ "excludes": [
98
+ "<think>",
99
+ "</think>"
100
+ ]
101
+ }
102
+ ]
103
+ },
104
+ "coverage": "from_token_zero"
105
+ },
106
+ "completion": "Need call the weather and add tools.\n</think>\n\nI will call the tools.\n\n<tool_call>\n<function=get_weather>\n<parameter=location>\nShanghai\n</parameter>\n</function>\n</tool_call>\n<tool_call>\n<function=add>\n<parameter=y>\n1.0\n</parameter>\n<parameter=x>\n2\n</parameter>\n<parameter=items>\n[\"left\", \"right\"]\n</parameter>\n</function>\n</tool_call>",
107
+ "generation_token_ids": [22916, 1562, 279, 8831, 321, 884, 7141, 13, 198, 248069, 271, 40, 668, 1562, 279, 7141, 13, 271, 248058, 198, 27, 1628, 27362, 67017, 29, 198, 27, 15704, 28, 2447, 29, 198, 1948, 29086, 198, 510, 15704, 29, 198, 510, 1628, 29, 198, 248059, 198, 248058, 198, 27, 1628, 89587, 29, 198, 27, 15704, 28408, 29, 198, 16, 13, 15, 198, 510, 15704, 29, 198, 27, 15704, 24582, 29, 198, 17, 198, 510, 15704, 29, 198, 27, 15704, 28, 3494, 29, 198, 1143, 2282, 487, 328, 1246, 1293, 198, 510, 15704, 29, 198, 510, 1628, 29, 198, 248059, 248046]
108
+ },
109
+ "required": {
110
+ "grammar": {
111
+ "format": {
112
+ "type": "sequence",
113
+ "elements": [
114
+ {
115
+ "type": "sequence",
116
+ "elements": [
117
+ {
118
+ "type": "tag",
119
+ "begin": "",
120
+ "content": {
121
+ "type": "any_text",
122
+ "excludes": [],
123
+ "max_tokens": null,
124
+ "max_chars": null
125
+ },
126
+ "end": "</think>"
127
+ },
128
+ {
129
+ "type": "const_string",
130
+ "value": "\n\n"
131
+ }
132
+ ]
133
+ },
134
+ {
135
+ "type": "triggered_tags",
136
+ "triggers": [
137
+ "<tool_call>\n<function="
138
+ ],
139
+ "tags": [
140
+ {
141
+ "type": "tag",
142
+ "begin": "<tool_call>\n<function=get_weather>\n",
143
+ "content": {
144
+ "type": "json_schema",
145
+ "json_schema": {
146
+ "type": "object",
147
+ "properties": {
148
+ "location": {
149
+ "type": "string"
150
+ }
151
+ },
152
+ "required": [
153
+ "location"
154
+ ]
155
+ },
156
+ "style": "qwen_xml",
157
+ "any_order": false,
158
+ "max_whitespace_cnt": null,
159
+ "excludes": []
160
+ },
161
+ "end": "\n</function>\n</tool_call>"
162
+ },
163
+ {
164
+ "type": "tag",
165
+ "begin": "<tool_call>\n<function=add>\n",
166
+ "content": {
167
+ "type": "json_schema",
168
+ "json_schema": {
169
+ "type": "object",
170
+ "properties": {
171
+ "y": {
172
+ "type": "number"
173
+ },
174
+ "x": {
175
+ "type": "number"
176
+ },
177
+ "items": {
178
+ "type": "array",
179
+ "items": {
180
+ "type": "string"
181
+ }
182
+ }
183
+ },
184
+ "required": [
185
+ "y",
186
+ "x",
187
+ "items"
188
+ ]
189
+ },
190
+ "style": "qwen_xml",
191
+ "any_order": false,
192
+ "max_whitespace_cnt": null,
193
+ "excludes": []
194
+ },
195
+ "end": "\n</function>\n</tool_call>"
196
+ }
197
+ ],
198
+ "at_least_one": true,
199
+ "stop_after_first": false,
200
+ "excludes": [
201
+ "<think>",
202
+ "</think>"
203
+ ]
204
+ }
205
+ ]
206
+ },
207
+ "coverage": "from_token_zero"
208
+ },
209
+ "completion": "Need call the weather and add tools.\n</think>\n\n<tool_call>\n<function=get_weather>\n<parameter=location>\nShanghai\n</parameter>\n</function>\n</tool_call>\n<tool_call>\n<function=add>\n<parameter=y>\n1.0\n</parameter>\n<parameter=x>\n2\n</parameter>\n<parameter=items>\n[\"left\", \"right\"]\n</parameter>\n</function>\n</tool_call>",
210
+ "generation_token_ids": [22916, 1562, 279, 8831, 321, 884, 7141, 13, 198, 248069, 271, 248058, 198, 27, 1628, 27362, 67017, 29, 198, 27, 15704, 28, 2447, 29, 198, 1948, 29086, 198, 510, 15704, 29, 198, 510, 1628, 29, 198, 248059, 198, 248058, 198, 27, 1628, 89587, 29, 198, 27, 15704, 28408, 29, 198, 16, 13, 15, 198, 510, 15704, 29, 198, 27, 15704, 24582, 29, 198, 17, 198, 510, 15704, 29, 198, 27, 15704, 28, 3494, 29, 198, 1143, 2282, 487, 328, 1246, 1293, 198, 510, 15704, 29, 198, 510, 1628, 29, 198, 248059, 248046]
211
+ },
212
+ "named": {
213
+ "grammar": {
214
+ "format": {
215
+ "type": "sequence",
216
+ "elements": [
217
+ {
218
+ "type": "sequence",
219
+ "elements": [
220
+ {
221
+ "type": "tag",
222
+ "begin": "",
223
+ "content": {
224
+ "type": "any_text",
225
+ "excludes": [],
226
+ "max_tokens": null,
227
+ "max_chars": null
228
+ },
229
+ "end": "</think>"
230
+ },
231
+ {
232
+ "type": "const_string",
233
+ "value": "\n\n"
234
+ }
235
+ ]
236
+ },
237
+ {
238
+ "type": "tag",
239
+ "begin": "<tool_call>\n<function=get_weather>\n",
240
+ "content": {
241
+ "type": "json_schema",
242
+ "json_schema": {
243
+ "type": "object",
244
+ "properties": {
245
+ "location": {
246
+ "type": "string"
247
+ }
248
+ },
249
+ "required": [
250
+ "location"
251
+ ]
252
+ },
253
+ "style": "qwen_xml",
254
+ "any_order": false,
255
+ "max_whitespace_cnt": null,
256
+ "excludes": []
257
+ },
258
+ "end": "\n</function>\n</tool_call>"
259
+ }
260
+ ]
261
+ },
262
+ "coverage": "from_token_zero"
263
+ },
264
+ "completion": "Need call the weather and add tools.\n</think>\n\n<tool_call>\n<function=get_weather>\n<parameter=location>\nShanghai\n</parameter>\n</function>\n</tool_call>",
265
+ "generation_token_ids": [22916, 1562, 279, 8831, 321, 884, 7141, 13, 198, 248069, 271, 248058, 198, 27, 1628, 27362, 67017, 29, 198, 27, 15704, 28, 2447, 29, 198, 1948, 29086, 198, 510, 15704, 29, 198, 510, 1628, 29, 198, 248059, 248046]
266
+ }
267
+ }
268
+}
rust/src/chat/tests/roundtrip.rs
+368
-90
@@ -6,20 +6,31 @@
6
//! The invariant under test is that a structured assistant message rendered as history can be
7
//! parsed from the generated assistant completion and then rendered back to the exact same
8
//! assistant completion.
9
-
9
+//!
10
+//! The tool-call fixture also snapshots the output grammar built by the initialized parser for each
11
+//! tool-choice variant, with the rendered completion as its generation, as one file per model under
12
+//! `tests/grammar_replay/`. `grammar_replay.rs` replays these cases through the real XGrammar.
13
+
14
+use std::collections::{BTreeMap, BTreeSet};
15
+use std::fmt::Debug;
16
+use std::ops::Range;
17
+use std::path::PathBuf;
18
use std::pin::Pin;
19
use std::sync::Arc;
20
21
use anyhow::{Context as _, Result, bail, ensure};
22
use futures::{Stream, StreamExt as _, stream};
23
+use serde::ser::Error as _;
24
+use serde::{Serialize, Serializer};
25
use serde_json_fmt::JsonFormat as JsonFmt;
26
use serial_test::file_serial;
27
use vllm_chat::{
28
AssistantContentBlock, AssistantMessage, AssistantMessageExt as _, AssistantToolCall,
19
- ChatEvent, ChatMessage, ChatRequest, ChatRole, ChatTool, FinishReason, GenerationPromptMode,
20
- LoadModelBackendsOptions, NewChatOutputProcessorOptions, ParserSelection, RendererSelection,
21
- load_model_backends,
29
+ ChatEvent, ChatMessage, ChatRequest, ChatRole, ChatTool, ChatToolChoice, FinishReason,
30
+ GenerationPromptMode, LoadModelBackendsOptions, NewChatOutputProcessorOptions, ParserSelection,
31
+ RendererSelection, ToolStrictLevel, load_model_backends,
32
};
33
+use vllm_parser::output_grammar::BuiltOutputGrammar;
34
use vllm_text::{DecodedTextEvent, Finished, Prompt};
35
use vllm_tokenizer::Tokenizer;
36
@@ -114,6 +125,33 @@ impl ThinkingBehavior {
125
}
126
}
127
128
+/// Tool-choice variant of the tool-call fixture.
129
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
130
+#[serde(rename_all = "snake_case")]
131
+enum ToolChoiceVariant {
132
+ /// `auto`, with content before the tool calls.
133
+ Auto,
134
+ /// `required`, with tool calls only: builder grammars admit no content
135
+ /// before the calls.
136
+ Required,
137
+ /// Named `get_weather`, with a single call to it and no content.
138
+ Named,
139
+}
140
+
141
+impl ToolChoiceVariant {
142
+ const ALL: [Self; 3] = [Self::Auto, Self::Required, Self::Named];
143
+
144
+ fn tool_choice(self) -> ChatToolChoice {
145
+ match self {
146
+ Self::Auto => ChatToolChoice::Auto,
147
+ Self::Required => ChatToolChoice::Required,
148
+ Self::Named => ChatToolChoice::Function {
149
+ name: "get_weather".to_string(),
150
+ },
151
+ }
152
+ }
153
+}
154
+
155
impl RoundtripCase {
156
/// Qwen3 XML tool-call format with `qwen3` reasoning tags.
157
fn qwen3() -> Self {
@@ -451,6 +489,7 @@ async fn run_roundtrip_reasoning_and_content_inner(
489
"roundtrip-reasoning-content",
490
vec![ChatMessage::text(ChatRole::User, "What is 2 + 2?")],
491
Vec::new(),
492
+ None,
493
thinking,
494
case.thinking_behavior,
495
);
@@ -489,11 +528,25 @@ async fn run_roundtrip_reasoning_and_content_inner(
528
Ok(())
529
}
530
492
-/// Run the fixed reasoning+multiple-tools fixture for one model/parser case.
531
+/// Run the fixed reasoning+multiple-tools fixture for one model/parser case,
532
+/// once per tool-choice variant.
533
async fn run_roundtrip_tool_call_mix(
534
case: &RoundtripCase,
535
backends: &vllm_chat::LoadedModelBackends,
536
) -> Result<()> {
537
+ let mut results = Vec::new();
538
+ for variant in ToolChoiceVariant::ALL {
539
+ let result = run_roundtrip_tool_call_mix_inner(case, backends, variant).await?;
540
+ results.push((variant, result));
541
+ }
542
+ check_grammar_replay_file(case, backends, results)
543
+}
544
+
545
+async fn run_roundtrip_tool_call_mix_inner(
546
+ case: &RoundtripCase,
547
+ backends: &vllm_chat::LoadedModelBackends,
548
+ variant: ToolChoiceVariant,
549
+) -> Result<RoundtripResult> {
550
let request = roundtrip_request(
551
"roundtrip-reasoning-tools",
552
vec![ChatMessage::text(
@@ -501,41 +554,40 @@ async fn run_roundtrip_tool_call_mix(
554
"Check Shanghai weather and add 1.0 plus 2.",
555
)],
556
test_tools(),
557
+ Some(variant.tool_choice()),
558
Some(true), // always enable thinking in this fixture
559
case.thinking_behavior,
560
);
561
let expected_reasoning = "Need call the weather and add tools.";
508
- let expected_text = "I will call the tools.";
509
-
510
- let result = run_roundtrip(
511
- case,
512
- backends,
513
- &request,
514
- AssistantMessage {
515
- content: vec![
516
- AssistantContentBlock::Reasoning {
517
- text: expected_reasoning.to_string(),
518
- },
519
- AssistantContentBlock::Text {
520
- text: expected_text.to_string(),
521
- },
522
- AssistantContentBlock::ToolCall(AssistantToolCall {
523
- id: "functions.get_weather:0".to_string(),
524
- name: "get_weather".to_string(),
525
- arguments: r#"{"location":"Shanghai"}"#.to_string(),
526
- }),
527
- AssistantContentBlock::ToolCall(AssistantToolCall {
528
- id: "functions.add:1".to_string(),
529
- name: "add".to_string(),
530
- // Intentionally use a non-lexical order of keys to verify text-level
531
- // fidelity of the roundtrip where JSON formatting remains stable. The
532
- // `items` key also exercises templates that call `arguments.items()`.
533
- arguments: r#"{"y":1.0,"x":2,"items":["left","right"]}"#.to_string(),
534
- }),
535
- ],
536
- },
537
- )
538
- .await?;
562
+ let (expected_text, tool_call_count) = match variant {
563
+ ToolChoiceVariant::Auto => (Some("I will call the tools."), 2),
564
+ ToolChoiceVariant::Required => (None, 2),
565
+ ToolChoiceVariant::Named => (None, 1),
566
+ };
567
+
568
+ let mut content = vec![AssistantContentBlock::Reasoning {
569
+ text: expected_reasoning.to_string(),
570
+ }];
571
+ content.extend(expected_text.map(|text| AssistantContentBlock::Text {
572
+ text: text.to_string(),
573
+ }));
574
+ let tool_calls = [
575
+ AssistantContentBlock::ToolCall(AssistantToolCall {
576
+ id: "functions.get_weather:0".to_string(),
577
+ name: "get_weather".to_string(),
578
+ arguments: r#"{"location":"Shanghai"}"#.to_string(),
579
+ }),
580
+ AssistantContentBlock::ToolCall(AssistantToolCall {
581
+ id: "functions.add:1".to_string(),
582
+ name: "add".to_string(),
583
+ // Intentionally use a non-lexical order of keys to verify text-level
584
+ // fidelity of the roundtrip where JSON formatting remains stable. The
585
+ // `items` key also exercises templates that call `arguments.items()`.
586
+ arguments: r#"{"y":1.0,"x":2,"items":["left","right"]}"#.to_string(),
587
+ }),
588
+ ];
589
+ content.extend(tool_calls.into_iter().take(tool_call_count));
590
+ let result = run_roundtrip(case, backends, &request, AssistantMessage { content }).await?;
591
592
assert_eq!(
593
result.parsed_message.reasoning().as_deref(),
@@ -543,12 +595,15 @@ async fn run_roundtrip_tool_call_mix(
595
"parsed message: {:#?}",
596
result.parsed_message
597
);
546
- assert_eq!(result.parsed_message.text(), expected_text);
598
+ assert_eq!(
599
+ result.parsed_message.text(),
600
+ expected_text.unwrap_or_default()
601
+ );
602
603
let tool_calls = result.parsed_message.tool_calls().collect::<Vec<_>>();
604
assert_eq!(
605
tool_calls.len(),
551
- 2,
606
+ tool_call_count,
607
"parsed message: {:#?}",
608
result.parsed_message
609
);
@@ -557,18 +612,20 @@ async fn run_roundtrip_tool_call_mix(
612
tool_calls[0].arguments,
613
expected_arguments(case, r#"{"location": "Shanghai"}"#)?,
614
);
560
- assert_eq!(tool_calls[1].name, "add");
561
- assert_eq!(
562
- tool_calls[1].arguments,
563
- expected_arguments(case, r#"{"y": 1.0, "x": 2, "items": ["left", "right"]}"#)?,
564
- );
615
+ if let Some(add) = tool_calls.get(1) {
616
+ assert_eq!(add.name, "add");
617
+ assert_eq!(
618
+ add.arguments,
619
+ expected_arguments(case, r#"{"y": 1.0, "x": 2, "items": ["left", "right"]}"#)?,
620
+ );
621
+ }
622
623
assert_eq!(
624
result.rerendered_closed_completion,
625
result.closed_completion
626
);
627
571
- Ok(())
628
+ Ok(result)
629
}
630
631
/// Compact JSON argument formatting used by JSON-native parsers/renderers.
@@ -645,6 +702,9 @@ struct RoundtripResult {
702
/// Assistant-completion suffix cut after rendering the parsed assistant
703
/// back as history.
704
rerendered_closed_completion: Prompt,
705
+ /// Output grammar built by the same initialized parser that parsed the
706
+ /// completion.
707
+ output_grammar: Option<BuiltOutputGrammar>,
708
}
709
710
/// Render, parse, and rerender one assistant turn through the production
@@ -658,13 +718,17 @@ async fn run_roundtrip(
718
let renderer = backends.chat_backend.chat_renderer();
719
let rendered = render_closed_completion(renderer.as_ref(), request, &assistant)?;
720
661
- let parsed_message = parse_completion(case, backends, request, &rendered).await?;
721
+ let ParsedCompletion {
722
+ message: parsed_message,
723
+ output_grammar,
724
+ } = parse_completion(case, backends, request, &rendered).await?;
725
let rerendered = render_closed_completion(renderer.as_ref(), request, &parsed_message)?;
726
727
Ok(RoundtripResult {
728
parsed_message,
729
closed_completion: rendered.completion,
730
rerendered_closed_completion: rerendered.completion,
731
+ output_grammar,
732
})
733
}
734
@@ -675,7 +739,7 @@ struct RenderedTurn {
739
}
740
741
/// Render `history` as a production prompt and `history + assistant` as closed
678
-/// history, then return the production prompt and assistant-completion suffix.
742
+/// history, then return the production prompt and the assistant completion.
743
fn render_closed_completion(
744
renderer: &dyn vllm_chat::ChatRenderer,
745
base_request: &ChatRequest,
@@ -692,18 +756,15 @@ fn render_closed_completion(
756
757
let completion = match (&prompt, full) {
758
(Prompt::Text(prompt), Prompt::Text(full)) => {
695
- ensure!(
696
- full.starts_with(prompt),
697
- "full prompt must extend production prompt\nprompt: {prompt:?}\nfull: {full:?}"
698
- );
699
- Prompt::Text(full[prompt.len()..].to_string())
759
+ let range = completion_range(prompt.as_bytes(), full.as_bytes())
760
+ .with_context(|| format!("prompt: {prompt:?}\nfull: {full:?}"))?;
761
+ let completion = full.get(range).context("completion splits a UTF-8 character")?;
762
+ Prompt::Text(completion.to_string())
763
}
764
(Prompt::TokenIds(prompt), Prompt::TokenIds(full)) => {
702
- ensure!(
703
- full.starts_with(prompt),
704
- "full prompt must extend production prompt\nprompt: {prompt:?}\nfull: {full:?}"
705
- );
706
- Prompt::TokenIds(full[prompt.len()..].to_vec())
765
+ let range = completion_range(prompt, &full)
766
+ .with_context(|| format!("prompt: {prompt:?}\nfull: {full:?}"))?;
767
+ Prompt::TokenIds(full[range].to_vec())
768
}
769
(prompt, full) => bail!("prompt kind changed between renders: {prompt:?} vs {full:?}"),
770
};
@@ -711,14 +772,81 @@ fn render_closed_completion(
772
Ok(RenderedTurn { prompt, completion })
773
}
774
775
+/// Locate the assistant completion in `full`, the history rendered with the
776
+/// assistant message, given `prompt`, the same history rendered for generation.
777
+///
778
+/// Some templates render request-scoped control messages after the whole
779
+/// history, such as Kimi K3's `tool_choice=required` message. With history `A`,
780
+/// control messages `C`, generation prompt `G`, and completion `B`, the renders
781
+/// are `prompt = A + C + G` and `full = A + G + B + C`. `C` is empty for most
782
+/// templates, which makes `prompt` a prefix of `full`.
783
+fn completion_range<T: PartialEq + Debug>(prompt: &[T], full: &[T]) -> Result<Range<usize>> {
784
+ if full.starts_with(prompt) {
785
+ return Ok(prompt.len()..full.len());
786
+ }
787
+ ensure!(
788
+ full.len() >= prompt.len(),
789
+ "full prompt is shorter than production prompt"
790
+ );
791
+
792
+ // `A` is a common prefix of both renders; try every split of the remaining
793
+ // `prompt` into `C + G` and require exactly one to fit `full`.
794
+ let common_prefix_len = prompt.iter().zip(full).take_while(|(p, f)| p == f).count();
795
+ let mut control_lens = Vec::new();
796
+ for history_len in 0..=common_prefix_len {
797
+ let rest = &prompt[history_len..];
798
+ for control_len in 1..=rest.len() {
799
+ let (control, generation) = rest.split_at(control_len);
800
+ if full[history_len..].starts_with(generation) && full.ends_with(control) {
801
+ control_lens.push(control_len);
802
+ }
803
+ }
804
+ }
805
+ let [control_len] = control_lens[..] else {
806
+ bail!(
807
+ "full prompt must be `A + G + B + C` for production prompt `A + C + G`; found {} splits",
808
+ control_lens.len()
809
+ );
810
+ };
811
+
812
+ Ok(prompt.len() - control_len..full.len() - control_len)
813
+}
814
+
815
+#[test]
816
+fn completion_range_skips_trailing_control_messages() {
817
+ let history = "<user>hi</user>";
818
+ let control = "<system>call tools</system>";
819
+ let generation = "<assistant>";
820
+ let completion = "call()</assistant>";
821
+
822
+ let prompt = format!("{history}{generation}");
823
+ let full = format!("{history}{generation}{completion}");
824
+ let range = completion_range(prompt.as_bytes(), full.as_bytes()).unwrap();
825
+ assert_eq!(&full[range], completion);
826
+
827
+ let prompt = format!("{history}{control}{generation}");
828
+ let full = format!("{history}{generation}{completion}{control}");
829
+ let range = completion_range(prompt.as_bytes(), full.as_bytes()).unwrap();
830
+ assert_eq!(&full[range], completion);
831
+}
832
+
833
+/// Output of one production output processor over a rendered completion.
834
+struct ParsedCompletion {
835
+ /// Terminal assistant message.
836
+ message: AssistantMessage,
837
+ /// Output grammar built by the processor after initialization.
838
+ output_grammar: Option<BuiltOutputGrammar>,
839
+}
840
+
841
/// Feed one rendered assistant completion body into the real output processor
715
-/// and collect its terminal assistant message.
842
+/// and collect its terminal assistant message, along with the output grammar
843
+/// the initialized processor builds for the request.
844
async fn parse_completion(
845
case: &RoundtripCase,
846
backends: &vllm_chat::LoadedModelBackends,
847
base_request: &ChatRequest,
848
rendered: &RenderedTurn,
721
-) -> Result<AssistantMessage> {
849
+) -> Result<ParsedCompletion> {
850
let tokenizer = backends.text_backend.tokenizer();
851
let prompt_token_ids = match &rendered.prompt {
852
Prompt::Text(prompt) => tokenizer
@@ -731,12 +859,15 @@ async fn parse_completion(
859
let mut processor = backends.chat_backend.new_chat_output_processor(
860
&mut request,
861
NewChatOutputProcessorOptions {
734
- tool_strict_level: vllm_chat::ToolStrictLevel::Auto,
862
+ // Pin every tool schema so that builder-backed parsers build an
863
+ // output grammar for replay. The strict level does not affect parsing.
864
+ tool_strict_level: ToolStrictLevel::Parameter,
865
tool_call_parser: &case.tool_call_parser,
866
reasoning_parser: &case.reasoning_parser,
867
},
868
)?;
869
processor.initialize(&prompt_token_ids)?;
870
+ let output_grammar = processor.build_output_grammar()?;
871
872
let decoded = decoded_completion_stream(
873
tokenizer.as_ref(),
@@ -748,7 +879,10 @@ async fn parse_completion(
879
880
while let Some(event) = events.next().await {
881
if let ChatEvent::Done { message, .. } = event? {
751
- return Ok(message);
882
+ return Ok(ParsedCompletion {
883
+ message,
884
+ output_grammar,
885
+ });
886
}
887
}
888
@@ -772,14 +906,8 @@ fn decoded_completion_stream(
906
prompt_logprobs: None,
907
}];
908
775
- let chunks = match completion {
776
- Prompt::Text(text) => {
777
- let body = text.strip_suffix(assistant_stop_suffix).with_context(|| {
778
- format!(
779
- "closed assistant completion did not end with {:?}: {:?}",
780
- assistant_stop_suffix, text
781
- )
782
- })?;
909
+ let chunks = match completion_body(tokenizer, completion, assistant_stop_suffix)? {
910
+ CompletionBody::Text(body) => {
911
// The engine returns token ids, so attribute the rendered text
912
// through the production decoder, then split it into small char
913
// chunks to exercise marker splits.
@@ -807,27 +935,12 @@ fn decoded_completion_stream(
935
})
936
.collect()
937
}
810
- Prompt::TokenIds(token_ids) => {
811
- let body = if assistant_stop_suffix.is_empty() {
812
- token_ids.as_slice()
813
- } else {
814
- let stop_token_ids = tokenizer
815
- .encode(assistant_stop_suffix, false)
816
- .context("failed to encode token-id completion stop suffix")?;
817
- token_ids.strip_suffix(stop_token_ids.as_slice()).with_context(|| {
818
- format!(
819
- "token-id completion did not end with {:?}: {:?}",
820
- assistant_stop_suffix, token_ids
821
- )
822
- })?
823
- };
824
- incremental_decode_chunks(
825
- tokenizer,
826
- &prompt_token_ids,
827
- body,
828
- TOKEN_COMPLETION_CHUNK_TOKENS,
829
- )?
830
- }
938
+ CompletionBody::TokenIds(body) => incremental_decode_chunks(
939
+ tokenizer,
940
+ &prompt_token_ids,
941
+ body,
942
+ TOKEN_COMPLETION_CHUNK_TOKENS,
943
+ )?,
944
};
945
946
if chunks.is_empty() {
@@ -868,6 +981,48 @@ fn decoded_completion_stream(
981
Ok(stream::iter(events).map(Ok).boxed())
982
}
983
984
+/// Generated part of a closed assistant completion, without the rendered stop
985
+/// suffix.
986
+enum CompletionBody<'a> {
987
+ Text(&'a str),
988
+ TokenIds(&'a [u32]),
989
+}
990
+
991
+/// Strip the rendered stop suffix from a closed assistant completion.
992
+fn completion_body<'a>(
993
+ tokenizer: &dyn Tokenizer,
994
+ completion: &'a Prompt,
995
+ assistant_stop_suffix: &str,
996
+) -> Result<CompletionBody<'a>> {
997
+ match completion {
998
+ Prompt::Text(text) => {
999
+ let body = text.strip_suffix(assistant_stop_suffix).with_context(|| {
1000
+ format!(
1001
+ "closed assistant completion did not end with {:?}: {:?}",
1002
+ assistant_stop_suffix, text
1003
+ )
1004
+ })?;
1005
+ Ok(CompletionBody::Text(body))
1006
+ }
1007
+ Prompt::TokenIds(token_ids) => {
1008
+ let body = if assistant_stop_suffix.is_empty() {
1009
+ token_ids.as_slice()
1010
+ } else {
1011
+ let stop_token_ids = tokenizer
1012
+ .encode(assistant_stop_suffix, false)
1013
+ .context("failed to encode token-id completion stop suffix")?;
1014
+ token_ids.strip_suffix(stop_token_ids.as_slice()).with_context(|| {
1015
+ format!(
1016
+ "token-id completion did not end with {:?}: {:?}",
1017
+ assistant_stop_suffix, token_ids
1018
+ )
1019
+ })?
1020
+ };
1021
+ Ok(CompletionBody::TokenIds(body))
1022
+ }
1023
+ }
1024
+}
1025
+
1026
/// One decoded completion chunk fed into the output processor.
1027
struct DecodedCompletionChunk {
1028
/// Attributed for token-id completions, as the production decoder emits it.
@@ -941,15 +1096,138 @@ fn incremental_decode_chunks(
1096
Ok(chunks)
1097
}
1098
1099
+/// Grammar replay cases of one model, replayed through the real XGrammar by
1100
+/// `grammar_replay.py`.
1101
+#[derive(Serialize)]
1102
+struct GrammarReplayFile {
1103
+ /// Hugging Face model id whose tokenizer the replay loads.
1104
+ model_id: &'static str,
1105
+ /// Model vocabulary size, which sizes the XGrammar token mask.
1106
+ vocab_size: usize,
1107
+ /// Request stop set, passed to the matcher as `override_stop_tokens`.
1108
+ #[serde(serialize_with = "serialize_one_line")]
1109
+ all_stop_token_ids: BTreeSet<u32>,
1110
+ /// Cases keyed by tool-choice variant.
1111
+ cases: BTreeMap<ToolChoiceVariant, GrammarReplayCase>,
1112
+}
1113
+
1114
+/// One parser-built output grammar with the generation it must accept.
1115
+#[derive(Serialize)]
1116
+struct GrammarReplayCase {
1117
+ /// Parser-built grammar; only token-zero grammars are replayed from the
1118
+ /// first generated token.
1119
+ grammar: BuiltOutputGrammar,
1120
+ /// Generated completion text, without the stop token.
1121
+ completion: String,
1122
+ /// Generated token IDs presented to the matcher, ending with a stop token.
1123
+ #[serde(serialize_with = "serialize_one_line")]
1124
+ generation_token_ids: Vec<u32>,
1125
+}
1126
+
1127
+/// Serialize a token ID list on one line inside the pretty-printed file.
1128
+fn serialize_one_line<S: Serializer>(
1129
+ token_ids: &impl Serialize,
1130
+ serializer: S,
1131
+) -> std::result::Result<S::Ok, S::Error> {
1132
+ let json = spaced_json_fmt().format_to_string(token_ids).map_err(S::Error::custom)?;
1133
+ serde_json::value::RawValue::from_string(json)
1134
+ .map_err(S::Error::custom)?
1135
+ .serialize(serializer)
1136
+}
1137
+
1138
+/// Directory of checked-in grammar replay files.
1139
+fn grammar_replay_dir() -> PathBuf {
1140
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/grammar_replay")
1141
+}
1142
+
1143
+/// Snapshot the output grammars of one model's roundtrips, keyed by variant,
1144
+/// as its grammar replay file, or check that no stale file remains when the
1145
+/// parser builds none.
1146
+fn check_grammar_replay_file(
1147
+ case: &RoundtripCase,
1148
+ backends: &vllm_chat::LoadedModelBackends,
1149
+ results: Vec<(ToolChoiceVariant, RoundtripResult)>,
1150
+) -> Result<()> {
1151
+ let model_name = case.model_id.rsplit_once('/').map_or(case.model_id, |(_, name)| name);
1152
+ let path = grammar_replay_dir().join(format!("{model_name}.json"));
1153
+ let results = results
1154
+ .into_iter()
1155
+ .filter_map(|(variant, result)| {
1156
+ Some((variant, result.output_grammar?, result.closed_completion))
1157
+ })
1158
+ .collect::<Vec<_>>();
1159
+ if results.is_empty() {
1160
+ ensure!(
1161
+ !path.exists(),
1162
+ "{} builds no output grammar, but grammar replay file {path:?} exists",
1163
+ case.model_id
1164
+ );
1165
+ return Ok(());
1166
+ }
1167
+
1168
+ let tokenizer = backends.text_backend.tokenizer();
1169
+ let hints = backends.text_backend.sampling_hints()?;
1170
+ // Lowered `_all_stop_token_ids` of a request without user stop tokens.
1171
+ let mut all_stop_token_ids = hints.extra_eos_token_ids;
1172
+ all_stop_token_ids.extend(hints.primary_eos_token_id);
1173
+
1174
+ // The generation ends with the first token of the rendered stop suffix, or
1175
+ // with the primary EOS for templates that render none.
1176
+ let stop_token_id = if case.assistant_stop_suffix.is_empty() {
1177
+ hints.primary_eos_token_id.context("model has no primary EOS token")?
1178
+ } else {
1179
+ tokenizer.encode(case.assistant_stop_suffix, false)?[0]
1180
+ };
1181
+ ensure!(
1182
+ all_stop_token_ids.contains(&stop_token_id),
1183
+ "stop token {stop_token_id} of {:?} is not in the stop set {all_stop_token_ids:?}",
1184
+ case.assistant_stop_suffix
1185
+ );
1186
+
1187
+ let mut cases = BTreeMap::new();
1188
+ for (variant, grammar, closed_completion) in results {
1189
+ let (completion, mut generation_token_ids) = match completion_body(
1190
+ tokenizer.as_ref(),
1191
+ &closed_completion,
1192
+ case.assistant_stop_suffix,
1193
+ )? {
1194
+ CompletionBody::Text(body) => (body.to_string(), tokenizer.encode(body, false)?),
1195
+ CompletionBody::TokenIds(body) => (tokenizer.decode(body, false)?, body.to_vec()),
1196
+ };
1197
+ generation_token_ids.push(stop_token_id);
1198
+ cases.insert(
1199
+ variant,
1200
+ GrammarReplayCase {
1201
+ grammar,
1202
+ completion,
1203
+ generation_token_ids,
1204
+ },
1205
+ );
1206
+ }
1207
+
1208
+ let file = GrammarReplayFile {
1209
+ model_id: case.model_id,
1210
+ vocab_size: backends.text_backend.model_vocab_size(),
1211
+ all_stop_token_ids,
1212
+ cases,
1213
+ };
1214
+ let mut json = serde_json::to_string_pretty(&file)?;
1215
+ json.push('\n');
1216
+ expect_test::expect_file![path].assert_eq(&json);
1217
+
1218
+ Ok(())
1219
+}
1220
+
1221
/// Build a chat request fixture with parser-enabling tool-choice semantics.
1222
fn roundtrip_request(
1223
request_id: impl Into<String>,
1224
messages: Vec<ChatMessage>,
1225
tools: Vec<ChatTool>,
1226
+ tool_choice: Option<ChatToolChoice>,
1227
thinking: Option<bool>,
1228
thinking_behavior: ThinkingBehavior,
1229
) -> ChatRequest {
952
- let tool_context = vllm_chat::ResolvedToolContext::new(&messages, tools, None, true)
1230
+ let tool_context = vllm_chat::ResolvedToolContext::new(&messages, tools, tool_choice, true)
1231
.expect("tool context should resolve");
1232
let mut request = ChatRequest {
1233
request_id: request_id.into(),
rust/src/parser/src/output_grammar.rs
+4
-2
@@ -5,6 +5,7 @@
5
6
use std::{fmt, str::FromStr};
7
8
+use serde::Serialize;
9
use serde_with::{DeserializeFromStr, SerializeDisplay};
10
use thiserror::Error;
11
use xgrammar_structural_tag::builders::{StructuralTagBuilder, StructuralTagOptions};
@@ -20,7 +21,8 @@ use crate::tool::Tool;
21
pub type Result<T> = std::result::Result<T, OutputGrammarError>;
22
23
/// How much of the generated stream an output grammar covers.
23
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25
+#[serde(rename_all = "snake_case")]
26
pub enum GrammarCoverage {
27
/// The grammar covers reasoning and advances from the first generated token.
28
FromTokenZero,
@@ -29,7 +31,7 @@ pub enum GrammarCoverage {
31
}
32
33
/// One parser-built output grammar and its generated-stream coverage.
32
-#[derive(Debug, Clone, PartialEq)]
34
+#[derive(Debug, Clone, PartialEq, Serialize)]
35
pub struct BuiltOutputGrammar {
36
/// Typed XGrammar structural-tag format.
37
pub format: Format,