1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use openai_protocol::common::Tool;
5use regex::Regex;
6use serde_json::Value;
7
8use crate::{
9 errors::{ParserError, ParserResult},
10 parsers::helpers,
11 traits::ToolParser,
12 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
13};
14
15pub struct Glm4MoeParser {
26 tool_call_extractor: Regex,
28 func_detail_extractor: Regex,
30 arg_extractor: Regex,
32
33 buffer: String,
35
36 prev_tool_call_arr: Vec<Value>,
38
39 current_tool_id: i32,
41
42 streamed_args_for_tool: Vec<String>,
44
45 bot_token: &'static str,
47 eot_token: &'static str,
48}
49
50impl Glm4MoeParser {
51 #[expect(
58 clippy::expect_used,
59 reason = "regex patterns are compile-time string literals"
60 )]
61 pub(crate) fn new(func_detail_pattern: &str) -> Self {
62 let tool_call_pattern = r"(?s)<tool_call>.*?</tool_call>";
64 let tool_call_extractor = Regex::new(tool_call_pattern).expect("Valid regex pattern");
65
66 let func_detail_extractor = Regex::new(func_detail_pattern).expect("Valid regex pattern");
67
68 let arg_pattern = r"(?s)<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>";
69 let arg_extractor = Regex::new(arg_pattern).expect("Valid regex pattern");
70
71 Self {
72 tool_call_extractor,
73 func_detail_extractor,
74 arg_extractor,
75 buffer: String::new(),
76 prev_tool_call_arr: Vec::new(),
77 current_tool_id: -1,
78 streamed_args_for_tool: Vec::new(),
79 bot_token: "<tool_call>",
80 eot_token: "</tool_call>",
81 }
82 }
83
84 pub fn glm45() -> Self {
86 Self::new(r"(?s)<tool_call>([^\n]*)\n(.*)</tool_call>")
87 }
88
89 pub fn glm47() -> Self {
91 Self::new(r"(?s)<tool_call>\s*([^<\s]+)\s*(.*?)</tool_call>")
92 }
93
94 pub fn build_structural_tag(tools: &[Tool], at_least_one: bool) -> Value {
122 let tags: Vec<Value> = tools
123 .iter()
124 .filter(|tool| !tool.function.name.is_empty())
125 .map(|tool| {
126 serde_json::json!({
127 "type": "tag",
128 "begin": format!("<tool_call>{}", tool.function.name),
129 "content": {
130 "type": "json_schema",
131 "json_schema": tool.function.parameters,
132 "style": "glm_xml",
133 },
134 "end": "</tool_call>",
135 })
136 })
137 .collect();
138 serde_json::json!({
139 "format": {
140 "type": "triggered_tags",
141 "triggers": ["<tool_call>"],
142 "tags": tags,
143 "at_least_one": at_least_one,
144 }
145 })
146 }
147
148 pub fn reasoning_prefix() -> Value {
155 serde_json::json!({
156 "type": "tag",
157 "begin": "",
158 "content": {
159 "type": "any_text",
160 "excludes": [
161 "<think>", "</think>",
162 "<tool_call>", "</tool_call>",
163 "<arg_key>", "</arg_key>", "<arg_value>", "</arg_value>",
164 ],
165 },
166 "end": "</think>",
167 })
168 }
169
170 fn parse_arguments(
173 &self,
174 args_text: &str,
175 param_types: &HashMap<String, String>,
176 ) -> serde_json::Map<String, Value> {
177 let mut arguments = serde_json::Map::new();
178
179 for capture in self.arg_extractor.captures_iter(args_text) {
180 let key = capture.get(1).map_or("", |m| m.as_str()).trim();
181 let value_str = capture.get(2).map_or("", |m| m.as_str()).trim();
182
183 let value =
184 helpers::coerce_by_schema_type(value_str, param_types.get(key).map(String::as_str))
185 .unwrap_or_else(|| infer_value(value_str));
186
187 arguments.insert(key.to_string(), value);
188 }
189
190 arguments
191 }
192
193 fn parse_tool_call(&self, block: &str, tools: &[Tool]) -> ParserResult<Option<ToolCall>> {
195 if let Some(captures) = self.func_detail_extractor.captures(block) {
196 let func_name = captures.get(1).map_or("", |m| m.as_str()).trim();
198
199 let args_text = captures.get(2).map_or("", |m| m.as_str());
201
202 let param_types = helpers::param_types_for_function(tools, func_name);
203 let arguments = self.parse_arguments(args_text, ¶m_types);
204
205 let arguments_str = serde_json::to_string(&arguments)
206 .map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
207
208 Ok(Some(ToolCall {
209 function: FunctionCall {
210 name: func_name.to_string(),
211 arguments: arguments_str,
212 },
213 }))
214 } else {
215 Ok(None)
216 }
217 }
218
219 fn parse_tool_calls_from_text(&self, text: &str, tools: &[Tool]) -> Vec<ToolCall> {
221 let mut parsed = Vec::new();
222
223 for mat in self.tool_call_extractor.find_iter(text) {
224 match self.parse_tool_call(mat.as_str(), tools) {
225 Ok(Some(tool)) => parsed.push(tool),
226 Ok(None) => continue,
227 Err(e) => {
228 tracing::debug!("Failed to parse tool call: {}", e);
229 continue;
230 }
231 }
232 }
233
234 parsed
235 }
236}
237
238impl Glm4MoeParser {
239 fn parse_complete_inner(
241 &self,
242 text: &str,
243 tools: &[Tool],
244 ) -> ParserResult<(String, Vec<ToolCall>)> {
245 if !self.has_tool_markers(text) {
246 return Ok((text.to_string(), vec![]));
247 }
248
249 let idx = text
252 .find("<tool_call>")
253 .ok_or_else(|| ParserError::ParsingFailed("tool call marker not found".to_string()))?;
254 let normal_text = text[..idx].to_string();
255
256 let parsed = self.parse_tool_calls_from_text(text, tools);
257
258 if parsed.is_empty() {
260 return Ok((text.to_string(), vec![]));
261 }
262
263 Ok((normal_text, parsed))
264 }
265}
266
267fn infer_value(value_str: &str) -> Value {
270 if let Ok(json_val) = serde_json::from_str::<Value>(value_str) {
271 return json_val;
272 }
273 match value_str {
274 "true" | "True" => Value::Bool(true),
275 "false" | "False" => Value::Bool(false),
276 "null" | "None" => Value::Null,
277 _ => {
278 if let Ok(num) = value_str.parse::<i64>() {
279 Value::Number(num.into())
280 } else if let Ok(num) = value_str.parse::<f64>() {
281 serde_json::Number::from_f64(num)
282 .map_or_else(|| Value::String(value_str.to_string()), Value::Number)
283 } else {
284 Value::String(value_str.to_string())
285 }
286 }
287 }
288}
289
290impl Default for Glm4MoeParser {
291 fn default() -> Self {
292 Self::glm45()
293 }
294}
295
296#[async_trait]
297impl ToolParser for Glm4MoeParser {
298 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
299 self.parse_complete_inner(text, &[])
300 }
301
302 async fn parse_complete_with_tools(
303 &self,
304 text: &str,
305 tools: &[Tool],
306 ) -> ParserResult<(String, Vec<ToolCall>)> {
307 self.parse_complete_inner(text, tools)
308 }
309
310 async fn parse_incremental(
311 &mut self,
312 chunk: &str,
313 tools: &[Tool],
314 ) -> ParserResult<StreamingParseResult> {
315 self.buffer.push_str(chunk);
317 let current_text = &self.buffer.clone();
318
319 let start = current_text.find(self.bot_token);
321 if start.is_none() {
322 self.buffer.clear();
323 let normal_text = if self.current_tool_id > 0 {
325 String::new()
326 } else {
327 current_text.clone()
328 };
329 return Ok(StreamingParseResult {
330 normal_text,
331 calls: vec![],
332 });
333 }
334
335 let end = current_text.find(self.eot_token);
337 if let Some(end_pos) = end {
338 if self.current_tool_id == -1 {
342 self.current_tool_id = 0;
343 self.prev_tool_call_arr = Vec::new();
344 self.streamed_args_for_tool = vec![String::new()];
345 }
346
347 helpers::ensure_capacity(
349 self.current_tool_id,
350 &mut self.prev_tool_call_arr,
351 &mut self.streamed_args_for_tool,
352 );
353
354 let block_end = end_pos + self.eot_token.len();
356 let parsed_tools = self.parse_tool_calls_from_text(¤t_text[..block_end], tools);
357
358 let idx = current_text.find(self.bot_token);
360 let normal_text = if let Some(pos) = idx {
361 current_text[..pos].trim().to_string()
362 } else {
363 String::new()
364 };
365
366 let tool_indices = helpers::get_tool_indices(tools);
368
369 let mut calls = Vec::new();
370
371 if !parsed_tools.is_empty() {
372 let tool_call = &parsed_tools[0];
374 let tool_id = self.current_tool_id as usize;
375
376 if !tool_indices.contains_key(&tool_call.function.name) {
378 tracing::debug!("Invalid tool name '{}' - skipping", tool_call.function.name);
380 helpers::reset_current_tool_state(
381 &mut self.buffer,
382 &mut false, &mut self.streamed_args_for_tool,
384 &self.prev_tool_call_arr,
385 );
386 return Ok(StreamingParseResult::default());
387 }
388
389 calls.push(ToolCallItem {
390 tool_index: tool_id,
391 name: Some(tool_call.function.name.clone()),
392 parameters: tool_call.function.arguments.clone(),
393 });
394
395 if self.prev_tool_call_arr.len() <= tool_id {
397 self.prev_tool_call_arr
398 .resize_with(tool_id + 1, || Value::Null);
399 }
400
401 if let Ok(args) = serde_json::from_str::<Value>(&tool_call.function.arguments) {
403 self.prev_tool_call_arr[tool_id] = serde_json::json!({
404 "name": tool_call.function.name,
405 "arguments": args,
406 });
407 }
408
409 if self.streamed_args_for_tool.len() <= tool_id {
410 self.streamed_args_for_tool
411 .resize_with(tool_id + 1, String::new);
412 }
413 self.streamed_args_for_tool[tool_id].clone_from(&tool_call.function.arguments);
414
415 self.current_tool_id += 1;
416 }
417
418 self.buffer = current_text[block_end..].to_string();
420 return Ok(StreamingParseResult { normal_text, calls });
421 }
422
423 let Some(start_pos) = start else {
426 return Ok(StreamingParseResult::default());
427 };
428 let normal_text = current_text[..start_pos].to_string();
429 self.buffer = current_text[start_pos..].to_string();
430
431 Ok(StreamingParseResult {
432 normal_text,
433 calls: vec![],
434 })
435 }
436
437 fn has_tool_markers(&self, text: &str) -> bool {
438 text.contains(self.bot_token)
439 }
440
441 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
442 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
443 }
444
445 fn reset(&mut self) {
446 self.buffer.clear();
447 self.prev_tool_call_arr.clear();
448 self.current_tool_id = -1;
449 self.streamed_args_for_tool.clear();
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use openai_protocol::common::Function;
456
457 use super::*;
458
459 fn tool(name: &str, strict: Option<bool>, parameters: Value) -> Tool {
460 Tool {
461 tool_type: "function".to_string(),
462 function: Function {
463 name: name.to_string(),
464 description: None,
465 parameters,
466 strict,
467 },
468 }
469 }
470
471 #[test]
475 fn structural_tag_mirrors_the_xgrammar_glm_4_7_model() {
476 let schema =
477 serde_json::json!({"type": "object", "properties": {"city": {"type": "string"}}});
478 let tools = vec![
479 tool("get_weather", None, schema.clone()),
480 tool("lookup", Some(false), schema.clone()),
481 tool("ping", None, serde_json::json!({})),
482 tool("", None, schema.clone()),
483 ];
484 let tag = Glm4MoeParser::build_structural_tag(&tools, true);
485 let format = &tag["format"];
486 assert_eq!(format["type"], "triggered_tags");
487 assert_eq!(format["triggers"], serde_json::json!(["<tool_call>"]));
488 assert_eq!(format["at_least_one"], true);
489 let tags = format["tags"].as_array().unwrap();
490 assert_eq!(tags.len(), 3, "a nameless tool has no tag");
491 assert_eq!(tags[0]["begin"], "<tool_call>get_weather");
492 assert_eq!(tags[0]["end"], "</tool_call>");
493 assert_eq!(tags[0]["content"]["type"], "json_schema");
494 assert_eq!(tags[0]["content"]["style"], "glm_xml");
495 assert_eq!(tags[0]["content"]["json_schema"], schema);
496 assert_eq!(
497 tags[1]["content"]["json_schema"], schema,
498 "strict is not a constraint switch here, as in the other builders"
499 );
500 assert_eq!(
501 tags[2]["content"]["json_schema"],
502 serde_json::json!({}),
503 "a no-argument tool keeps its empty schema"
504 );
505
506 let tag = Glm4MoeParser::build_structural_tag(&tools, false);
507 assert_eq!(tag["format"]["at_least_one"], false);
508 }
509
510 #[test]
511 fn reasoning_prefix_mirrors_the_xgrammar_glm_4_7_reasoning_block() {
512 let prefix = Glm4MoeParser::reasoning_prefix();
513 assert_eq!(prefix["type"], "tag");
514 assert_eq!(prefix["begin"], "");
515 assert_eq!(prefix["end"], "</think>");
516 assert_eq!(prefix["content"]["type"], "any_text");
517 let excludes = prefix["content"]["excludes"].as_array().unwrap();
518 for token in [
519 "<think>",
520 "</think>",
521 "<tool_call>",
522 "</tool_call>",
523 "<arg_key>",
524 "</arg_key>",
525 "<arg_value>",
526 "</arg_value>",
527 ] {
528 assert!(
529 excludes.contains(&Value::String(token.to_string())),
530 "{token} must not appear inside the reasoning block"
531 );
532 }
533 }
534
535 fn tool_with_props(props: Value) -> Vec<Tool> {
536 vec![Tool {
537 tool_type: "function".to_string(),
538 function: Function {
539 name: "f".to_string(),
540 description: None,
541 parameters: serde_json::json!({"type": "object", "properties": props}),
542 strict: None,
543 },
544 }]
545 }
546
547 #[tokio::test]
549 async fn test_schema_aware_coercion_keeps_strings() {
550 let tools = tool_with_props(serde_json::json!({
551 "limit": {"type": "string"},
552 "flag": {"type": "string"},
553 "coords": {"type": "string"},
554 "count": {"type": "integer"},
555 }));
556 let text = "<tool_call>f\n\
557 <arg_key>limit</arg_key>\n<arg_value>4</arg_value>\n\
558 <arg_key>flag</arg_key>\n<arg_value>true</arg_value>\n\
559 <arg_key>coords</arg_key>\n<arg_value>[60,30]</arg_value>\n\
560 <arg_key>count</arg_key>\n<arg_value>5</arg_value>\n\
561 </tool_call>";
562 let (_, calls) = Glm4MoeParser::glm45()
563 .parse_complete_with_tools(text, &tools)
564 .await
565 .unwrap();
566 assert_eq!(calls.len(), 1);
567 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
568 assert_eq!(args["limit"], Value::String("4".to_string()));
569 assert_eq!(args["flag"], Value::String("true".to_string()));
570 assert_eq!(args["coords"], Value::String("[60,30]".to_string()));
571 assert_eq!(args["count"], Value::Number(5.into()));
572 }
573
574 #[tokio::test]
576 async fn test_streaming_schema_aware_coercion() {
577 let tools = tool_with_props(serde_json::json!({
578 "limit": {"type": "string"},
579 "count": {"type": "integer"},
580 }));
581 let text = "<tool_call>f\n\
582 <arg_key>limit</arg_key>\n<arg_value>4</arg_value>\n\
583 <arg_key>count</arg_key>\n<arg_value>5</arg_value>\n\
584 </tool_call>";
585 let result = Glm4MoeParser::glm45()
586 .parse_incremental(text, &tools)
587 .await
588 .unwrap();
589 let args: Value = serde_json::from_str(&result.calls[0].parameters).unwrap();
590 assert_eq!(args["limit"], Value::String("4".to_string()));
591 assert_eq!(args["count"], Value::Number(5.into()));
592 }
593}