From 20abf42889026e47bbf8cca56e9770c8cef291ee Mon Sep 17 00:00:00 2001
From: Jiayi Yan <1195343015@qq.com>
Date: Thu, 28 May 2026 20:07:08 +0800
Subject: [PATCH 1/3] 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>
---
crates/tool_parser/README.md | 2 +-
crates/tool_parser/src/factory.rs | 15 +++---
crates/tool_parser/src/lib.rs | 2 +-
.../src/parsers/{glm4_moe.rs => glm.rs} | 42 +++++++----------
crates/tool_parser/src/parsers/mod.rs | 4 +-
...parser_glm47_moe.rs => tool_parser_glm.rs} | 18 +++----
...arser_glm4_moe.rs => tool_parser_glm45.rs} | 47 ++++++++++++-------
docs/concepts/architecture/grpc-pipeline.md | 4 +-
docs/getting-started/grpc-workers.md | 4 +-
.../benches/tool_parser_benchmark.rs | 4 +-
10 files changed, 74 insertions(+), 68 deletions(-)
rename crates/tool_parser/src/parsers/{glm4_moe.rs => glm.rs} (89%)
rename crates/tool_parser/tests/{tool_parser_glm47_moe.rs => tool_parser_glm.rs} (92%)
rename crates/tool_parser/tests/{tool_parser_glm4_moe.rs => tool_parser_glm45.rs} (79%)
diff --git a/crates/tool_parser/README.md b/crates/tool_parser/README.md
index 38c7a5683..84b3e57d2 100644
--- a/crates/tool_parser/README.md
+++ b/crates/tool_parser/README.md
@@ -13,7 +13,7 @@ Parser library for extracting tool/function calls from LLM model outputs. Suppor
| `LlamaParser` | Llama 3.2 | `<\|python_tag\|>{...}` |
| `PythonicParser` | Llama 4, DeepSeek R1 | `[func_name(arg="val")]` |
| `DeepSeekParser` | DeepSeek V3 | `<\|tool▁calls▁begin\|>...<\|tool▁calls▁end\|>` |
-| `Glm4MoeParser` | GLM-4.5/4.6/4.7 | `<\|observation\|>...<\|/observation\|>` |
+| `GlmParser` | GLM-4.5 through GLM-5.1 | `.........` |
| `Step3Parser` | Step-3 | `...` |
| `KimiK2Parser` | Kimi K2 | `<\|tool_call_begin\|>...<\|tool_call_end\|>` |
| `MinimaxM2Parser` | MiniMax M2 | `{...}` |
diff --git a/crates/tool_parser/src/factory.rs b/crates/tool_parser/src/factory.rs
index 97fb62977..cf139766e 100644
--- a/crates/tool_parser/src/factory.rs
+++ b/crates/tool_parser/src/factory.rs
@@ -9,7 +9,7 @@ use tokio::sync::Mutex;
use crate::{
parsers::{
- CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, Glm4MoeParser,
+ CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, GlmParser,
JsonParser, KimiK2Parser, LlamaParser, MinimaxM2Parser, MistralParser, PassthroughParser,
PythonicParser, QwenParser, QwenXmlParser, Step3Parser,
},
@@ -318,8 +318,8 @@ impl ParserFactory {
registry.register_parser("deepseek31", || Box::new(DeepSeek31Parser::new()));
registry.register_parser("deepseek32", || Box::new(DeepSeekDsmlParser::v32()));
registry.register_parser("deepseek_v4", || Box::new(DeepSeekDsmlParser::v4()));
- registry.register_parser("glm45_moe", || Box::new(Glm4MoeParser::glm45()));
- registry.register_parser("glm47_moe", || Box::new(Glm4MoeParser::glm47()));
+ registry.register_parser("glm", || Box::new(GlmParser::default()));
+ registry.register_parser("glm45", || Box::new(GlmParser::glm45()));
registry.register_parser("step3", || Box::new(Step3Parser::new()));
registry.register_parser_with_structural_tag(
"kimik2",
@@ -386,11 +386,10 @@ impl ParserFactory {
registry.map_model("deepseek-ai/DeepSeek-V4*", "deepseek_v4");
registry.map_model("deepseek-*", "pythonic");
- // GLM models
- registry.map_model("glm-4.5*", "glm45_moe");
- registry.map_model("glm-4.6*", "glm45_moe");
- registry.map_model("glm-4.7*", "glm47_moe");
- registry.map_model("glm-*", "json");
+ // GLM models (4.5/4.6 use newline format, 4.7+ uses whitespace-only format)
+ registry.map_model("glm-4.5*", "glm45");
+ registry.map_model("glm-4.6*", "glm45");
+ registry.map_model("glm-*", "glm");
// Step3 models
registry.map_model("step3*", "step3");
diff --git a/crates/tool_parser/src/lib.rs b/crates/tool_parser/src/lib.rs
index f3e6cf458..3589506b2 100644
--- a/crates/tool_parser/src/lib.rs
+++ b/crates/tool_parser/src/lib.rs
@@ -17,7 +17,7 @@ mod tests;
// Re-export types used outside this module
pub use factory::{ParserFactory, PooledParser, ToolConstraint};
pub use parsers::{
- CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, Glm4MoeParser, JsonParser,
+ CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, GlmParser, JsonParser,
KimiK2Parser, LlamaParser, MinimaxM2Parser, MistralParser, PythonicParser, QwenParser,
Step3Parser,
};
diff --git a/crates/tool_parser/src/parsers/glm4_moe.rs b/crates/tool_parser/src/parsers/glm.rs
similarity index 89%
rename from crates/tool_parser/src/parsers/glm4_moe.rs
rename to crates/tool_parser/src/parsers/glm.rs
index 2eec81c03..c18f4c2f4 100644
--- a/crates/tool_parser/src/parsers/glm4_moe.rs
+++ b/crates/tool_parser/src/parsers/glm.rs
@@ -10,17 +10,14 @@ use crate::{
types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
};
-/// GLM-4 MoE format parser for tool calls
+/// GLM tool call format parser.
///
-/// Handles both GLM-4 MoE and GLM-4.7 MoE formats:
-/// - GLM-4: `{name}\n{key}\n{value}\n`
-/// - GLM-4.7: `{name}{key}{value}`
+/// Handles the XML-style `` format used by GLM-4.5 through GLM-5.1:
+/// - GLM-4.5/4.6: `{name}\n{key}\n{value}\n`
+/// - GLM-4.7/5/5.1: `{name}{key}{value}`
///
-/// Features:
-/// - XML-style tags for tool calls
-/// - Key-value pairs for arguments
-/// - Support for multiple sequential tool calls
-pub struct Glm4MoeParser {
+/// The default constructor uses the 4.7+ format (no newline between function name and args).
+pub struct GlmParser {
/// Regex for extracting complete tool calls
tool_call_extractor: Regex,
/// Regex for extracting function details
@@ -45,19 +42,13 @@ pub struct Glm4MoeParser {
eot_token: &'static str,
}
-impl Glm4MoeParser {
- /// Create a new generic GLM MoE parser with a custom func_detail_extractor pattern
- ///
- /// # Arguments
- /// - `func_detail_pattern`: Regex pattern for extracting function name and arguments
- /// - For GLM-4: `r"(?s)([^\n]*)\n(.*)"`
- /// - For GLM-4.7: `r"(?s)\s*([^<\s]+)\s*(.*?)"`
+impl GlmParser {
+ /// Create a new GLM parser with a custom func_detail_extractor pattern.
#[expect(
clippy::expect_used,
reason = "regex patterns are compile-time string literals"
)]
pub(crate) fn new(func_detail_pattern: &str) -> Self {
- // Use (?s) flag for DOTALL mode to handle newlines
let tool_call_pattern = r"(?s).*?";
let tool_call_extractor = Regex::new(tool_call_pattern).expect("Valid regex pattern");
@@ -79,13 +70,14 @@ impl Glm4MoeParser {
}
}
- /// Create a new GLM-4.5/4.6 MoE parser (with newline-based format)
+ /// Create a GLM-4.5/4.6 parser (newline between function name and args).
pub fn glm45() -> Self {
Self::new(r"(?s)([^\n]*)\n(.*)")
}
- /// Create a new GLM-4.7 MoE parser (with whitespace-based format)
- pub fn glm47() -> Self {
+ /// Create a GLM-4.7+ parser (no newline required between function name and args).
+ /// Compatible with GLM-4.7, GLM-5, GLM-5.1.
+ pub(crate) fn glm47() -> Self {
Self::new(r"(?s)\s*([^<\s]+)\s*(.*?)")
}
@@ -172,16 +164,16 @@ impl Glm4MoeParser {
}
}
-impl Default for Glm4MoeParser {
+impl Default for GlmParser {
fn default() -> Self {
- Self::glm45()
+ Self::glm47()
}
}
#[async_trait]
-impl ToolParser for Glm4MoeParser {
+impl ToolParser for GlmParser {
async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec)> {
- // Check if text contains GLM-4 MoE format
+ // Check if text contains GLM format
if !self.has_tool_markers(text) {
return Ok((text.to_string(), vec![]));
}
@@ -276,7 +268,7 @@ impl ToolParser for Glm4MoeParser {
tracing::debug!("Invalid tool name '{}' - skipping", tool_call.function.name);
helpers::reset_current_tool_state(
&mut self.buffer,
- &mut false, // glm45_moe/glm47_moe doesn't track name_sent per tool
+ &mut false,
&mut self.streamed_args_for_tool,
&self.prev_tool_call_arr,
);
diff --git a/crates/tool_parser/src/parsers/mod.rs b/crates/tool_parser/src/parsers/mod.rs
index 216473741..38b34cd6e 100644
--- a/crates/tool_parser/src/parsers/mod.rs
+++ b/crates/tool_parser/src/parsers/mod.rs
@@ -7,7 +7,7 @@ pub mod cohere;
pub mod deepseek;
pub mod deepseek31;
pub mod deepseek_dsml;
-pub mod glm4_moe;
+pub mod glm;
pub mod json;
pub mod kimik2;
pub mod llama;
@@ -27,7 +27,7 @@ pub use cohere::CohereParser;
pub use deepseek::DeepSeekParser;
pub use deepseek31::DeepSeek31Parser;
pub use deepseek_dsml::DeepSeekDsmlParser;
-pub use glm4_moe::Glm4MoeParser;
+pub use glm::GlmParser;
pub use json::JsonParser;
pub use kimik2::KimiK2Parser;
pub use llama::LlamaParser;
diff --git a/crates/tool_parser/tests/tool_parser_glm47_moe.rs b/crates/tool_parser/tests/tool_parser_glm.rs
similarity index 92%
rename from crates/tool_parser/tests/tool_parser_glm47_moe.rs
rename to crates/tool_parser/tests/tool_parser_glm.rs
index be43acba5..d5978d051 100644
--- a/crates/tool_parser/tests/tool_parser_glm47_moe.rs
+++ b/crates/tool_parser/tests/tool_parser_glm.rs
@@ -1,12 +1,12 @@
-//! GLM-4.7 MoE Parser Integration Tests
+//! GLM-4.7+ Tool Call Parser Integration Tests (default format)
mod common;
use common::create_test_tools;
-use tool_parser::{Glm4MoeParser, ToolParser};
+use tool_parser::{GlmParser, ToolParser};
#[tokio::test]
async fn test_glm47_complete_parsing() {
- let parser = Glm4MoeParser::glm47();
+ let parser = GlmParser::default();
let input = r"Let me search for that.
get_weathercityBeijingdate2024-12-25
@@ -24,7 +24,7 @@ The weather will be...";
#[tokio::test]
async fn test_glm47_multiple_tools() {
- let parser = Glm4MoeParser::glm47();
+ let parser = GlmParser::default();
let input = r"searchqueryrust tutorialstranslatetextHello Worldtarget_langzh";
@@ -37,7 +37,7 @@ async fn test_glm47_multiple_tools() {
#[tokio::test]
async fn test_glm47_type_conversion() {
- let parser = Glm4MoeParser::glm47();
+ let parser = GlmParser::default();
let input = r"processcount42rate1.5enabledtruedatanulltextstring value";
@@ -55,7 +55,7 @@ async fn test_glm47_type_conversion() {
#[tokio::test]
async fn test_glm47_streaming() {
- let mut parser = Glm4MoeParser::glm47();
+ let mut parser = GlmParser::default();
let tools = create_test_tools();
@@ -88,7 +88,7 @@ async fn test_glm47_streaming() {
#[test]
fn test_glm47_format_detection() {
- let parser = Glm4MoeParser::glm47();
+ let parser = GlmParser::default();
// Should detect GLM-4 format
assert!(parser.has_tool_markers(""));
@@ -102,7 +102,7 @@ fn test_glm47_format_detection() {
#[tokio::test]
async fn test_python_literals() {
- let parser = Glm4MoeParser::glm47();
+ let parser = GlmParser::default();
let input = r"test_funcbool_trueTruebool_falseFalsenone_valNone";
@@ -118,7 +118,7 @@ async fn test_python_literals() {
#[tokio::test]
async fn test_glm47_nested_json_in_arg_values() {
- let parser = Glm4MoeParser::glm47();
+ let parser = GlmParser::default();
let input = r#"processdata{"nested": {"key": "value"}}list[1, 2, 3]"#;
diff --git a/crates/tool_parser/tests/tool_parser_glm4_moe.rs b/crates/tool_parser/tests/tool_parser_glm45.rs
similarity index 79%
rename from crates/tool_parser/tests/tool_parser_glm4_moe.rs
rename to crates/tool_parser/tests/tool_parser_glm45.rs
index 4d47e96d9..6e362df25 100644
--- a/crates/tool_parser/tests/tool_parser_glm4_moe.rs
+++ b/crates/tool_parser/tests/tool_parser_glm45.rs
@@ -1,12 +1,12 @@
-//! GLM-4 MoE Parser Integration Tests
+//! GLM Tool Call Parser Integration Tests
mod common;
use common::create_test_tools;
-use tool_parser::{Glm4MoeParser, ToolParser};
+use tool_parser::{GlmParser, ToolParser};
#[tokio::test]
-async fn test_glm4_complete_parsing() {
- let parser = Glm4MoeParser::glm45();
+async fn test_glm_complete_parsing() {
+ let parser = GlmParser::glm45();
let input = r"Let me search for that.
get_weather
@@ -28,8 +28,8 @@ The weather will be...";
}
#[tokio::test]
-async fn test_glm4_multiple_tools() {
- let parser = Glm4MoeParser::glm45();
+async fn test_glm_multiple_tools() {
+ let parser = GlmParser::glm45();
let input = r"search
query
@@ -50,8 +50,8 @@ async fn test_glm4_multiple_tools() {
}
#[tokio::test]
-async fn test_glm4_type_conversion() {
- let parser = Glm4MoeParser::glm45();
+async fn test_glm_type_conversion() {
+ let parser = GlmParser::glm45();
let input = r"process
count
@@ -79,8 +79,8 @@ async fn test_glm4_type_conversion() {
}
#[tokio::test]
-async fn test_glm4_streaming() {
- let mut parser = Glm4MoeParser::glm45();
+async fn test_glm_streaming() {
+ let mut parser = GlmParser::glm45();
let tools = create_test_tools();
@@ -112,10 +112,10 @@ async fn test_glm4_streaming() {
}
#[test]
-fn test_glm4_format_detection() {
- let parser = Glm4MoeParser::glm45();
+fn test_glm_format_detection() {
+ let parser = GlmParser::glm45();
- // Should detect GLM-4 format
+ // Should detect GLM format
assert!(parser.has_tool_markers(""));
assert!(parser.has_tool_markers("text with marker"));
@@ -127,7 +127,7 @@ fn test_glm4_format_detection() {
#[tokio::test]
async fn test_python_literals() {
- let parser = Glm4MoeParser::glm45();
+ let parser = GlmParser::glm45();
let input = r"test_func
bool_true
@@ -149,8 +149,8 @@ async fn test_python_literals() {
}
#[tokio::test]
-async fn test_glm4_nested_json_in_arg_values() {
- let parser = Glm4MoeParser::glm45();
+async fn test_glm_nested_json_in_arg_values() {
+ let parser = GlmParser::glm45();
let input = r#"process
data
@@ -166,3 +166,18 @@ async fn test_glm4_nested_json_in_arg_values() {
assert!(args["data"].is_object());
assert!(args["list"].is_array());
}
+
+#[tokio::test]
+async fn test_glm_default_parses_glm47_format() {
+ let parser = GlmParser::default();
+
+ let input =
+ r"get_weathercityTokyo";
+
+ let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
+ assert_eq!(tools.len(), 1);
+ assert_eq!(tools[0].function.name, "get_weather");
+
+ let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
+ assert_eq!(args["city"], "Tokyo");
+}
diff --git a/docs/concepts/architecture/grpc-pipeline.md b/docs/concepts/architecture/grpc-pipeline.md
index 9280ffde8..d3ad3c70f 100644
--- a/docs/concepts/architecture/grpc-pipeline.md
+++ b/docs/concepts/architecture/grpc-pipeline.md
@@ -273,8 +273,8 @@ Qwen3-Coder / Qwen3.5+ XML format with parameter tags.
| `pythonic` | `llama-4*`, `deepseek-*` | Python-style function syntax |
| `llama` | `llama-3.2*` | Python tag with JSON |
| `deepseek` | `deepseek-v3*` | XML with function syntax |
-| `glm45_moe` | `glm-4.5*`, `glm-4.6*` | GLM 4.5/4.6 MoE format |
-| `glm47_moe` | `glm-4.7*` | GLM 4.7 MoE format |
+| `glm` | `glm-*` | GLM 4.7+ format (4.5/4.6 fall back to `glm45`) |
+| `glm45` | `glm-4.5*`, `glm-4.6*` | GLM 4.5/4.6 format (newline-based) |
| `step3` | `step3*`, `Step-3*` | Step-3 model format |
| `kimik2` | `kimi-k2*`, `Kimi-K2*` | Kimi K2 model format |
| `minimax_m2` | `minimax*`, `MiniMax*` | MiniMax M2 model format |
diff --git a/docs/getting-started/grpc-workers.md b/docs/getting-started/grpc-workers.md
index 72fc6d158..30891a064 100644
--- a/docs/getting-started/grpc-workers.md
+++ b/docs/getting-started/grpc-workers.md
@@ -196,8 +196,8 @@ Auto-detected from the model name. Override with `--tool-call-parser` if needed.
| `mistral` | Mistral, Mixtral |
| `qwen` | Qwen |
| `qwen_xml` | Qwen3-Coder, Qwen3.5+ |
-| `glm45_moe` | GLM-4.5, GLM-4.6 |
-| `glm47_moe` | GLM-4.7 |
+| `glm` | GLM-4.7, GLM-5, GLM-5.1 |
+| `glm45` | GLM-4.5, GLM-4.6 |
| `step3` | Step-3 |
| `kimik2` | Kimi-K2 |
| `minimax_m2` | MiniMax |
diff --git a/model_gateway/benches/tool_parser_benchmark.rs b/model_gateway/benches/tool_parser_benchmark.rs
index 7a81a4270..922daaae7 100644
--- a/model_gateway/benches/tool_parser_benchmark.rs
+++ b/model_gateway/benches/tool_parser_benchmark.rs
@@ -291,8 +291,8 @@ fn bench_complete_parsing(c: &mut Criterion) {
("pythonic_multi", "pythonic", PYTHONIC_MULTI),
("deepseek", "deepseek", DEEPSEEK_FORMAT),
("kimik2", "kimik2", KIMIK2_FORMAT),
- ("glm45", "glm45_moe", GLM45_FORMAT),
- ("glm47", "glm47_moe", GLM47_FORMAT),
+ ("glm45", "glm45", GLM45_FORMAT),
+ ("glm47", "glm", GLM47_FORMAT),
("step3", "step3", STEP3_FORMAT),
("gpt_oss", "gpt_oss", GPT_OSS_FORMAT),
];
From 5e86bb28ae1233858939451439ebea6365eb2527 Mon Sep 17 00:00:00 2001
From: Jiayi Yan <1195343015@qq.com>
Date: Fri, 29 May 2026 22:31:00 +0800
Subject: [PATCH 2/3] docs: clarify glm-* pattern routing in pipeline reference
table
Replace misleading "fall back" phrasing with an explicit note that
glm-4.5*/glm-4.6* patterns are matched to glm45 before the catch-all.
Signed-off-by: Jiayi Yan <1195343015@qq.com>
---
docs/concepts/architecture/grpc-pipeline.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/concepts/architecture/grpc-pipeline.md b/docs/concepts/architecture/grpc-pipeline.md
index d3ad3c70f..782e6f2ae 100644
--- a/docs/concepts/architecture/grpc-pipeline.md
+++ b/docs/concepts/architecture/grpc-pipeline.md
@@ -273,7 +273,7 @@ Qwen3-Coder / Qwen3.5+ XML format with parameter tags.
| `pythonic` | `llama-4*`, `deepseek-*` | Python-style function syntax |
| `llama` | `llama-3.2*` | Python tag with JSON |
| `deepseek` | `deepseek-v3*` | XML with function syntax |
-| `glm` | `glm-*` | GLM 4.7+ format (4.5/4.6 fall back to `glm45`) |
+| `glm` | `glm-*` | GLM 4.7+ format (`glm-4.5*`/`glm-4.6*` are matched to `glm45` first) |
| `glm45` | `glm-4.5*`, `glm-4.6*` | GLM 4.5/4.6 format (newline-based) |
| `step3` | `step3*`, `Step-3*` | Step-3 model format |
| `kimik2` | `kimi-k2*`, `Kimi-K2*` | Kimi K2 model format |
From a75ec5620615adec5463c11a29105cae0280d126 Mon Sep 17 00:00:00 2001
From: Jiayi Yan <1195343015@qq.com>
Date: Fri, 29 May 2026 22:43:19 +0800
Subject: [PATCH 3/3] style(tool-parser): fix nightly fmt import line wrapping
Shortened GlmParser name allows JsonParser to fit on the same line.
Signed-off-by: Jiayi Yan <1195343015@qq.com>
---
crates/tool_parser/src/factory.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/crates/tool_parser/src/factory.rs b/crates/tool_parser/src/factory.rs
index cf139766e..e52b29503 100644
--- a/crates/tool_parser/src/factory.rs
+++ b/crates/tool_parser/src/factory.rs
@@ -9,8 +9,8 @@ use tokio::sync::Mutex;
use crate::{
parsers::{
- CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, GlmParser,
- JsonParser, KimiK2Parser, LlamaParser, MinimaxM2Parser, MistralParser, PassthroughParser,
+ CohereParser, DeepSeek31Parser, DeepSeekDsmlParser, DeepSeekParser, GlmParser, JsonParser,
+ KimiK2Parser, LlamaParser, MinimaxM2Parser, MistralParser, PassthroughParser,
PythonicParser, QwenParser, QwenXmlParser, Step3Parser,
},
traits::ToolParser,