[router] add glm and step3 reasoning parser (#9415)
This commit is contained in:
@@ -5,7 +5,8 @@ use std::collections::HashMap;
|
|||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
use crate::reasoning_parser::parsers::{
|
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};
|
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)
|
// Register Kimi parser with Unicode tokens (starts with in_reasoning=false)
|
||||||
registry.register_parser("kimi", || Box::new(KimiParser::new()));
|
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
|
// Register model patterns
|
||||||
registry.register_pattern("deepseek-r1", "deepseek_r1");
|
registry.register_pattern("deepseek-r1", "deepseek_r1");
|
||||||
registry.register_pattern("qwen3-thinking", "qwen3_thinking");
|
registry.register_pattern("qwen3-thinking", "qwen3_thinking");
|
||||||
registry.register_pattern("qwen-thinking", "qwen3_thinking");
|
registry.register_pattern("qwen-thinking", "qwen3_thinking");
|
||||||
registry.register_pattern("qwen3", "qwen3");
|
registry.register_pattern("qwen3", "qwen3");
|
||||||
registry.register_pattern("qwen", "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("kimi", "kimi");
|
||||||
registry.register_pattern("step3", "deepseek_r1"); // Step3 alias for DeepSeek-R1
|
registry.register_pattern("step3", "step3");
|
||||||
|
|
||||||
Self { registry }
|
Self { registry }
|
||||||
}
|
}
|
||||||
@@ -281,13 +288,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_alias_models() {
|
fn test_step3_model() {
|
||||||
let factory = ParserFactory::new();
|
let factory = ParserFactory::new();
|
||||||
let step3 = factory.create("step3-model").unwrap();
|
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");
|
#[test]
|
||||||
assert_eq!(glm45.model_type(), "qwen3");
|
fn test_glm45_model() {
|
||||||
|
let factory = ParserFactory::new();
|
||||||
|
let glm45 = factory.create("glm45-v2").unwrap();
|
||||||
|
assert_eq!(glm45.model_type(), "glm45");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pub mod traits;
|
|||||||
|
|
||||||
pub use factory::{ParserFactory, ParserRegistry, PooledParser};
|
pub use factory::{ParserFactory, ParserRegistry, PooledParser};
|
||||||
pub use parsers::{
|
pub use parsers::{
|
||||||
BaseReasoningParser, DeepSeekR1Parser, KimiParser, Qwen3Parser, QwenThinkingParser,
|
BaseReasoningParser, DeepSeekR1Parser, Glm45Parser, KimiParser, Qwen3Parser,
|
||||||
|
QwenThinkingParser, Step3Parser,
|
||||||
};
|
};
|
||||||
pub use traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
|
pub use traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
|
||||||
|
|||||||
118
sgl-router/src/reasoning_parser/parsers/glm45.rs
Normal file
118
sgl-router/src/reasoning_parser/parsers/glm45.rs
Normal file
@@ -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 (<think>...</think>) 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: "<think>".to_string(),
|
||||||
|
think_end_token: "</think>".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<ParserResult, ParseError> {
|
||||||
|
self.base.detect_and_parse_reasoning(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_reasoning_streaming_incremental(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
) -> Result<ParserResult, ParseError> {
|
||||||
|
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("<think>reasoning content</think>answer")
|
||||||
|
.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("<think>reasoning")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result2.normal_text, "");
|
||||||
|
assert_eq!(result2.reasoning_text, "reasoning");
|
||||||
|
|
||||||
|
// Third chunk - exits reasoning
|
||||||
|
let result3 = parser
|
||||||
|
.parse_reasoning_streaming_incremental("</think>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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
pub mod base;
|
pub mod base;
|
||||||
pub mod deepseek_r1;
|
pub mod deepseek_r1;
|
||||||
|
pub mod glm45;
|
||||||
pub mod kimi;
|
pub mod kimi;
|
||||||
pub mod qwen3;
|
pub mod qwen3;
|
||||||
|
pub mod step3;
|
||||||
|
|
||||||
pub use base::BaseReasoningParser;
|
pub use base::BaseReasoningParser;
|
||||||
pub use deepseek_r1::DeepSeekR1Parser;
|
pub use deepseek_r1::DeepSeekR1Parser;
|
||||||
|
pub use glm45::Glm45Parser;
|
||||||
pub use kimi::KimiParser;
|
pub use kimi::KimiParser;
|
||||||
pub use qwen3::{Qwen3Parser, QwenThinkingParser};
|
pub use qwen3::{Qwen3Parser, QwenThinkingParser};
|
||||||
|
pub use step3::Step3Parser;
|
||||||
|
|||||||
123
sgl-router/src/reasoning_parser/parsers/step3.rs
Normal file
123
sgl-router/src/reasoning_parser/parsers/step3.rs
Normal file
@@ -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 (<think>...</think>) 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: "<think>".to_string(),
|
||||||
|
think_end_token: "</think>".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<ParserResult, ParseError> {
|
||||||
|
self.base.detect_and_parse_reasoning(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_reasoning_streaming_incremental(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
) -> Result<ParserResult, ParseError> {
|
||||||
|
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 content</think>answer")
|
||||||
|
.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("<think>reasoning content</think>answer")
|
||||||
|
.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 reasoning</think>answer")
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user