Skip to content

Commit d623e7c

Browse files
committed
refactor(tool-parser): rename Glm4MoeParser to GlmParser with catch-all "glm" pattern
Rename Glm4MoeParser to GlmParser. Use glm47 format (whitespace-only, GLM-4.7/5/5.1) as the default, with specialized glm45 format for GLM-4.5/4.6 where the function name is separated by a newline. Replace glm-* -> json fallback with glm-* -> glm so that GLM-5/5.1 models are routed to the correct tool call parser instead of the generic JSON parser. Drop the misleading "_moe" suffix from parser keys. Signed-off-by: Jiayi Yan <1195343015@qq.com>
1 parent c72ae0e commit d623e7c

10 files changed

Lines changed: 73 additions & 67 deletions

File tree

crates/tool_parser/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Parser library for extracting tool/function calls from LLM model outputs. Suppor
1313
| `LlamaParser` | Llama 3.2 | `<\|python_tag\|>{...}` |
1414
| `PythonicParser` | Llama 4, DeepSeek R1 | `[func_name(arg="val")]` |
1515
| `DeepSeekParser` | DeepSeek V3 | `<\|tool▁calls▁begin\|>...<\|tool▁calls▁end\|>` |
16-
| `Glm4MoeParser` | GLM-4.5/4.6/4.7 | `<\|observation\|>...<\|/observation\|>` |
16+
| `GlmParser` | GLM-4.5 through GLM-5.1 | `<tool_call>...<arg_key>...</arg_key><arg_value>...</arg_value></tool_call>` |
1717
| `Step3Parser` | Step-3 | `<steptml:function_call>...</steptml:function_call>` |
1818
| `KimiK2Parser` | Kimi K2 | `<\|tool_call_begin\|>...<\|tool_call_end\|>` |
1919
| `MinimaxM2Parser` | MiniMax M2 | `<FUNCTION_CALL>{...}</FUNCTION_CALL>` |

crates/tool_parser/src/factory.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use tokio::sync::Mutex;
99

