use std::collections::HashMap;
use std::sync::OnceLock;
mod base_parser;
mod gemma4_parser;
mod gpt_oss_parser;
mod granite_parser;
mod minimax_append_think_parser;
pub use base_parser::BasicReasoningParser;
pub use gemma4_parser::Gemma4ReasoningParser;
pub use gpt_oss_parser::{GptOssReasoningParser, harmony_terminator_token_ids};
pub use granite_parser::GraniteReasoningParser;
pub use minimax_append_think_parser::MiniMaxAppendThinkParser;
pub(crate) const KIMI_K2_TOOL_SECTION_BEGIN: &str = "<|tool_calls_section_begin|>";
static REASONING_PARSER_MAP: OnceLock<HashMap<&'static str, ReasoningParserType>> = OnceLock::new();
fn get_reasoning_parser_map() -> &'static HashMap<&'static str, ReasoningParserType> {
REASONING_PARSER_MAP.get_or_init(|| {
let mut map = HashMap::new();
map.insert("deepseek_r1", ReasoningParserType::DeepseekR1);
map.insert("deepseek_v3", ReasoningParserType::DeepseekR1);
map.insert("deepseek_v3_1", ReasoningParserType::DeepseekR1);
map.insert("deepseek_v3_2", ReasoningParserType::DeepseekR1);
map.insert("basic", ReasoningParserType::Basic);
map.insert("gpt_oss", ReasoningParserType::GptOss);
map.insert("qwen3", ReasoningParserType::Qwen);
map.insert("deepseek_v4", ReasoningParserType::DeepSeekV4);
map.insert("deepseek-v4", ReasoningParserType::DeepSeekV4);
map.insert("deepseekv4", ReasoningParserType::DeepSeekV4);
map.insert("nemotron_deci", ReasoningParserType::NemotronDeci);
map.insert("kimi", ReasoningParserType::Kimi);
map.insert("kimi_k25", ReasoningParserType::KimiK25);
map.insert("step3", ReasoningParserType::Step3);
map.insert("mistral", ReasoningParserType::Mistral);
map.insert("granite", ReasoningParserType::Granite);
map.insert("nemotron_nano", ReasoningParserType::DeepseekR1); map.insert("nemotron3", ReasoningParserType::DeepseekR1);
map.insert("nemotron_v3", ReasoningParserType::DeepseekR1);
map.insert("glm45", ReasoningParserType::NemotronDeci); map.insert(
"minimax_append_think",
ReasoningParserType::MiniMaxAppendThink,
);
map.insert("minimax_m3", ReasoningParserType::MiniMaxM3);
map.insert("minimax-m3", ReasoningParserType::MiniMaxM3);
map.insert("gemma4", ReasoningParserType::Gemma4);
map.insert("gemma-4", ReasoningParserType::Gemma4);
map
})
}
pub fn get_available_reasoning_parsers() -> Vec<&'static str> {
get_reasoning_parser_map().keys().copied().collect()
}
#[derive(Debug, Clone, Default)]
pub struct ParserResult {
pub normal_text: String,
pub reasoning_text: String,
}
impl ParserResult {
pub fn get_some_reasoning(&self) -> Option<String> {
if self.reasoning_text.is_empty() {
None
} else {
Some(self.reasoning_text.clone())
}
}
pub fn get_some_normal_text(&self) -> Option<String> {
if self.normal_text.is_empty() {
None
} else {
Some(self.normal_text.clone())
}
}
}
pub trait ReasoningParser: Send + std::fmt::Debug {
fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult;
fn parse_reasoning_streaming_incremental(
&mut self,
text: &str,
token_ids: &[u32],
) -> ParserResult;
fn finish_reasoning_stream(&mut self) -> ParserResult {
ParserResult::default()
}
fn set_in_reasoning(&mut self, _in_reasoning: bool) {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReasoningParserType {
DeepseekR1,
Step3,
Basic,
GptOss,
Qwen,
DeepSeekV4,
NemotronDeci,
Kimi,
KimiK25,
Mistral,
Granite,
MiniMaxAppendThink,
MiniMaxM3,
Gemma4,
}
#[derive(std::fmt::Debug)]
pub struct ReasoningParserWrapper {
parser: Box<dyn ReasoningParser>,
}
impl ReasoningParser for ReasoningParserWrapper {
fn detect_and_parse_reasoning(&mut self, text: &str, token_ids: &[u32]) -> ParserResult {
self.parser.detect_and_parse_reasoning(text, token_ids)
}
fn parse_reasoning_streaming_incremental(
&mut self,
text: &str,
token_ids: &[u32],
) -> ParserResult {
self.parser
.parse_reasoning_streaming_incremental(text, token_ids)
}
fn finish_reasoning_stream(&mut self) -> ParserResult {
self.parser.finish_reasoning_stream()
}
fn set_in_reasoning(&mut self, in_reasoning: bool) {
self.parser.set_in_reasoning(in_reasoning)
}
}
impl ReasoningParserType {
pub fn get_reasoning_parser(self) -> ReasoningParserWrapper {
let basic_parser =
BasicReasoningParser::new("<think>".into(), "</think>".into(), false, true);
let force_reasoning_basic_parser =
BasicReasoningParser::new("<think>".into(), "</think>".into(), true, true);
match self {
ReasoningParserType::DeepseekR1 => ReasoningParserWrapper {
parser: Box::new(force_reasoning_basic_parser),
},
ReasoningParserType::Step3 => ReasoningParserWrapper {
parser: Box::new(force_reasoning_basic_parser),
},
ReasoningParserType::Basic => ReasoningParserWrapper {
parser: Box::new(basic_parser),
},
ReasoningParserType::Qwen => ReasoningParserWrapper {
parser: Box::new(basic_parser),
},
ReasoningParserType::DeepSeekV4 => ReasoningParserWrapper {
parser: Box::new(basic_parser),
},
ReasoningParserType::NemotronDeci => ReasoningParserWrapper {
parser: Box::new(basic_parser),
},
ReasoningParserType::Kimi => ReasoningParserWrapper {
parser: Box::new(BasicReasoningParser::new(
"◁think▷".into(),
"◁/think▷".into(),
false,
true,
)),
},
ReasoningParserType::KimiK25 => ReasoningParserWrapper {
parser: Box::new(
BasicReasoningParser::new("<think>".into(), "</think>".into(), true, true)
.with_tool_start_token(KIMI_K2_TOOL_SECTION_BEGIN),
),
},
ReasoningParserType::Mistral => ReasoningParserWrapper {
parser: Box::new(BasicReasoningParser::new(
"[THINK]".into(),
"[/THINK]".into(),
true,
true,
)),
},
ReasoningParserType::GptOss => match GptOssReasoningParser::new() {
Ok(parser) => ReasoningParserWrapper {
parser: Box::new(parser),
},
Err(e) => {
tracing::warn!(
"GptOssReasoningParser could not be initialized, falling back to Basic Reasoning Parser: {e}"
);
ReasoningParserWrapper {
parser: Box::new(BasicReasoningParser::new(
"<think>".into(),
"</think>".into(),
false,
true,
)),
}
}
},
ReasoningParserType::Granite => ReasoningParserWrapper {
parser: Box::new(GraniteReasoningParser::new()),
},
ReasoningParserType::MiniMaxAppendThink => ReasoningParserWrapper {
parser: Box::new(MiniMaxAppendThinkParser::new()),
},
ReasoningParserType::MiniMaxM3 => ReasoningParserWrapper {
parser: Box::new(
BasicReasoningParser::new(
"<mm:think>".into(),
"</mm:think>".into(),
false,
true,
)
.with_dangling_end_recovery(),
),
},
ReasoningParserType::Gemma4 => ReasoningParserWrapper {
parser: Box::new(Gemma4ReasoningParser::new()),
},
}
}
pub fn get_reasoning_parser_from_name(name: &str) -> ReasoningParserWrapper {
tracing::debug!("Selected reasoning parser: {}", name);
let parser_map = get_reasoning_parser_map();
let normalized_name = name.to_lowercase();
match parser_map.get(normalized_name.as_str()) {
Some(parser_type) => parser_type.get_reasoning_parser(),
None => {
tracing::warn!(
parser_name = name,
"Unknown reasoning parser type, falling back to Basic Reasoning Parser",
);
Self::Basic.get_reasoning_parser()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn test_get_available_reasoning_parsers() {
let parsers = get_available_reasoning_parsers();
assert!(!parsers.is_empty());
let available_parsers = [
"deepseek_r1",
"deepseek_v3",
"deepseek_v3_1",
"deepseek_v3_2",
"basic",
"gpt_oss",
"qwen3",
"deepseek_v4",
"deepseek-v4",
"deepseekv4",
"nemotron_deci",
"kimi",
"kimi_k25",
"step3",
"mistral",
"granite",
"nemotron_nano",
"nemotron3",
"nemotron_v3",
"glm45",
"minimax_append_think",
"minimax_m3",
"minimax-m3",
"gemma4",
"gemma-4",
];
for parser in available_parsers {
assert!(parsers.contains(&parser));
}
}
#[test] fn test_minimax_m3_detect_and_parse() {
for parser_name in ["minimax_m3", "minimax-m3"] {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name(parser_name);
let result =
parser.detect_and_parse_reasoning("<mm:think>thinking</mm:think>answer", &[]);
assert_eq!(result.reasoning_text, "thinking");
assert_eq!(result.normal_text, "answer");
}
}
#[test] fn test_minimax_m3_streaming() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let r1 = parser.parse_reasoning_streaming_incremental("<mm:think>rea", &[]);
let r2 = parser.parse_reasoning_streaming_incremental("son</mm:think>answer", &[]);
assert_eq!(
format!("{}{}", r1.reasoning_text, r2.reasoning_text),
"reason"
);
assert_eq!(format!("{}{}", r1.normal_text, r2.normal_text), "answer");
}
#[test] fn test_minimax_m3_streaming_with_prompt_prefilled_start_marker() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
parser.set_in_reasoning(true);
let result = parser.parse_reasoning_streaming_incremental("reason</mm:think>answer", &[]);
assert_eq!(result.reasoning_text, "reason");
assert_eq!(result.normal_text, "answer");
}
#[test] fn test_minimax_m3_detect_and_parse_dangling_end_marker() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let result = parser.detect_and_parse_reasoning("reason</mm:think>answer", &[]);
assert_eq!(result.reasoning_text, "reason");
assert_eq!(result.normal_text, "answer");
}
#[test] fn test_minimax_m3_streaming_dangling_end_marker() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let r1 = parser.parse_reasoning_streaming_incremental("reason</mm:", &[]);
let r2 = parser.parse_reasoning_streaming_incremental("think>answer", &[]);
assert_eq!(
format!("{}{}", r1.reasoning_text, r2.reasoning_text),
"reason"
);
assert_eq!(format!("{}{}", r1.normal_text, r2.normal_text), "answer");
}
#[test] fn test_minimax_m3_streaming_close_marker_only_is_stripped() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let result = parser.parse_reasoning_streaming_incremental(
"</mm:think>I'll check the content of both files.",
&[],
);
assert_eq!(result.reasoning_text, "");
assert_eq!(result.normal_text, "I'll check the content of both files.");
}
#[test] fn test_minimax_m3_detect_and_parse_multiple_spans() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let result = parser.detect_and_parse_reasoning(
"<mm:think>first</mm:think> middle <mm:think>second</mm:think> done",
&[],
);
assert_eq!(result.reasoning_text, "firstsecond");
assert_eq!(result.normal_text, "middle done");
}
#[test] fn test_minimax_m3_streaming_prompt_prefilled_close_after_complete_span() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let r1 = parser.parse_reasoning_streaming_incremental("<mm:think>preamble</mm:think>", &[]);
let r2 = parser.parse_reasoning_streaming_incremental("body</mm:think>", &[]);
let r3 = parser.parse_reasoning_streaming_incremental("answer", &[]);
assert_eq!(
format!(
"{}{}{}",
r1.reasoning_text, r2.reasoning_text, r3.reasoning_text
),
"preamblebody"
);
assert_eq!(
format!("{}{}{}", r1.normal_text, r2.normal_text, r3.normal_text),
"answer"
);
}
#[test] fn test_minimax_m3_streaming_partial_start_prefix_becomes_normal_text() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("minimax_m3");
let r1 = parser.parse_reasoning_streaming_incremental("plain <mm:th", &[]);
let r2 = parser.parse_reasoning_streaming_incremental("esis answer", &[]);
assert_eq!(format!("{}{}", r1.reasoning_text, r2.reasoning_text), "");
assert_eq!(
format!("{}{}", r1.normal_text, r2.normal_text),
"plain <mm:thesis answer"
);
}
#[test] fn test_deepseek_v4_detect_and_parse() {
for parser_name in ["deepseek_v4", "deepseek-v4", "deepseekv4"] {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name(parser_name);
let result = parser.detect_and_parse_reasoning("<think>thinking</think>answer", &[]);
assert_eq!(result.reasoning_text, "thinking");
assert_eq!(result.normal_text, "answer");
}
}
#[test] fn test_deepseek_v4_no_forced_reasoning_without_tags() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
let result = parser.detect_and_parse_reasoning("answer only", &[]);
assert_eq!(result.reasoning_text, "");
assert_eq!(result.normal_text, "answer only");
}
#[test] fn test_deepseek_v4_streaming() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
let chunks = ["<think>rea", "son</think>answer"];
let mut reasoning = String::new();
let mut normal = String::new();
for chunk in chunks {
let result = parser.parse_reasoning_streaming_incremental(chunk, &[]);
reasoning.push_str(&result.reasoning_text);
normal.push_str(&result.normal_text);
}
assert_eq!(reasoning, "reason");
assert_eq!(normal, "answer");
}
#[test] fn test_kimi_k25_detect_and_parse() {
let cases = [
(
"force reasoning: no think tags",
"no think tags here",
"no think tags here",
"",
),
(
"standard think tags",
"<think>Let me reason about this.</think>Hello!",
"Let me reason about this.",
"Hello!",
),
(
"empty think block (instant mode)",
"<think></think>Hello from instant mode!",
"",
"Hello from instant mode!",
),
(
"empty think block with newline",
"<think>\n</think>Hello from instant mode!",
"",
"Hello from instant mode!",
),
];
for (desc, input, expected_reasoning, expected_normal) in cases {
let mut parser = ReasoningParserType::KimiK25.get_reasoning_parser();
let result = parser.detect_and_parse_reasoning(input, &[]);
assert_eq!(
result.reasoning_text, expected_reasoning,
"FAILED reasoning: {desc}"
);
assert_eq!(result.normal_text, expected_normal, "FAILED normal: {desc}");
}
}
#[test] fn test_kimi_k25_streaming_force_reasoning() {
let mut parser = ReasoningParserType::KimiK25.get_reasoning_parser();
let r1 = parser.parse_reasoning_streaming_incremental("<thi", &[]);
assert_eq!(r1.reasoning_text, "");
assert_eq!(r1.normal_text, "");
let r2 = parser.parse_reasoning_streaming_incremental("nk>reasoning here", &[]);
assert_eq!(r2.reasoning_text, "reasoning here");
assert_eq!(r2.normal_text, "");
let r3 = parser.parse_reasoning_streaming_incremental("</think>Hello!", &[]);
assert_eq!(r3.reasoning_text, "");
assert_eq!(r3.normal_text, "Hello!");
}
#[test] fn test_kimi_k25_streaming() {
let cases: Vec<(&str, &[&str], &str, &str)> = vec![
(
"complete response",
&[
"<think>",
"I need to",
" think about",
" this carefully.",
"</think>",
"Bonjour",
"!",
],
"I need to think about this carefully.",
"Bonjour!",
),
(
"empty think (instant mode)",
&["<think>", "</think>", "Direct answer."],
"",
"Direct answer.",
),
];
for (desc, tokens, expected_reasoning, expected_content) in cases {
let mut parser = ReasoningParserType::KimiK25.get_reasoning_parser();
let mut all_reasoning = String::new();
let mut all_content = String::new();
for token in tokens {
let r = parser.parse_reasoning_streaming_incremental(token, &[]);
all_reasoning.push_str(&r.reasoning_text);
all_content.push_str(&r.normal_text);
}
assert_eq!(
all_reasoning, expected_reasoning,
"FAILED reasoning: {desc}"
);
assert_eq!(all_content, expected_content, "FAILED content: {desc}");
}
}
#[test] fn test_kimi_k25_parser_lookup_by_name() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("kimi_k25");
let result = parser.detect_and_parse_reasoning("<think>thinking</think>answer", &[]);
assert_eq!(result.reasoning_text, "thinking");
assert_eq!(result.normal_text, "answer");
}
#[test] fn test_kimi_vs_kimi_k25_different_tags() {
let mut kimi = ReasoningParserType::Kimi.get_reasoning_parser();
let mut kimi_k25 = ReasoningParserType::KimiK25.get_reasoning_parser();
let r_kimi = kimi.detect_and_parse_reasoning("<think>reasoning</think>answer", &[]);
assert_eq!(r_kimi.normal_text, "<think>reasoning</think>answer");
assert_eq!(r_kimi.reasoning_text, "");
let r_k25 = kimi_k25.detect_and_parse_reasoning("<think>reasoning</think>answer", &[]);
assert_eq!(r_k25.reasoning_text, "reasoning");
assert_eq!(r_k25.normal_text, "answer");
}
#[test] fn test_nemotron_streaming_with_set_in_reasoning() {
let mut parser = ReasoningParserType::DeepseekR1.get_reasoning_parser();
parser.set_in_reasoning(true);
let tokens = &["Think", "ing about", " this", ".\n\n", "</think>", "Four"];
let mut all_reasoning = String::new();
let mut all_content = String::new();
for token in tokens {
let r = parser.parse_reasoning_streaming_incremental(token, &[]);
all_reasoning.push_str(&r.reasoning_text);
all_content.push_str(&r.normal_text);
}
assert_eq!(all_reasoning, "Thinking about this.\n\n");
assert_eq!(all_content, "Four");
}
#[test] fn test_nemotron_streaming_force_reasoning_without_set_in_reasoning() {
let mut parser = ReasoningParserType::DeepseekR1.get_reasoning_parser();
let tokens = &["Think", "ing about", " this", ".\n\n", "</think>", "Four"];
let mut all_reasoning = String::new();
let mut all_content = String::new();
for token in tokens {
let r = parser.parse_reasoning_streaming_incremental(token, &[]);
all_reasoning.push_str(&r.reasoning_text);
all_content.push_str(&r.normal_text);
}
assert_eq!(all_reasoning, "Thinking about this.\n\n");
assert_eq!(all_content, "Four");
}
#[test] fn test_nemotron_streaming_split_end_think_tokens() {
let mut parser = ReasoningParserType::DeepseekR1.get_reasoning_parser();
parser.set_in_reasoning(true);
let tokens = &[
"reason", "ing", " done", ".", "</", "think", ">", "Hello", " world",
];
let mut all_reasoning = String::new();
let mut all_content = String::new();
for token in tokens {
let r = parser.parse_reasoning_streaming_incremental(token, &[]);
all_reasoning.push_str(&r.reasoning_text);
all_content.push_str(&r.normal_text);
}
assert_eq!(all_reasoning, "reasoning done.");
assert_eq!(all_content, "Hello world");
}
#[test] fn test_nemotron_v3_detect_and_parse_vllm_cases() {
let cases = [
(
"without start token",
"This is a reasoning section</think>This is the rest",
"This is a reasoning section",
"This is the rest",
),
(
"with start token",
"<think>This is a reasoning section</think>This is the rest",
"This is a reasoning section",
"This is the rest",
),
];
for (desc, input, expected_reasoning, expected_content) in cases {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("nemotron_v3");
let result = parser.detect_and_parse_reasoning(input, &[]);
assert_eq!(
result.reasoning_text, expected_reasoning,
"FAILED reasoning: {desc}"
);
assert_eq!(
result.normal_text, expected_content,
"FAILED content: {desc}"
);
}
}
#[test] fn test_nemotron_v3_streaming_vllm_cases() {
let cases: Vec<(&str, &[&str], &str, &str)> = vec![
(
"without start token",
&[
"This is a reasoning section",
"</think>",
"This is the rest",
],
"This is a reasoning section",
"This is the rest",
),
(
"with start token",
&[
"<think>",
"This is a reasoning section",
"</think>",
"This is the rest",
],
"This is a reasoning section",
"This is the rest",
),
];
for (desc, tokens, expected_reasoning, expected_content) in cases {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("nemotron_v3");
let mut all_reasoning = String::new();
let mut all_content = String::new();
for token in tokens {
let result = parser.parse_reasoning_streaming_incremental(token, &[]);
all_reasoning.push_str(&result.reasoning_text);
all_content.push_str(&result.normal_text);
}
assert_eq!(
all_reasoning, expected_reasoning,
"FAILED reasoning: {desc}"
);
assert_eq!(all_content, expected_content, "FAILED content: {desc}");
}
}
#[test]
fn test_deepseek_v4_streaming_with_set_in_reasoning() {
let mut parser = ReasoningParserType::get_reasoning_parser_from_name("deepseek_v4");
parser.set_in_reasoning(true);
let tokens = &[
"Wei", "gh", "ing ", "options", ".", "</think>", "Bei", "jing", " is", " sunny.",
];
let mut all_reasoning = String::new();
let mut all_content = String::new();
for token in tokens {
let r = parser.parse_reasoning_streaming_incremental(token, &[]);
all_reasoning.push_str(&r.reasoning_text);
all_content.push_str(&r.normal_text);
}
assert_eq!(all_reasoning, "Weighing options.");
assert_eq!(all_content, "Beijing is sunny.");
}
}