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");
+ }
+}