[Rust Frontend] Use the MiMo structural-tag builder (#59148)

Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

Bugen Zhao committed Sep 30, 2026 at 09:34 UTC 8039d3cbfd55be2aa172d544042c53b4e966369d
4 files changed +34 -5
rust/Cargo.lock
+2 -2
@@ -6605,9 +6605,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
6605
6606 [[package]]
6607 name = "xgrammar-structural-tag"
6608 -version = "0.3.0+xgrammar.0.2.7.82505d0"
6608 +version = "0.4.1+xgrammar.0.2.8.9778737"
6609 source = "registry+https://github.com/rust-lang/crates.io-index"
6610 -checksum = "3864005575c2ab76057f789898f59fb9fa1378d07c5ceabc24764e012d0e90bb"
6610 +checksum = "06637fe6ff753ef929b4cea2933b9432f78335255c6931dc748957122b9b492c"
6611 dependencies = [
6612 "auto_impl",
6613 "serde",
rust/Cargo.toml
+1 -1
@@ -153,7 +153,7 @@ vllm-text = { path = "src/text" }
153 vllm-tokenizer = { path = "src/tokenizer" }
154 vllm-tracing = { path = "src/tracing" }
155 winnow = { version = "1.0.2", features = ["simd"] }
156 -xgrammar-structural-tag = "0.3.0"
156 +xgrammar-structural-tag = "0.4.1"
157 zeromq = { version = "0.6.0", default-features = false, features = [
158 "tokio-runtime",
159 "all-transport",
rust/src/parser/src/tool/mimo.rs
+30 -1
@@ -26,7 +26,8 @@ impl ToolParser for MiMoToolParser {
26 }
27
28 fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
29 - self.inner.structural_tag_builder()
29 + // MiMo's template emits compact tags without the newlines Qwen3-Coder uses.
30 + Some(xgrammar_structural_tag::Model::Mimo.builder())
31 }
32
33 fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
@@ -88,4 +89,32 @@ mod tests {
89 );
90 }
91 }
92 +
93 + #[test]
94 + fn structural_tag_uses_compact_mimo_tags() {
95 + use xgrammar_structural_tag::builders::StructuralTagOptions;
96 + use xgrammar_structural_tag::{
97 + FunctionDefinition, FunctionToolParam, ToolChoice, ToolParam, build_structural_tag,
98 + };
99 +
100 + let parser = MiMoToolParser::create(&[]).unwrap();
101 + let builder = parser.structural_tag_builder().expect("MiMo exposes a builder");
102 + let tools = [ToolParam::Function(FunctionToolParam::new(
103 + FunctionDefinition::new("get_weather"),
104 + ))];
105 + let tag = build_structural_tag(
106 + builder,
107 + &tools,
108 + ToolChoice::required(),
109 + StructuralTagOptions::default().with_reasoning(false),
110 + )
111 + .unwrap();
112 + let value = serde_json::to_value(tag).unwrap();
113 + assert_eq!(value["format"]["triggers"], json!(["<tool_call>"]));
114 + assert_eq!(
115 + value["format"]["tags"][0]["begin"],
116 + "<tool_call><function=get_weather>"
117 + );
118 + assert_eq!(value["format"]["tags"][0]["end"], "</function></tool_call>");
119 + }
120 }
rust/src/parser/src/unified/kimi_k3/structural_tag.rs
+1 -1
@@ -395,7 +395,7 @@ mod tests {
395 )
396 .unwrap();
397
398 - expect![[r##"{"type":"structural_tag","format":{"type":"sequence","elements":[{"type":"optional","content":{"type":"const_string","value":"<|open|>response<|sep|>"}},{"type":"tag","begin":"","content":{"type":"any_text","excludes":["<|open|>","<|close|>","<|end_of_msg|>"],"max_tokens":null,"max_chars":null},"end":"<|close|>response<|sep|>"},{"type":"tag","begin":"<|open|>tools<|sep|>","content":{"type":"tags_with_separator","tags":[{"type":"tag","begin":"<|open|>call tool=\"get_weather\" index=\"","content":{"type":"sequence","elements":[{"type":"regex","pattern":"[1-9][0-9]*"},{"type":"const_string","value":"\"<|sep|>"},{"type":"or","elements":[{"type":"plus","content":{"type":"or","elements":[{"type":"tag","begin":"<|open|>argument key=\"unit\" type=\"string\"<|sep|>","content":{"type":"or","elements":[{"type":"const_string","value":"celsius"},{"type":"const_string","value":"fahrenheit"}]},"end":"<|close|>argument<|sep|>"},{"type":"tag","begin":"<|open|>argument key=\"place\" type=\"object\"<|sep|>","content":{"type":"json_schema","json_schema":{"$ref":"#/$defs/place","type":"object","$defs":{"place":{"type":"object","properties":{"city":{"type":"string"}}}}},"style":"json","any_order":false,"max_whitespace_cnt":null},"end":"<|close|>argument<|sep|>"}]}},{"type":"tag","begin":"<|open|>json type=\"object\"<|sep|>","content":{"type":"json_schema","json_schema":{"$defs":{"place":{"type":"object","properties":{"city":{"type":"string"}}}},"type":"object","properties":{"unit":{"type":"string","enum":["celsius","fahrenheit"]},"place":{"$ref":"#/$defs/place","type":"object"}},"required":["place"]},"style":"json","any_order":false,"max_whitespace_cnt":null},"end":"<|close|>json<|sep|>"}]}]},"end":"<|close|>call<|sep|>"}],"separator":"","at_least_one":true,"stop_after_first":false},"end":"<|close|>tools<|sep|>"},{"type":"optional","content":{"type":"const_string","value":"<|close|>message<|sep|>"}}]}}"##]].assert_eq(&tag.to_json_string().unwrap());
398 + expect![[r##"{"type":"structural_tag","format":{"type":"sequence","elements":[{"type":"optional","content":{"type":"const_string","value":"<|open|>response<|sep|>"}},{"type":"tag","begin":"","content":{"type":"any_text","excludes":["<|open|>","<|close|>","<|end_of_msg|>"],"max_tokens":null,"max_chars":null},"end":"<|close|>response<|sep|>"},{"type":"tag","begin":"<|open|>tools<|sep|>","content":{"type":"tags_with_separator","tags":[{"type":"tag","begin":"<|open|>call tool=\"get_weather\" index=\"","content":{"type":"sequence","elements":[{"type":"regex","pattern":"[1-9][0-9]*"},{"type":"const_string","value":"\"<|sep|>"},{"type":"or","elements":[{"type":"plus","content":{"type":"or","elements":[{"type":"tag","begin":"<|open|>argument key=\"unit\" type=\"string\"<|sep|>","content":{"type":"or","elements":[{"type":"const_string","value":"celsius"},{"type":"const_string","value":"fahrenheit"}]},"end":"<|close|>argument<|sep|>"},{"type":"tag","begin":"<|open|>argument key=\"place\" type=\"object\"<|sep|>","content":{"type":"json_schema","json_schema":{"$ref":"#/$defs/place","type":"object","$defs":{"place":{"type":"object","properties":{"city":{"type":"string"}}}}},"style":"json","any_order":false,"max_whitespace_cnt":null,"excludes":[]},"end":"<|close|>argument<|sep|>"}]}},{"type":"tag","begin":"<|open|>json type=\"object\"<|sep|>","content":{"type":"json_schema","json_schema":{"$defs":{"place":{"type":"object","properties":{"city":{"type":"string"}}}},"type":"object","properties":{"unit":{"type":"string","enum":["celsius","fahrenheit"]},"place":{"$ref":"#/$defs/place","type":"object"}},"required":["place"]},"style":"json","any_order":false,"max_whitespace_cnt":null,"excludes":[]},"end":"<|close|>json<|sep|>"}]}]},"end":"<|close|>call<|sep|>"}],"separator":"","at_least_one":true,"stop_after_first":false},"end":"<|close|>tools<|sep|>"},{"type":"optional","content":{"type":"const_string","value":"<|close|>message<|sep|>"}}]}}"##]].assert_eq(&tag.to_json_string().unwrap());
399 }
400
401 #[test]