kernel/capabilities/
think_splitter.rs1use crate::capabilities::held_suffix_len;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Piece {
9 Text(String),
11 Thinking(String),
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct TagPair {
18 pub open: String,
20 pub close: String,
22}
23
24pub fn default_pairs() -> Vec<TagPair> {
26 vec![
27 TagPair {
28 open: "<think>".to_owned(),
29 close: "</think>".to_owned(),
30 },
31 TagPair {
32 open: "<|START_THINKING|>".to_owned(),
33 close: "<|END_THINKING|>".to_owned(),
34 },
35 ]
36}
37
38pub fn has_visible_tags(text: &str) -> bool {
40 default_pairs()
41 .iter()
42 .any(|pair| text.contains(&pair.open) || text.contains(&pair.close))
43}
44
45enum Mode {
46 Text,
47 Thinking(String),
48}
49
50pub struct ThinkSplitter {
52 pairs: Vec<TagPair>,
53 open_tags: Vec<String>,
54 mode: Mode,
55 buffer: String,
56}
57
58impl Default for ThinkSplitter {
59 fn default() -> Self {
60 Self::with_pairs(default_pairs())
61 }
62}
63
64impl ThinkSplitter {
65 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn with_pairs(pairs: Vec<TagPair>) -> Self {
74 let pairs: Vec<TagPair> = pairs
75 .into_iter()
76 .filter(|pair| !pair.open.is_empty() && !pair.close.is_empty())
77 .collect();
78 let open_tags = pairs.iter().map(|pair| pair.open.clone()).collect();
79 Self {
80 pairs,
81 open_tags,
82 mode: Mode::Text,
83 buffer: String::new(),
84 }
85 }
86
87 pub fn feed(&mut self, chunk: &str) -> Vec<Piece> {
90 self.buffer.push_str(chunk);
91 let mut output = Vec::new();
92 loop {
93 match &self.mode {
94 Mode::Text => {
95 let opening = self
96 .pairs
97 .iter()
98 .filter_map(|pair| {
99 self.buffer
100 .find(&pair.open)
101 .map(|at| (at, pair.open.len(), &pair.close))
102 })
103 .min_by_key(|(at, _, _)| *at);
104 match opening {
105 Some((at, open_len, close)) => {
106 let close = close.clone();
107 let before = self.buffer[..at].to_owned();
108 if !before.is_empty() {
109 output.push(Piece::Text(before));
110 }
111 self.buffer.drain(..at + open_len);
112 self.mode = Mode::Thinking(close);
113 }
114 None => {
115 Self::drain_prefix(
116 &mut self.buffer,
117 &self.open_tags,
118 false,
119 &mut output,
120 );
121 break;
122 }
123 }
124 }
125 Mode::Thinking(close) => {
126 let close = close.clone();
127 match self.buffer.find(&close) {
128 Some(at) => {
129 let before = self.buffer[..at].to_owned();
130 if !before.is_empty() {
131 output.push(Piece::Thinking(before));
132 }
133 self.buffer.drain(..at + close.len());
134 self.mode = Mode::Text;
135 }
136 None => {
137 Self::drain_prefix(&mut self.buffer, &[close], true, &mut output);
138 break;
139 }
140 }
141 }
142 }
143 }
144 output
145 }
146
147 pub fn flush(&mut self) -> Vec<Piece> {
149 if self.buffer.is_empty() {
150 return Vec::new();
151 }
152 let buffer = std::mem::take(&mut self.buffer);
153 match &self.mode {
154 Mode::Thinking(_) => vec![Piece::Thinking(buffer)],
155 Mode::Text => vec![Piece::Text(buffer)],
156 }
157 }
158
159 fn drain_prefix(buffer: &mut String, tags: &[String], thinking: bool, output: &mut Vec<Piece>) {
160 let emit_len = buffer.len() - held_suffix_len(buffer, tags);
161 if emit_len > 0 {
162 let emit = buffer[..emit_len].to_owned();
163 output.push(if thinking {
164 Piece::Thinking(emit)
165 } else {
166 Piece::Text(emit)
167 });
168 buffer.drain(..emit_len);
169 }
170 }
171}