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,