From 5cfbb4c1369284e77084a2405595a94895bcf15e Mon Sep 17 00:00:00 2001 From: Chang Su Date: Wed, 20 Aug 2025 18:33:10 -0700 Subject: [PATCH] [router] add glm and step3 reasoning parser (#9415) --- sgl-router/src/reasoning_parser/factory.rs | 25 +++- sgl-router/src/reasoning_parser/mod.rs | 3 +- .../src/reasoning_parser/parsers/glm45.rs | 118 +++++++++++++++++ .../src/reasoning_parser/parsers/mod.rs | 4 + .../src/reasoning_parser/parsers/step3.rs | 123 ++++++++++++++++++ 5 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 sgl-router/src/reasoning_parser/parsers/glm45.rs create mode 100644 sgl-router/src/reasoning_parser/parsers/step3.rs diff --git a/sgl-router/src/reasoning_parser/factory.rs b/sgl-router/src/reasoning_parser/factory.rs index 970f9e41a..771f0e856 100644 --- a/sgl-router/src/reasoning_parser/factory.rs +++ b/sgl-router/src/reasoning_parser/factory.rs @@ -5,7 +5,8 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; use crate::reasoning_parser::parsers::{ - BaseReasoningParser, DeepSeekR1Parser, KimiParser, Qwen3Parser, QwenThinkingParser, + BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, Qwen3Parser, + QwenThinkingParser, Step3Parser, }; use crate::reasoning_parser::traits::{ParseError, ParserConfig, ReasoningParser}; @@ -153,15 +154,21 @@ impl ParserFactory { // Register Kimi parser with Unicode tokens (starts with in_reasoning=false) registry.register_parser("kimi", || Box::new(KimiParser::new())); + // Register GLM45 parser (same format as Qwen3 but separate for debugging) + registry.register_parser("glm45", || Box::new(Glm45Parser::new())); + + // Register Step3 parser (same format as DeepSeek-R1 but separate for debugging) + registry.register_parser("step3", || Box::new(Step3Parser::new())); + // Register model patterns registry.register_pattern("deepseek-r1", "deepseek_r1"); registry.register_pattern("qwen3-thinking", "qwen3_thinking"); registry.register_pattern("qwen-thinking", "qwen3_thinking"); registry.register_pattern("qwen3", "qwen3"); registry.register_pattern("qwen", "qwen3"); - registry.register_pattern("glm45", "qwen3"); // GLM45 uses same format as Qwen3 + registry.register_pattern("glm45", "glm45"); registry.register_pattern("kimi", "kimi"); - registry.register_pattern("step3", "deepseek_r1"); // Step3 alias for DeepSeek-R1 + registry.register_pattern("step3", "step3"); Self { registry } } @@ -281,13 +288,17 @@ mod tests { } #[test] - fn test_alias_models() { + fn test_step3_model() { let factory = ParserFactory::new(); let step3 = factory.create("step3-model").unwrap(); - let glm45 = factory.create("glm45-v2").unwrap(); + assert_eq!(step3.model_type(), "step3"); + } - assert_eq!(step3.model_type(), "deepseek_r1"); - assert_eq!(glm45.model_type(), "qwen3"); + #[test] + fn test_glm45_model() { + let factory = ParserFactory::new(); + let glm45 = factory.create("glm45-v2").unwrap(); + assert_eq!(glm45.model_type(), "glm45"); } #[test] diff --git a/sgl-router/src/reasoning_parser/mod.rs b/sgl-router/src/reasoning_parser/mod.rs index 3be6321c7..95ffcbc4f 100644 --- a/sgl-router/src/reasoning_parser/mod.rs +++ b/sgl-router/src/reasoning_parser/mod.rs @@ -4,6 +4,7 @@ pub mod traits; pub use factory::{ParserFactory, ParserRegistry, PooledParser}; pub use parsers::{ - BaseReasoningParser, DeepSeekR1Parser, KimiParser, Qwen3Parser, QwenThinkingParser, + BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, Qwen3Parser, + QwenThinkingParser, Step3Parser, }; pub use traits::{ParseError, ParserConfig, ParserResult, ReasoningParser}; diff --git a/sgl-router/src/reasoning_parser/parsers/glm45.rs b/sgl-router/src/reasoning_parser/parsers/glm45.rs new file mode 100644 index 000000000..e4e56723c --- /dev/null +++ b/sgl-router/src/reasoning_parser/parsers/glm45.rs @@ -0,0 +1,118 @@ +// GLM45 specific reasoning parser. +// Uses the same format as Qwen3 but has its own implementation for debugging. + +use crate::reasoning_parser::parsers::BaseReasoningParser; +use crate::reasoning_parser::traits::{ParseError, ParserConfig, ParserResult, ReasoningParser}; + +/// GLM45 reasoning parser. +/// +/// This parser uses the same format as Qwen3 (...) but has +/// its own implementation for better debugging and potential future customization. +pub struct Glm45Parser { + base: BaseReasoningParser, +} + +impl Glm45Parser { + /// Create a new GLM45 parser. + pub fn new() -> Self { + let config = ParserConfig { + think_start_token: "".to_string(), + think_end_token: "".to_string(), + stream_reasoning: true, + max_buffer_size: 65536, + initial_in_reasoning: false, // Requires explicit start token like Qwen3 + }; + + Self { + base: BaseReasoningParser::new(config).with_model_type("glm45".to_string()), + } + } +} + +impl Default for Glm45Parser { + fn default() -> Self { + Self::new() + } +} + +impl ReasoningParser for Glm45Parser { + fn detect_and_parse_reasoning(&mut self, text: &str) -> Result { + self.base.detect_and_parse_reasoning(text) + } + + fn parse_reasoning_streaming_incremental( + &mut self, + text: &str, + ) -> Result { + self.base.parse_reasoning_streaming_incremental(text) + } + + fn reset(&mut self) { + self.base.reset() + } + + fn model_type(&self) -> &str { + self.base.model_type() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_glm45_initial_state() { + let mut parser = Glm45Parser::new(); + + // Should NOT treat text as reasoning without start token + let result = parser + .detect_and_parse_reasoning("This is normal content") + .unwrap(); + assert_eq!(result.normal_text, "This is normal content"); + assert_eq!(result.reasoning_text, ""); + } + + #[test] + fn test_glm45_with_tokens() { + let mut parser = Glm45Parser::new(); + + // Should extract reasoning with proper tokens + let result = parser + .detect_and_parse_reasoning("reasoning contentanswer") + .unwrap(); + assert_eq!(result.normal_text, "answer"); + assert_eq!(result.reasoning_text, "reasoning content"); + } + + #[test] + fn test_glm45_streaming() { + let mut parser = Glm45Parser::new(); + + // First chunk - normal text + let result1 = parser + .parse_reasoning_streaming_incremental("normal text ") + .unwrap(); + assert_eq!(result1.normal_text, "normal text "); + assert_eq!(result1.reasoning_text, ""); + + // Second chunk - enters reasoning + let result2 = parser + .parse_reasoning_streaming_incremental("reasoning") + .unwrap(); + assert_eq!(result2.normal_text, ""); + assert_eq!(result2.reasoning_text, "reasoning"); + + // Third chunk - exits reasoning + let result3 = parser + .parse_reasoning_streaming_incremental("answer") + .unwrap(); + assert_eq!(result3.normal_text, "answer"); + assert_eq!(result3.reasoning_text, ""); + } + + #[test] + fn test_model_type() { + let parser = Glm45Parser::new(); + assert_eq!(parser.model_type(), "glm45"); + } +} diff --git a/sgl-router/src/reasoning_parser/parsers/mod.rs b/sgl-router/src/reasoning_parser/parsers/mod.rs index 7505a1da3..a940a055c 100644 --- a/sgl-router/src/reasoning_parser/parsers/mod.rs +++ b/sgl-router/src/reasoning_parser/parsers/mod.rs @@ -1,9 +1,13 @@ pub mod base; pub mod deepseek_r1; +pub mod glm45; pub mod kimi; pub mod qwen3; +pub mod step3; pub use base::BaseReasoningParser; pub use deepseek_r1::DeepSeekR1Parser; +pub use glm45::Glm45Parser; pub use kimi::KimiParser; pub use qwen3::{Qwen3Parser, QwenThinkingParser}; +pub use step3::Step3Parser; diff --git a/sgl-router/src/reasoning_parser/parsers/step3.rs b/sgl-router/src/reasoning_parser/parsers/step3.rs new file mode 100644 index 000000000..cec0bcd15 --- /dev/null +++ b/sgl-router/src/reasoning_parser/parsers/step3.rs @@ -0,0 +1,123 @@ +// Step3 specific reasoning parser. +// Uses the same format as DeepSeek-R1 but has its own implementation for debugging. + +use crate::reasoning_parser::parsers::BaseReasoningParser; +use crate::reasoning_parser::traits::{ParseError, ParserConfig, ParserResult, ReasoningParser}; + +/// Step3 reasoning parser. +/// +/// This parser uses the same format as DeepSeek-R1 (...) but has +/// its own implementation for better debugging and potential future customization. +pub struct Step3Parser { + base: BaseReasoningParser, +} + +impl Step3Parser { + /// Create a new Step3 parser. + pub fn new() -> Self { + let config = ParserConfig { + think_start_token: "".to_string(), + think_end_token: "".to_string(), + stream_reasoning: true, + max_buffer_size: 65536, + initial_in_reasoning: true, // Assumes reasoning from start like DeepSeek-R1 + }; + + Self { + base: BaseReasoningParser::new(config).with_model_type("step3".to_string()), + } + } +} + +impl Default for Step3Parser { + fn default() -> Self { + Self::new() + } +} + +impl ReasoningParser for Step3Parser { + fn detect_and_parse_reasoning(&mut self, text: &str) -> Result { + self.base.detect_and_parse_reasoning(text) + } + + fn parse_reasoning_streaming_incremental( + &mut self, + text: &str, + ) -> Result { + self.base.parse_reasoning_streaming_incremental(text) + } + + fn reset(&mut self) { + self.base.reset() + } + + fn model_type(&self) -> &str { + self.base.model_type() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_step3_initial_state() { + let mut parser = Step3Parser::new(); + + // Should treat text as reasoning even without start token + let result = parser + .detect_and_parse_reasoning("This is reasoning content") + .unwrap(); + assert_eq!(result.normal_text, ""); + assert_eq!(result.reasoning_text, "This is reasoning content"); + } + + #[test] + fn test_step3_with_end_token() { + let mut parser = Step3Parser::new(); + + // Should handle text with end token + let result = parser + .detect_and_parse_reasoning("reasoning contentanswer") + .unwrap(); + assert_eq!(result.normal_text, "answer"); + assert_eq!(result.reasoning_text, "reasoning content"); + } + + #[test] + fn test_step3_with_both_tokens() { + let mut parser = Step3Parser::new(); + + // Should handle both start and end tokens + let result = parser + .detect_and_parse_reasoning("reasoning contentanswer") + .unwrap(); + assert_eq!(result.normal_text, "answer"); + assert_eq!(result.reasoning_text, "reasoning content"); + } + + #[test] + fn test_step3_streaming() { + let mut parser = Step3Parser::new(); + + // First chunk - treated as reasoning (initial_in_reasoning=true) + let result1 = parser + .parse_reasoning_streaming_incremental("reasoning text ") + .unwrap(); + assert_eq!(result1.normal_text, ""); + assert_eq!(result1.reasoning_text, "reasoning text "); + + // Second chunk - continues reasoning until end token + let result2 = parser + .parse_reasoning_streaming_incremental("more reasoninganswer") + .unwrap(); + assert_eq!(result2.normal_text, "answer"); + assert_eq!(result2.reasoning_text, "more reasoning"); + } + + #[test] + fn test_model_type() { + let parser = Step3Parser::new(); + assert_eq!(parser.model_type(), "step3"); + } +}