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 fn parse_arguments(
97 &self,
98 args_text: &str,
99 param_types: &HashMap<String, String>,
100 ) -> serde_json::Map<String, Value> {
101 let mut arguments = serde_json::Map::new();
102
103 for capture in self.arg_extractor.captures_iter(args_text) {
104 let key = capture.get(1).map_or("", |m| m.as_str()).trim();
105 let value_str = capture.get(2).map_or("", |m| m.as_str()).trim();
106
107 let value =
108 helpers::coerce_by_schema_type(value_str, param_types.get(key).map(String::as_str))
109 .unwrap_or_else(|| infer_value(value_str));
110
111 arguments.insert(key.to_string(), value);
112 }
113
114 arguments
115 }
116
117 fn parse_tool_call(&self, block: &str, tools: &[Tool]) -> ParserResult<Option<ToolCall>> {
119 if let Some(captures) = self.func_detail_extractor.captures(block) {
120 let func_name = captures.get(1).map_or("", |m| m.as_str()).trim();
122
123 let args_text = captures.get(2).map_or("", |m| m.as_str());
125
126 let param_types = helpers::param_types_for_function(tools, func_name);
127 let arguments = self.parse_arguments(args_text, ¶m_types);
128
129 let arguments_str = serde_json::to_string(&arguments)
130 .map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
131
132 Ok(Some(ToolCall {
133 function: FunctionCall {
134 name: func_name.to_string(),
135 arguments: arguments_str,
136 },
137 }))
138 } else {
139 Ok(None)
140 }
141 }
142
143 fn parse_tool_calls_from_text(&self, text: &str, tools: &[Tool]) -> Vec<ToolCall> {
145 let mut parsed = Vec::new();
146
147 for mat in self.tool_call_extractor.find_iter(text) {
148 match self.parse_tool_call(mat.as_str(), tools) {
149 Ok(Some(tool)) => parsed.push(tool),
150 Ok(None) => continue,
151 Err(e) => {
152 tracing::debug!("Failed to parse tool call: {}", e);
153 continue;
154 }
155 }
156 }
157
158 parsed
159 }
160}
161
162impl Glm4MoeParser {
163 fn parse_complete_inner(
165 &self,
166 text: &str,
167 tools: &[Tool],
168 ) -> ParserResult<(String, Vec<ToolCall>)> {
169 if !self.has_tool_markers(text) {
170 return Ok((text.to_string(), vec![]));
171 }
172
173 let idx = text
176 .find("<tool_call>")
177 .ok_or_else(|| ParserError::ParsingFailed("tool call marker not found".to_string()))?;
178 let normal_text = text[..idx].to_string();
179
180 let parsed = self.parse_tool_calls_from_text(text, tools);
181
182 if parsed.is_empty() {
184 return Ok((text.to_string(), vec![]));
185 }
186
187 Ok((normal_text, parsed))
188 }
189}
190
191fn infer_value(value_str: &str) -> Value {
194 if let Ok(json_val) = serde_json::from_str::<Value>(value_str) {
195 return json_val;
196 }
197 match value_str {
198 "true" | "True" => Value::Bool(true),
199 "false" | "False" => Value::Bool(false),
200 "null" | "None" => Value::Null,
201 _ => {
202 if let Ok(num) = value_str.parse::<i64>() {
203 Value::Number(num.into())
204 } else if let Ok(num) = value_str.parse::<f64>() {
205 serde_json::Number::from_f64(num)
206 .map_or_else(|| Value::String(value_str.to_string()), Value::Number)
207 } else {
208 Value::String(value_str.to_string())
209 }
210 }
211 }
212}
213
214impl Default for Glm4MoeParser {
215 fn default() -> Self {
216 Self::glm45()
217 }
218}
219
220#[async_trait]
221impl ToolParser for Glm4MoeParser {
222 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
223 self.parse_complete_inner(text, &[])
224 }
225
226 async fn parse_complete_with_tools(
227 &self,
228 text: &str,
229 tools: &[Tool],
230 ) -> ParserResult<(String, Vec<ToolCall>)> {
231 self.parse_complete_inner(text, tools)
232 }
233
234 async fn parse_incremental(
235 &mut self,
236 chunk: &str,
237 tools: &[Tool],
238 ) -> ParserResult<StreamingParseResult> {
239 self.buffer.push_str(chunk);
241 let current_text = &self.buffer.clone();
242
243 let start = current_text.find(self.bot_token);
245 if start.is_none() {
246 self.buffer.clear();
247 let normal_text = if self.current_tool_id > 0 {
249 String::new()
250 } else {
251 current_text.clone()
252 };
253 return Ok(StreamingParseResult {
254 normal_text,
255 calls: vec![],
256 });
257 }
258
259 let end = current_text.find(self.eot_token);
261 if let Some(end_pos) = end {
262 if self.current_tool_id == -1 {
266 self.current_tool_id = 0;
267 self.prev_tool_call_arr = Vec::new();
268 self.streamed_args_for_tool = vec![String::new()];
269 }
270
271 helpers::ensure_capacity(
273 self.current_tool_id,
274 &mut self.prev_tool_call_arr,
275 &mut self.streamed_args_for_tool,
276 );
277
278 let block_end = end_pos + self.eot_token.len();
280 let parsed_tools = self.parse_tool_calls_from_text(¤t_text[..block_end], tools);
281
282 let idx = current_text.find(self.bot_token);
284 let normal_text = if let Some(pos) = idx {
285 current_text[..pos].trim().to_string()
286 } else {
287 String::new()
288 };
289
290 let tool_indices = helpers::get_tool_indices(tools);
292
293 let mut calls = Vec::new();
294
295 if !parsed_tools.is_empty() {
296 let tool_call = &parsed_tools[0];
298 let tool_id = self.current_tool_id as usize;
299
300 if !tool_indices.contains_key(&tool_call.function.name) {
302 tracing::debug!("Invalid tool name '{}' - skipping", tool_call.function.name);
304 helpers::reset_current_tool_state(
305 &mut self.buffer,
306 &mut false, &mut self.streamed_args_for_tool,
308 &self.prev_tool_call_arr,
309 );
310 return Ok(StreamingParseResult::default());
311 }
312
313 calls.push(ToolCallItem {
314 tool_index: tool_id,
315 name: Some(tool_call.function.name.clone()),
316 parameters: tool_call.function.arguments.clone(),
317 });
318
319 if self.prev_tool_call_arr.len() <= tool_id {
321 self.prev_tool_call_arr
322 .resize_with(tool_id + 1, || Value::Null);
323 }
324
325 if let Ok(args) = serde_json::from_str::<Value>(&tool_call.function.arguments) {
327 self.prev_tool_call_arr[tool_id] = serde_json::json!({
328 "name": tool_call.function.name,
329 "arguments": args,
330 });
331 }
332
333 if self.streamed_args_for_tool.len() <= tool_id {
334 self.streamed_args_for_tool
335 .resize_with(tool_id + 1, String::new);
336 }
337 self.streamed_args_for_tool[tool_id].clone_from(&tool_call.function.arguments);
338
339 self.current_tool_id += 1;
340 }
341
342 self.buffer = current_text[block_end..].to_string();
344 return Ok(StreamingParseResult { normal_text, calls });
345 }
346
347 let Some(start_pos) = start else {
350 return Ok(StreamingParseResult::default());
351 };
352 let normal_text = current_text[..start_pos].to_string();
353 self.buffer = current_text[start_pos..].to_string();
354
355 Ok(StreamingParseResult {
356 normal_text,
357 calls: vec![],
358 })
359 }
360
361 fn has_tool_markers(&self, text: &str) -> bool {
362 text.contains(self.bot_token)
363 }
364
365 fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
366 helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
367 }
368
369 fn reset(&mut self) {
370 self.buffer.clear();
371 self.prev_tool_call_arr.clear();
372 self.current_tool_id = -1;
373 self.streamed_args_for_tool.clear();
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use openai_protocol::common::Function;
380
381 use super::*;
382
383 fn tool_with_props(props: Value) -> Vec<Tool> {
384 vec![Tool {
385 tool_type: "function".to_string(),
386 function: Function {
387 name: "f".to_string(),
388 description: None,
389 parameters: serde_json::json!({"type": "object", "properties": props}),
390 strict: None,
391 },
392 }]
393 }
394
395 #[tokio::test]
397 async fn test_schema_aware_coercion_keeps_strings() {
398 let tools = tool_with_props(serde_json::json!({
399 "limit": {"type": "string"},
400 "flag": {"type": "string"},
401 "coords": {"type": "string"},
402 "count": {"type": "integer"},
403 }));
404 let text = "<tool_call>f\n\
405 <arg_key>limit</arg_key>\n<arg_value>4</arg_value>\n\
406 <arg_key>flag</arg_key>\n<arg_value>true</arg_value>\n\
407 <arg_key>coords</arg_key>\n<arg_value>[60,30]</arg_value>\n\
408 <arg_key>count</arg_key>\n<arg_value>5</arg_value>\n\
409 </tool_call>";
410 let (_, calls) = Glm4MoeParser::glm45()
411 .parse_complete_with_tools(text, &tools)
412 .await
413 .unwrap();
414 assert_eq!(calls.len(), 1);
415 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
416 assert_eq!(args["limit"], Value::String("4".to_string()));
417 assert_eq!(args["flag"], Value::String("true".to_string()));
418 assert_eq!(args["coords"], Value::String("[60,30]".to_string()));
419 assert_eq!(args["count"], Value::Number(5.into()));
420 }
421
422 #[tokio::test]
424 async fn test_streaming_schema_aware_coercion() {
425 let tools = tool_with_props(serde_json::json!({
426 "limit": {"type": "string"},
427 "count": {"type": "integer"},
428 }));
429 let text = "<tool_call>f\n\
430 <arg_key>limit</arg_key>\n<arg_value>4</arg_value>\n\
431 <arg_key>count</arg_key>\n<arg_value>5</arg_value>\n\
432 </tool_call>";
433 let result = Glm4MoeParser::glm45()
434 .parse_incremental(text, &tools)
435 .await
436 .unwrap();
437 let args: Value = serde_json::from_str(&result.calls[0].parameters).unwrap();
438 assert_eq!(args["limit"], Value::String("4".to_string()));
439 assert_eq!(args["count"], Value::Number(5.into()));
440 }
441}