[Rust Frontend] Fix GLM-5.2 chat template rendering parity (#51426)
Co-authored-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com> Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Woosuk Kwon committed
Aug 17, 2026 at 20:16 UTC
e0e5a7fb2808504ba86c94f7b379e38496002fd0
5 files changed
+106
-6
rust/Cargo.lock
+4
-4
@@ -2402,9 +2402,9 @@ dependencies = [
2402
2403
[[package]]
2404
name = "minijinja"
2405
-version = "2.22.0"
2405
+version = "2.24.0"
2406
source = "registry+https://github.com/rust-lang/crates.io-index"
2407
-checksum = "ef84a52be188a1d4124bd717903fdde96ca4705f2b56adfe2d91fcc57fcb6987"
2407
+checksum = "86886cf6dbf4e614b19c9a1eec9775f021869d7eadde0fc73921a81b90c9b4c9"
2408
dependencies = [
2409
"indexmap 2.13.0",
2410
"memo-map",
@@ -2414,9 +2414,9 @@ dependencies = [
2414
2415
[[package]]
2416
name = "minijinja-contrib"
2417
-version = "2.22.0"
2417
+version = "2.24.0"
2418
source = "registry+https://github.com/rust-lang/crates.io-index"
2419
-checksum = "fd6e279dc925840c2d9ebc1e9f85410611d01292561297463ba3e516c3ad94dd"
2419
+checksum = "bd3e5f077bc2379f0f7d911e7cfdd921114ed99fc884533dca502944cb355b11"
2420
dependencies = [
2421
"minijinja",
2422
"serde",
rust/Cargo.toml
+2
-2
@@ -61,8 +61,8 @@ itertools = "0.14.0"
61
libc = "0.2.177"
62
llm-multimodal = { git = "https://github.com/smg-project/llm-multimodal", rev = "15adba5e025d8636ba4a334fb379b1371f6196a1", default-features = false, features = ["native-tls"] }
63
mimalloc = "0.1.52"
64
-minijinja = { version = "2.22", features = ["unstable_machinery", "json", "builtins", "loader", "loop_controls", "preserve_order"] }
65
-minijinja-contrib = { version = "2.22", features = ["pycompat"] }
64
+minijinja = { version = "2.24", features = ["unstable_machinery", "json", "builtins", "loader", "loop_controls", "preserve_order"] }
65
+minijinja-contrib = { version = "2.24", features = ["pycompat"] }
66
native-tls-vendored = { package = "native-tls", version = "0.2.18", features = ["vendored"] }
67
ndarray = { version = "0.17", features = ["serde"] }
68
openai-harmony = { package = "oss-harmony", git = "https://github.com/oss-harmony/harmony", tag = "v0.0.11", default-features = false }
rust/src/chat/src/renderer/hf/mod.rs
+66
@@ -290,6 +290,7 @@ struct TemplateToolDefinition {
290
name: String,
291
description: Option<String>,
292
parameters: JsonValue,
293
+ #[serde(skip_serializing_if = "Option::is_none")]
294
strict: Option<bool>,
295
}
296
@@ -1280,6 +1281,71 @@ mod tests {
1281
assert_eq!(rendered, "get_weather|city");
1282
}
1283
1284
+ #[test]
1285
+ fn chat_template_preserves_openai_tool_field_order() {
1286
+ let mut request = sample_request(vec![ChatMessage::text(ChatRole::User, "hello")]);
1287
+ let tools = vec![ChatTool {
1288
+ name: "get_weather".to_string(),
1289
+ description: Some("Get weather".to_string()),
1290
+ parameters: serde_json::json!({"type": "object"}),
1291
+ strict: None,
1292
+ }];
1293
+ request.tool_context = crate::request::ResolvedToolContext::new(
1294
+ &request.messages,
1295
+ tools,
1296
+ Some(ChatToolChoice::Auto),
1297
+ true,
1298
+ )
1299
+ .expect("tool context should resolve");
1300
+
1301
+ let rendered = render(
1302
+ Some("{% for key, value in tools[0].function.items() %}{{ key }}|{% endfor %}"),
1303
+ &request,
1304
+ )
1305
+ .unwrap();
1306
+
1307
+ assert_eq!(rendered, "name|description|parameters|");
1308
+ }
1309
+
1310
+ #[test]
1311
+ fn chat_template_preserves_python_optional_tool_fields() {
1312
+ let mut request = sample_request(vec![ChatMessage::text(ChatRole::User, "hello")]);
1313
+ let tools = vec![
1314
+ ChatTool {
1315
+ name: "without_strict".to_string(),
1316
+ description: None,
1317
+ parameters: Value::Null,
1318
+ strict: None,
1319
+ },
1320
+ ChatTool {
1321
+ name: "with_strict".to_string(),
1322
+ description: Some("description".to_string()),
1323
+ parameters: serde_json::json!({"type": "object"}),
1324
+ strict: Some(false),
1325
+ },
1326
+ ];
1327
+ request.tool_context = crate::request::ResolvedToolContext::new(
1328
+ &request.messages,
1329
+ tools,
1330
+ Some(ChatToolChoice::Auto),
1331
+ true,
1332
+ )
1333
+ .expect("tool context should resolve");
1334
+
1335
+ let rendered = render(
1336
+ Some(
1337
+ "{% for tool in tools %}{% for key, value in tool.function.items() %}{{ key }}={{ value|tojson }}|{% endfor %};{% endfor %}",
1338
+ ),
1339
+ &request,
1340
+ )
1341
+ .unwrap();
1342
+
1343
+ assert_eq!(
1344
+ rendered,
1345
+ "name=\"without_strict\"|description=null|parameters=null|;name=\"with_strict\"|description=\"description\"|parameters={\"type\": \"object\"}|strict=false|;"
1346
+ );
1347
+ }
1348
+
1349
#[test]
1350
fn chat_template_exposes_assistant_tool_calls_and_tool_messages() {
1351
let request = sample_request(vec![
rust/src/chat/src/renderer/hf/template.rs
+20
@@ -152,6 +152,26 @@ mod tests {
152
assert_eq!(result, "[]");
153
}
154
155
+ #[test]
156
+ fn test_midchain_dotted_integer_lookup() {
157
+ let template = CompiledChatTemplate::new(
158
+ "{{ values.0.name }}".to_string(),
159
+ ChatTemplateContentFormatOption::Auto,
160
+ )
161
+ .unwrap();
162
+ let mut kwargs = HashMap::new();
163
+ kwargs.insert("values".to_string(), serde_json::json!([{"name": "first"}]));
164
+
165
+ let result = template
166
+ .apply(TemplateContext {
167
+ template_kwargs: Some(&kwargs),
168
+ ..Default::default()
169
+ })
170
+ .unwrap();
171
+
172
+ assert_eq!(result, "first");
173
+ }
174
+
175
#[test]
176
fn test_chat_template_state_invalid_template() {
177
let result = CompiledChatTemplate::new(
rust/src/chat/tests/roundtrip.rs
+14
@@ -185,6 +185,19 @@ impl RoundtripCase {
185
}
186
}
187
188
+ /// GLM-5.2 XML-like argument format with `<think>` reasoning tags.
189
+ fn glm52() -> Self {
190
+ Self {
191
+ model_id: "zai-org/GLM-5.2-FP8",
192
+ assistant_stop_suffix: "",
193
+ tool_call_parser: ParserSelection::Auto,
194
+ reasoning_parser: ParserSelection::Auto,
195
+ thinking_behavior: ThinkingBehavior::Toggleable { default: true },
196
+ json_fmt: compact_json_fmt(),
197
+ sort_json_keys: false,
198
+ }
199
+ }
200
+
201
/// Gemma4 channel reasoning with custom function-call arguments.
202
fn gemma4() -> Self {
203
Self {
@@ -324,6 +337,7 @@ roundtrip_tests! {
337
deepseek_v32 => [tool_call_mix],
338
glm45 => [reasoning_and_content, tool_call_mix],
339
glm47 => [reasoning_and_content, tool_call_mix],
340
+ glm52 => [reasoning_and_content, tool_call_mix],
341
seed_oss => [reasoning_and_content, tool_call_mix],
342
step3p5 => [reasoning_and_content],
343
nemotron_v3 => [reasoning_and_content],