use super::glm47::Glm47ReasoningParser;
use super::{ReasoningFormat, ReasoningParser};
pub(super) fn build(fmt: ReasoningFormat) -> Box<dyn ReasoningParser> {
match fmt {
ReasoningFormat::Qwen => Box::new(TagReasoningParser::THINK_PROMPT_OPENED.named("qwen")),
ReasoningFormat::DeepSeekR1 => {
Box::new(TagReasoningParser::THINK_PROMPT_OPENED.named("deepseek_r1"))
}
ReasoningFormat::MiniMax => {
Box::new(TagReasoningParser::THINK_PROMPT_OPENED.named("minimax"))
}
ReasoningFormat::Mistral => Box::new(TagReasoningParser {
name: "mistral",
start: "[THINK]",
end: "[/THINK]",
prompt_opens_think: false,
}),
ReasoningFormat::Gemma4 => Box::new(Gemma4ReasoningParser),
ReasoningFormat::Glm47 => Box::new(Glm47ReasoningParser),
}
}
pub(super) struct TagReasoningParser {
pub(super) name: &'static str,
pub(super) start: &'static str,
pub(super) end: &'static str,
pub(super) prompt_opens_think: bool,
}
impl TagReasoningParser {
const THINK_PROMPT_OPENED: Self = Self {
name: "qwen",
start: "<think>",
end: "</think>",
prompt_opens_think: true,
};
const fn named(self, name: &'static str) -> Self {
Self { name, ..self }
}
}
impl ReasoningParser for TagReasoningParser {
fn name(&self) -> &str {
self.name
}
fn start_tag(&self) -> &str {
self.start
}
fn end_tag(&self) -> &str {
self.end
}
fn extract_thinking(&self, text: &str, enable_thinking: bool) -> (Option<String>, String) {
extract_tag_thinking(
text,
self.start,
self.end,
self.prompt_opens_think,
enable_thinking,
)
}
}
pub(super) fn extract_tag_thinking(
text: &str,
start: &str,
end: &str,
prompt_opens_think: bool,
enable_thinking: bool,
) -> (Option<String>, String) {
let (pre, body, in_block): (&str, &str, bool) = if prompt_opens_think {
match text.trim_start().strip_prefix(start) {
Some(rest) => ("", rest, true),
None => ("", text, enable_thinking),
}
} else {
match text.find(start) {
Some(p) => (&text[..p], &text[p + start.len()..], true),
None => ("", text, false),
}
};
let (reasoning, content) = match body.find(end) {
Some(e) => {
let mut c = String::with_capacity(pre.len() + body.len() - e);
c.push_str(pre);
c.push_str(&body[e + end.len()..]);
(body[..e].to_string(), c)
}
None if in_block => (body.to_string(), pre.to_string()),
None => (String::new(), text.to_string()),
};
let content = strip_tag_pairs(&content, start, end);
let reasoning = reasoning.trim();
let content = content.trim().to_string();
if enable_thinking && !reasoning.is_empty() {
(Some(reasoning.to_string()), content)
} else {
(None, content)
}
}
fn strip_tag_pairs(s: &str, start: &str, end: &str) -> String {
let mut out = s.to_string();
while let Some(a) = out.find(start) {
match out[a + start.len()..].find(end) {
Some(rel) => {
let b = a + start.len() + rel + end.len();
out.replace_range(a..b, "");
}
None => break,
}
}
out
}
pub(super) struct Gemma4ReasoningParser;
impl Gemma4ReasoningParser {
const OPEN: &'static str = "<|channel>";
const CLOSE: &'static str = "<channel|>";
}
impl ReasoningParser for Gemma4ReasoningParser {
fn name(&self) -> &str {
"gemma4"
}
fn start_tag(&self) -> &str {
Self::OPEN
}
fn end_tag(&self) -> &str {
Self::CLOSE
}
fn extract_thinking(&self, text: &str, enable_thinking: bool) -> (Option<String>, String) {
let Some((before, after_open)) = text.split_once(Self::OPEN) else {
return (None, text.trim().to_string());
};
let after_label = after_open
.trim_start()
.strip_prefix("thought")
.unwrap_or(after_open);
match after_label.split_once(Self::CLOSE) {
Some((reasoning, answer)) => {
let reasoning = reasoning.trim();
let content = format!("{}{}", before.trim(), answer.trim());
let content = content.trim().to_string();
if enable_thinking && !reasoning.is_empty() {
(Some(reasoning.to_string()), content)
} else {
(None, content)
}
}
None => {
let reasoning = after_label.trim();
if enable_thinking && !reasoning.is_empty() {
(Some(reasoning.to_string()), before.trim().to_string())
} else {
(None, before.trim().to_string())
}
}
}
}
}