1010
use crate::{
1111
parsers::{
12-
CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, Glm4MoeParser,
12+
CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, GlmParser,
1313
JsonParser, KimiK2Parser, LlamaParser, MinimaxM2Parser, MistralParser, PassthroughParser,
1414
PythonicParser, QwenParser, QwenXmlParser, Step3Parser,
1515
},
@@ -318,8 +318,8 @@ impl ParserFactory {
318318
registry.register_parser("deepseek31", || Box::new(DeepSeek31Parser::new()));
319319
registry.register_parser("deepseek32", || Box::new(DeepSeekDsmlParser::v32()));
320320
registry.register_parser("deepseek_v4", || Box::new(DeepSeekDsmlParser::v4()));
321-
registry.register_parser("glm45_moe", || Box::new(Glm4MoeParser::glm45()));
322-
registry.register_parser("glm47_moe", || Box::new(Glm4MoeParser::glm47()));
321+
registry.register_parser("glm", || Box::new(GlmParser::default()));
322+
registry.register_parser("glm45", || Box::new(GlmParser::glm45()));
323323
registry.register_parser("step3", || Box::new(Step3Parser::new()));
324324
registry.register_parser_with_structural_tag(
325325
"kimik2",
@@ -386,11 +386,10 @@ impl ParserFactory {
386386
registry.map_model("deepseek-ai/DeepSeek-V4*", "deepseek_v4");
387387
registry.map_model("deepseek-*", "pythonic");
388388

389-
// GLM models
390-
registry.map_model("glm-4.5*", "glm45_moe");
391-
registry.map_model("glm-4.6*", "glm45_moe");
392-
registry.map_model("glm-4.7*", "glm47_moe");
393-
registry.map_model("glm-*", "json");
389+
// GLM models (4.5/4.6 use newline format, 4.7+ uses whitespace-only format)
390+
registry.map_model("glm-4.5*", "glm45");
391+
registry.map_model("glm-4.6*", "glm45");
392+
registry.map_model("glm-*", "glm");
394393

395394
// Step3 models
396395
registry.map_model("step3*", "step3");

crates/tool_parser/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mod tests;
1717
// Re-export types used outside this module
1818
pub use factory::{ParserFactory, PooledParser, ToolConstraint};
1919
pub use parsers::{
20-
CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, Glm4MoeParser, JsonParser,
20+
CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, GlmParser, JsonParser,
2121
KimiK2Parser, LlamaParser, MinimaxM2Parser, MistralParser, PythonicParser, QwenParser,
2222
Step3Parser,
2323
};

crates/tool_parser/src/parsers/glm4_moe.rs renamed to crates/tool_parser/src/parsers/glm.rs

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,14 @@ use crate::{
1010
types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
1111
};
1212

13-
/// GLM-4 MoE format parser for tool calls
13+
/// GLM tool call format parser.
1414
///
15-
/// Handles both GLM-4 MoE and GLM-4.7 MoE formats:
16-
/// - GLM-4: `<tool_call>{name}\n<arg_key>{key}</arg_key>\n<arg_value>{value}</arg_value>\n</tool_call>`
17-
/// - GLM-4.7: `<tool_call>{name}<arg_key>{key}</arg_key><arg_value>{value}</arg_value></tool_call>`
15+
/// Handles the XML-style `<tool_call>` format used by GLM-4.5 through GLM-5.1:
16+
/// - GLM-4.5/4.6: `<tool_call>{name}\n<arg_key>{key}</arg_key>\n<arg_value>{value}</arg_value>\n</tool_call>`
17+
/// - GLM-4.7/5/5.1: `<tool_call>{name}<arg_key>{key}</arg_key><arg_value>{value}</arg_value></tool_call>`
1818
///
19-
/// Features:
20-
/// - XML-style tags for tool calls
21-
/// - Key-value pairs for arguments
22-
/// - Support for multiple sequential tool calls
23-
pub struct Glm4MoeParser {
19+
/// The default constructor uses the 4.7+ format (no newline between function name and args).
20+
pub struct GlmParser {
2421
/// Regex for extracting complete tool calls
2522
tool_call_extractor: Regex,
2623
/// Regex for extracting function details
@@ -45,19 +42,13 @@ pub struct Glm4MoeParser {
4542
eot_token: &'static str,
4643
}
4744

48-
impl Glm4MoeParser {
49-
/// Create a new generic GLM MoE parser with a custom func_detail_extractor pattern
50-
///
51-
/// # Arguments
52-
/// - `func_detail_pattern`: Regex pattern for extracting function name and arguments
53-
/// - For GLM-4: `r"(?s)<tool_call>([^\n]*)\n(.*)</tool_call>"`
54-
/// - For GLM-4.7: `r"(?s)<tool_call>\s*([^<\s]+)\s*(.*?)</tool_call>"`
45+
impl GlmParser {
46+
/// Create a new GLM parser with a custom func_detail_extractor pattern.
5547
#[expect(
5648
clippy::expect_used,
5749
reason = "regex patterns are compile-time string literals"
5850
)]
5951
pub(crate) fn new(func_detail_pattern: &str) -> Self {
60-
// Use (?s) flag for DOTALL mode to handle newlines
6152
let tool_call_pattern = r"(?s)<tool_call>.*?</tool_call>";
6253
let tool_call_extractor = Regex::new(tool_call_pattern).expect("Valid regex pattern");
6354

@@ -79,12 +70,13 @@ impl Glm4MoeParser {
7970
}
8071
}
8172

82-
/// Create a new GLM-4.5/4.6 MoE parser (with newline-based format)
73+
/// Create a GLM-4.5/4.6 parser (newline between function name and args).
8374
pub fn glm45() -> Self {
8475
Self::new(r"(?s)<tool_call>([^\n]*)\n(.*)</tool_call>")
8576
}
8677

87-
/// Create a new GLM-4.7 MoE parser (with whitespace-based format)
78+
/// Create a GLM-4.7+ parser (no newline required between function name and args).
79+
/// Compatible with GLM-4.7, GLM-5, GLM-5.1.
8880
pub fn glm47() -> Self {
8981
Self::new(r"(?s)<tool_call>\s*([^<\s]+)\s*(.*?)</tool_call>")
9082
}
@@ -172,16 +164,16 @@ impl Glm4MoeParser {
172164
}
173165
}
174166

175-
impl Default for Glm4MoeParser {
167+
impl Default for GlmParser {
176168
fn default() -> Self {
177-
Self::glm45()
169+
Self::glm47()
178170
}
179171
}
180172

181173
#[async_trait]
182-
impl ToolParser for Glm4MoeParser {
174+
impl ToolParser for GlmParser {
183175
async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
184-
// Check if text contains GLM-4 MoE format
176+
// Check if text contains GLM format
185177
if !self.has_tool_markers(text) {
186178
return Ok((text.to_string(), vec![]));
187179
}
@@ -276,7 +268,7 @@ impl ToolParser for Glm4MoeParser {
276268
tracing::debug!("Invalid tool name '{}' - skipping", tool_call.function.name);
277269
helpers::reset_current_tool_state(
278270
&mut self.buffer,
279-
&mut false, // glm45_moe/glm47_moe doesn't track name_sent per tool
271+
&mut false,
280272
&mut self.streamed_args_for_tool,
281273
&self.prev_tool_call_arr,
282274
);

crates/tool_parser/src/parsers/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub mod cohere;
77
pub mod deepseek;
88
pub mod deepseek31;
99
pub mod deepseek_dsml;
10-
pub mod glm4_moe;
10+
pub mod glm;
1111
pub mod json;
1212
pub mod kimik2;
1313
pub mod llama;
@@ -27,7 +27,7 @@ pub use cohere::CohereParser;
2727
pub use deepseek::DeepSeekParser;
2828
pub use deepseek31::DeepSeek31Parser;
2929
pub use deepseek_dsml::DeepSeekDsmlParser;
30-
pub use glm4_moe::Glm4MoeParser;
30+
pub use glm::GlmParser;
3131
pub use json::JsonParser;
3232
pub use kimik2::KimiK2Parser;
3333
pub use llama::LlamaParser;

crates/tool_parser/tests/tool_parser_glm47_moe.rs renamed to crates/tool_parser/tests/tool_parser_glm.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
//! GLM-4.7 MoE Parser Integration Tests
1+
//! GLM-4.7+ Tool Call Parser Integration Tests (default format)
22
mod common;
33

44
use common::create_test_tools;
5-
use tool_parser::{Glm4MoeParser, ToolParser};
5+
use tool_parser::{GlmParser, ToolParser};
66

77
#[tokio::test]
88
async fn test_glm47_complete_parsing() {
9-
let parser = Glm4MoeParser::glm47();
9+
let parser = GlmParser::default();
1010

1111
let input = r"Let me search for that.
1212
<tool_call>get_weather<arg_key>city</arg_key><arg_value>Beijing</arg_value><arg_key>date</arg_key><arg_value>2024-12-25</arg_value></tool_call>
@@ -24,7 +24,7 @@ The weather will be...";
2424

2525
#[tokio::test]
2626
async fn test_glm47_multiple_tools() {
27-
let parser = Glm4MoeParser::glm47();
27+
let parser = GlmParser::default();
2828

2929
let input = r"<tool_call>search<arg_key>query</arg_key><arg_value>rust tutorials</arg_value></tool_call><tool_call>translate<arg_key>text</arg_key><arg_value>Hello World</arg_value><arg_key>target_lang</arg_key><arg_value>zh</arg_value></tool_call>";
3030

@@ -37,7 +37,7 @@ async fn test_glm47_multiple_tools() {
3737

3838
#[tokio::test]
3939
async fn test_glm47_type_conversion() {
40-
let parser = Glm4MoeParser::glm47();
40+
let parser = GlmParser::default();
4141

4242
let input = r"<tool_call>process<arg_key>count</arg_key><arg_value>42</arg_value><arg_key>rate</arg_key><arg_value>1.5</arg_value><arg_key>enabled</arg_key><arg_value>true</arg_value><arg_key>data</arg_key><arg_value>null</arg_value><arg_key>text</arg_key><arg_value>string value</arg_value></tool_call>";
4343

@@ -55,7 +55,7 @@ async fn test_glm47_type_conversion() {
5555

5656
#[tokio::test]
5757
async fn test_glm47_streaming() {
58-
let mut parser = Glm4MoeParser::glm47();
58+
let mut parser = GlmParser::default();
5959

6060
let tools = create_test_tools();
6161

@@ -88,7 +88,7 @@ async fn test_glm47_streaming() {
8888

8989
#[test]
9090
fn test_glm47_format_detection() {
91-
let parser = Glm4MoeParser::glm47();
91+
let parser = GlmParser::default();
9292

9393
// Should detect GLM-4 format
9494
assert!(parser.has_tool_markers("<tool_call>"));
@@ -102,7 +102,7 @@ fn test_glm47_format_detection() {
102102

103103
#[tokio::test]
104104
async fn test_python_literals() {
105-
let parser = Glm4MoeParser::glm47();
105+
let parser = GlmParser::default();
106106

107107
let input = r"<tool_call>test_func<arg_key>bool_true</arg_key><arg_value>True</arg_value><arg_key>bool_false</arg_key><arg_value>False</arg_value><arg_key>none_val</arg_key><arg_value>None</arg_value></tool_call>";
108108

@@ -118,7 +118,7 @@ async fn test_python_literals() {
118118

119119
#[tokio::test]
120120
async fn test_glm47_nested_json_in_arg_values() {
121-
let parser = Glm4MoeParser::glm47();
121+
let parser = GlmParser::default();
122122

123123
let input = r#"<tool_call>process<arg_key>data</arg_key><arg_value>{"nested": {"key": "value"}}</arg_value><arg_key>list</arg_key><arg_value>[1, 2, 3]</arg_value></tool_call>"#;
124124

crates/tool_parser/tests/tool_parser_glm4_moe.rs renamed to crates/tool_parser/tests/tool_parser_glm45.rs

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
//! GLM-4 MoE Parser Integration Tests
1+
//! GLM Tool Call Parser Integration Tests
22
mod common;
33

44
use common::create_test_tools;
5-
use tool_parser::{Glm4MoeParser, ToolParser};
5+
use tool_parser::{GlmParser, ToolParser};
66

77
#[tokio::test]
8-
async fn test_glm4_complete_parsing() {
9-
let parser = Glm4MoeParser::glm45();
8+
async fn test_glm_complete_parsing() {
9+
let parser = GlmParser::glm45();
1010

1111
let input = r"Let me search for that.
1212
<tool_call>get_weather
@@ -28,8 +28,8 @@ The weather will be...";
2828
}
2929

3030
#[tokio::test]
31-
async fn test_glm4_multiple_tools() {
32-
let parser = Glm4MoeParser::glm45();
31+
async fn test_glm_multiple_tools() {
32+
let parser = GlmParser::glm45();
3333

3434
let input = r"<tool_call>search
3535
<arg_key>query</arg_key>
@@ -50,8 +50,8 @@ async fn test_glm4_multiple_tools() {
5050
}
5151

5252
#[tokio::test]
53-
async fn test_glm4_type_conversion() {
54-
let parser = Glm4MoeParser::glm45();
53+
async fn test_glm_type_conversion() {
54+
let parser = GlmParser::glm45();
5555

5656
let input = r"<tool_call>process
5757
<arg_key>count</arg_key>
@@ -79,8 +79,8 @@ async fn test_glm4_type_conversion() {
7979
}
8080

8181
#[tokio::test]
82-
async fn test_glm4_streaming() {
83-
let mut parser = Glm4MoeParser::glm45();
82+
async fn test_glm_streaming() {
83+
let mut parser = GlmParser::glm45();
8484

8585
let tools = create_test_tools();
8686

@@ -112,10 +112,10 @@ async fn test_glm4_streaming() {
112112
}
113113

114114
#[test]
115-
fn test_glm4_format_detection() {
116-
let parser = Glm4MoeParser::glm45();
115+
fn test_glm_format_detection() {
116+
let parser = GlmParser::glm45();
117117

118-
// Should detect GLM-4 format
118+
// Should detect GLM format
119119
assert!(parser.has_tool_markers("<tool_call>"));
120120
assert!(parser.has_tool_markers("text with <tool_call> marker"));
121121

@@ -127,7 +127,7 @@ fn test_glm4_format_detection() {
127127

128128
#[tokio::test]
129129
async fn test_python_literals() {
130-
let parser = Glm4MoeParser::glm45();
130+
let parser = GlmParser::glm45();
131131

132132
let input = r"<tool_call>test_func
133133
<arg_key>bool_true</arg_key>
@@ -149,8 +149,8 @@ async fn test_python_literals() {
149149
}
150150

151151
#[tokio::test]
152-
async fn test_glm4_nested_json_in_arg_values() {
153-
let parser = Glm4MoeParser::glm45();
152+
async fn test_glm_nested_json_in_arg_values() {
153+
let parser = GlmParser::glm45();
154154

155155
let input = r#"<tool_call>process
156156
<arg_key>data</arg_key>
@@ -166,3 +166,18 @@ async fn test_glm4_nested_json_in_arg_values() {
166166
assert!(args["data"].is_object());
167167
assert!(args["list"].is_array());
168168
}
169+
170+
#[tokio::test]
171+
async fn test_glm_default_parses_glm47_format() {
172+
let parser = GlmParser::default();
173+
174+
let input =
175+
r"<tool_call>get_weather<arg_key>city</arg_key><arg_value>Tokyo</arg_value></tool_call>";
176+
177+
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
178+
assert_eq!(tools.len(), 1);
179+
assert_eq!(tools[0].function.name, "get_weather");
180+
181+
let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
182+
assert_eq!(args["city"], "Tokyo");
183+
}

docs/concepts/architecture/grpc-pipeline.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ Qwen3-Coder / Qwen3.5+ XML format with parameter tags.
273273
| `pythonic` | `llama-4*`, `deepseek-*` | Python-style function syntax |
274274
| `llama` | `llama-3.2*` | Python tag with JSON |
275275
| `deepseek` | `deepseek-v3*` | XML with function syntax |
276-
| `glm45_moe` | `glm-4.5*`, `glm-4.6*` | GLM 4.5/4.6 MoE format |
277-
| `glm47_moe` | `glm-4.7*` | GLM 4.7 MoE format |
276+
| `glm` | `glm-*` | GLM 4.7+ format (4.5/4.6 fall back to `glm45`) |
277+
| `glm45` | `glm-4.5*`, `glm-4.6*` | GLM 4.5/4.6 format (newline-based) |
278278
| `step3` | `step3*`, `Step-3*` | Step-3 model format |
279279
| `kimik2` | `kimi-k2*`, `Kimi-K2*` | Kimi K2 model format |
280280
| `minimax_m2` | `minimax*`, `MiniMax*` | MiniMax M2 model format |

docs/getting-started/grpc-workers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ Auto-detected from the model name. Override with `--tool-call-parser` if needed.
196196
| `mistral` | Mistral, Mixtral |
197197
| `qwen` | Qwen |
198198
| `qwen_xml` | Qwen3-Coder, Qwen3.5+ |
199-
| `glm45_moe` | GLM-4.5, GLM-4.6 |
200-
| `glm47_moe` | GLM-4.7 |
199+
| `glm` | GLM-4.7, GLM-5, GLM-5.1 |
200+
| `glm45` | GLM-4.5, GLM-4.6 |
201201
| `step3` | Step-3 |
202202
| `kimik2` | Kimi-K2 |
203203
| `minimax_m2` | MiniMax |

model_gateway/benches/tool_parser_benchmark.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ fn bench_complete_parsing(c: &mut Criterion) {
291291
("pythonic_multi", "pythonic", PYTHONIC_MULTI),
292292
("deepseek", "deepseek", DEEPSEEK_FORMAT),
293293
("kimik2", "kimik2", KIMIK2_FORMAT),
294-
("glm45", "glm45_moe", GLM45_FORMAT),
295-
("glm47", "glm47_moe", GLM47_FORMAT),
294+
("glm45", "glm45", GLM45_FORMAT),
295+
("glm47", "glm", GLM47_FORMAT),
296296
("step3", "step3", STEP3_FORMAT),
297297
("gpt_oss", "gpt_oss", GPT_OSS_FORMAT),
298298
];

0 commit comments

Comments
 (0)