Unverified Commit 94959237 authored by Simo Lin's avatar Simo Lin Committed by GitHub
Browse files

[router] add dsr1, kimi, and qwen reasoning parser (#9353)

parent f4fafacc
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use crate::reasoning_parser::parsers::BaseReasoningParser; use crate::reasoning_parser::parsers::{
BaseReasoningParser, DeepSeekR1Parser, KimiParser, Qwen3Parser, QwenThinkingParser,
};
use crate::reasoning_parser::traits::{ParseError, ParserConfig, ReasoningParser}; use crate::reasoning_parser::traits::{ParseError, ParserConfig, ReasoningParser};
/// Type alias for parser creator functions. /// Type alias for parser creator functions.
...@@ -82,53 +84,17 @@ impl ParserFactory { ...@@ -82,53 +84,17 @@ impl ParserFactory {
Box::new(BaseReasoningParser::new(ParserConfig::default())) Box::new(BaseReasoningParser::new(ParserConfig::default()))
}); });
// Register DeepSeek-R1 parser // Register DeepSeek-R1 parser (starts with in_reasoning=true)
registry.register_parser("deepseek_r1", || { registry.register_parser("deepseek_r1", || Box::new(DeepSeekR1Parser::new()));
let config = ParserConfig {
think_start_token: "<think>".to_string(),
think_end_token: "</think>".to_string(),
force_reasoning: true,
stream_reasoning: true,
max_buffer_size: 65536,
};
Box::new(BaseReasoningParser::new(config).with_model_type("deepseek_r1".to_string()))
});
// Register Qwen3 parser // Register Qwen3 parser (starts with in_reasoning=false)
registry.register_parser("qwen3", || { registry.register_parser("qwen3", || Box::new(Qwen3Parser::new()));
let config = ParserConfig {
think_start_token: "<think>".to_string(),
think_end_token: "</think>".to_string(),
force_reasoning: false,
stream_reasoning: true,
max_buffer_size: 65536,
};
Box::new(BaseReasoningParser::new(config).with_model_type("qwen3".to_string()))
});
// Register Qwen3-thinking parser (forced reasoning) // Register Qwen3-thinking parser (starts with in_reasoning=true)
registry.register_parser("qwen3_thinking", || { registry.register_parser("qwen3_thinking", || Box::new(QwenThinkingParser::new()));
let config = ParserConfig {
think_start_token: "<think>".to_string(),
think_end_token: "</think>".to_string(),
force_reasoning: true,
stream_reasoning: true,
max_buffer_size: 65536,
};
Box::new(BaseReasoningParser::new(config).with_model_type("qwen3_thinking".to_string()))
});
// Register Kimi parser with Unicode tokens // Register Kimi parser with Unicode tokens (starts with in_reasoning=false)
registry.register_parser("kimi", || { registry.register_parser("kimi", || Box::new(KimiParser::new()));
let config = ParserConfig {
think_start_token: "◁think▷".to_string(),
think_end_token: "◁/think▷".to_string(),
force_reasoning: false,
stream_reasoning: true,
max_buffer_size: 65536,
};
Box::new(BaseReasoningParser::new(config).with_model_type("kimi".to_string()))
});
// Register model patterns // Register model patterns
registry.register_pattern("deepseek-r1", "deepseek_r1"); registry.register_pattern("deepseek-r1", "deepseek_r1");
...@@ -155,9 +121,9 @@ impl ParserFactory { ...@@ -155,9 +121,9 @@ impl ParserFactory {
let config = ParserConfig { let config = ParserConfig {
think_start_token: "".to_string(), think_start_token: "".to_string(),
think_end_token: "".to_string(), think_end_token: "".to_string(),
force_reasoning: false,
stream_reasoning: true, stream_reasoning: true,
max_buffer_size: 65536, max_buffer_size: 65536,
initial_in_reasoning: false,
}; };
Ok(Box::new( Ok(Box::new(
BaseReasoningParser::new(config).with_model_type("passthrough".to_string()), BaseReasoningParser::new(config).with_model_type("passthrough".to_string()),
......
...@@ -3,5 +3,7 @@ pub mod parsers; ...@@ -3,5 +3,7 @@ pub mod parsers;
pub mod traits; pub mod traits;
pub use factory::{ParserFactory, ParserRegistry}; pub use factory::{ParserFactory, ParserRegistry};
pub use parsers::BaseReasoningParser; pub use parsers::{
pub use traits::{ParseError, ParserResult, ReasoningParser}; BaseReasoningParser, DeepSeekR1Parser, KimiParser, Qwen3Parser, QwenThinkingParser,
};
pub use traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
...@@ -20,7 +20,7 @@ pub struct BaseReasoningParser { ...@@ -20,7 +20,7 @@ pub struct BaseReasoningParser {
impl BaseReasoningParser { impl BaseReasoningParser {
/// Create a new BaseReasoningParser with the given configuration. /// Create a new BaseReasoningParser with the given configuration.
pub fn new(config: ParserConfig) -> Self { pub fn new(config: ParserConfig) -> Self {
let in_reasoning = config.force_reasoning; let in_reasoning = config.initial_in_reasoning;
Self { Self {
config, config,
in_reasoning, in_reasoning,
...@@ -179,7 +179,7 @@ impl ReasoningParser for BaseReasoningParser { ...@@ -179,7 +179,7 @@ impl ReasoningParser for BaseReasoningParser {
} }
fn reset(&mut self) { fn reset(&mut self) {
self.in_reasoning = self.config.force_reasoning; self.in_reasoning = self.config.initial_in_reasoning;
self.buffer.clear(); self.buffer.clear();
self.stripped_think_start = false; self.stripped_think_start = false;
} }
...@@ -193,13 +193,16 @@ impl ReasoningParser for BaseReasoningParser { ...@@ -193,13 +193,16 @@ impl ReasoningParser for BaseReasoningParser {
mod tests { mod tests {
use super::*; use super::*;
fn create_test_parser(force_reasoning: bool, stream_reasoning: bool) -> BaseReasoningParser { fn create_test_parser(
initial_in_reasoning: bool,
stream_reasoning: bool,
) -> BaseReasoningParser {
let config = ParserConfig { let config = ParserConfig {
think_start_token: "<think>".to_string(), think_start_token: "<think>".to_string(),
think_end_token: "</think>".to_string(), think_end_token: "</think>".to_string(),
force_reasoning,
stream_reasoning, stream_reasoning,
max_buffer_size: 65536, max_buffer_size: 65536,
initial_in_reasoning,
}; };
BaseReasoningParser::new(config) BaseReasoningParser::new(config)
} }
...@@ -265,7 +268,8 @@ mod tests { ...@@ -265,7 +268,8 @@ mod tests {
} }
#[test] #[test]
fn test_force_reasoning_mode() { fn test_initial_in_reasoning_true() {
// Parser starts with in_reasoning=true (like DeepSeek-R1)
let mut parser = create_test_parser(true, true); let mut parser = create_test_parser(true, true);
let result = parser let result = parser
.detect_and_parse_reasoning("no think tags here") .detect_and_parse_reasoning("no think tags here")
......
// DeepSeek-R1 specific reasoning parser.
// This parser starts with in_reasoning=true, assuming all text is reasoning
// until an end token is encountered.
use crate::reasoning_parser::parsers::BaseReasoningParser;
use crate::reasoning_parser::traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
/// DeepSeek-R1 reasoning parser.
///
/// This parser assumes reasoning from the start of text (in_reasoning=true)
/// and uses <think> and </think> tokens.
pub struct DeepSeekR1Parser {
base: BaseReasoningParser,
}
impl DeepSeekR1Parser {
/// Create a new DeepSeek-R1 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, // Always starts with reasoning
};
Self {
base: BaseReasoningParser::new(config).with_model_type("deepseek_r1".to_string()),
}
}
}
impl Default for DeepSeekR1Parser {
fn default() -> Self {
Self::new()
}
}
impl ReasoningParser for DeepSeekR1Parser {
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_deepseek_r1_initial_state() {
let mut parser = DeepSeekR1Parser::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_deepseek_r1_with_end_token() {
let mut parser = DeepSeekR1Parser::new();
// Should extract reasoning until end token
let result = parser
.detect_and_parse_reasoning("reasoning content</think>normal content")
.unwrap();
assert_eq!(result.normal_text, "normal content");
assert_eq!(result.reasoning_text, "reasoning content");
}
#[test]
fn test_deepseek_r1_streaming() {
let mut parser = DeepSeekR1Parser::new();
// First chunk - all reasoning
let result1 = parser
.parse_reasoning_streaming_incremental("thinking about")
.unwrap();
assert_eq!(result1.reasoning_text, "thinking about");
assert_eq!(result1.normal_text, "");
// Second chunk - ends reasoning
let result2 = parser
.parse_reasoning_streaming_incremental(" the problem</think>answer")
.unwrap();
assert_eq!(result2.reasoning_text, "the problem"); // Text is trimmed
assert_eq!(result2.normal_text, "answer");
}
#[test]
fn test_model_type() {
let parser = DeepSeekR1Parser::new();
assert_eq!(parser.model_type(), "deepseek_r1");
}
}
// Kimi specific reasoning parser.
// This parser uses Unicode tokens and starts with in_reasoning=false.
use crate::reasoning_parser::parsers::BaseReasoningParser;
use crate::reasoning_parser::traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
/// Kimi reasoning parser.
///
/// This parser uses Unicode tokens (◁think▷ and ◁/think▷) and requires
/// explicit start tokens to enter reasoning mode.
pub struct KimiParser {
base: BaseReasoningParser,
}
impl KimiParser {
/// Create a new Kimi 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
};
Self {
base: BaseReasoningParser::new(config).with_model_type("kimi".to_string()),
}
}
}
impl Default for KimiParser {
fn default() -> Self {
Self::new()
}
}
impl ReasoningParser for KimiParser {
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_kimi_initial_state() {
let mut parser = KimiParser::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_kimi_with_unicode_tokens() {
let mut parser = KimiParser::new();
// Should extract reasoning with Unicode 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_kimi_partial_unicode() {
let mut parser = KimiParser::new();
// Test partial Unicode token buffering
let result1 = parser
.parse_reasoning_streaming_incremental("◁thi")
.unwrap();
assert_eq!(result1.normal_text, "");
assert_eq!(result1.reasoning_text, "");
// Complete the token
let result2 = parser
.parse_reasoning_streaming_incremental("nk▷reasoning")
.unwrap();
assert_eq!(result2.normal_text, "");
assert_eq!(result2.reasoning_text, "reasoning");
}
#[test]
fn test_kimi_streaming() {
let mut parser = KimiParser::new();
// Normal text first
let result1 = parser
.parse_reasoning_streaming_incremental("normal ")
.unwrap();
assert_eq!(result1.normal_text, "normal ");
assert_eq!(result1.reasoning_text, "");
// Enter reasoning with Unicode token
let result2 = parser
.parse_reasoning_streaming_incremental("◁think▷thinking")
.unwrap();
assert_eq!(result2.normal_text, "");
assert_eq!(result2.reasoning_text, "thinking");
// Exit reasoning
let result3 = parser
.parse_reasoning_streaming_incremental("◁/think▷answer")
.unwrap();
assert_eq!(result3.normal_text, "answer");
assert_eq!(result3.reasoning_text, ""); // Already returned in stream mode
}
#[test]
fn test_model_type() {
let parser = KimiParser::new();
assert_eq!(parser.model_type(), "kimi");
}
}
pub mod base; pub mod base;
pub mod deepseek_r1;
pub mod kimi;
pub mod qwen3;
pub use base::BaseReasoningParser; pub use base::BaseReasoningParser;
pub use deepseek_r1::DeepSeekR1Parser;
pub use kimi::KimiParser;
pub use qwen3::{Qwen3Parser, QwenThinkingParser};
// Qwen3 specific reasoning parser.
// This parser starts with in_reasoning=false, requiring an explicit
// start token to enter reasoning mode.
use crate::reasoning_parser::parsers::BaseReasoningParser;
use crate::reasoning_parser::traits::{ParseError, ParserConfig, ParserResult, ReasoningParser};
/// Qwen3 reasoning parser.
///
/// This parser requires explicit <think> tokens to enter reasoning mode
/// (in_reasoning=false initially).
pub struct Qwen3Parser {
base: BaseReasoningParser,
}
impl Qwen3Parser {
/// Create a new Qwen3 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
};
Self {
base: BaseReasoningParser::new(config).with_model_type("qwen3".to_string()),
}
}
}
impl Default for Qwen3Parser {
fn default() -> Self {
Self::new()
}
}
impl ReasoningParser for Qwen3Parser {
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()
}
}
/// QwenThinking parser - variant that assumes reasoning from start.
///
/// This is for qwen*thinking models that behave like DeepSeek-R1.
pub struct QwenThinkingParser {
base: BaseReasoningParser,
}
impl QwenThinkingParser {
/// Create a new QwenThinking 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
};
Self {
base: BaseReasoningParser::new(config).with_model_type("qwen_thinking".to_string()),
}
}
}
impl Default for QwenThinkingParser {
fn default() -> Self {
Self::new()
}
}
impl ReasoningParser for QwenThinkingParser {
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_qwen3_initial_state() {
let mut parser = Qwen3Parser::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_qwen3_with_tokens() {
let mut parser = Qwen3Parser::new();
// Should extract reasoning with proper tokens
let result = parser
.detect_and_parse_reasoning("<think>reasoning</think>answer")
.unwrap();
assert_eq!(result.normal_text, "answer");
assert_eq!(result.reasoning_text, "reasoning");
}
#[test]
fn test_qwen_thinking_initial_state() {
let mut parser = QwenThinkingParser::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_qwen3_streaming() {
let mut parser = Qwen3Parser::new();
// First chunk - normal text (no start token yet)
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");
}
#[test]
fn test_model_types() {
let qwen3 = Qwen3Parser::new();
assert_eq!(qwen3.model_type(), "qwen3");
let qwen_thinking = QwenThinkingParser::new();
assert_eq!(qwen_thinking.model_type(), "qwen_thinking");
}
}
...@@ -96,14 +96,14 @@ pub struct ParserConfig { ...@@ -96,14 +96,14 @@ pub struct ParserConfig {
/// The token that marks the end of reasoning content. /// The token that marks the end of reasoning content.
pub think_end_token: String, pub think_end_token: String,
/// Whether to force all text to be treated as reasoning.
pub force_reasoning: bool,
/// Whether to stream reasoning content as it arrives. /// Whether to stream reasoning content as it arrives.
pub stream_reasoning: bool, pub stream_reasoning: bool,
/// Maximum buffer size in bytes. /// Maximum buffer size in bytes.
pub max_buffer_size: usize, pub max_buffer_size: usize,
/// Initial state for in_reasoning flag (fixed per parser type).
pub initial_in_reasoning: bool,
} }
impl Default for ParserConfig { impl Default for ParserConfig {
...@@ -111,9 +111,9 @@ impl Default for ParserConfig { ...@@ -111,9 +111,9 @@ impl Default for ParserConfig {
Self { Self {
think_start_token: "<think>".to_string(), think_start_token: "<think>".to_string(),
think_end_token: "</think>".to_string(), think_end_token: "</think>".to_string(),
force_reasoning: false,
stream_reasoning: true, stream_reasoning: true,
max_buffer_size: 65536, // 64KB default max_buffer_size: 65536, // 64KB default
initial_in_reasoning: false, // Default to false (explicit reasoning)
} }
} }
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment