deepseek_recipe/stream/
processor.rs1use std::collections::VecDeque;
2use std::pin::pin;
3
4use async_stream::stream;
5use tokio_stream::{Stream, StreamExt};
6
7use super::decoder::{StreamDecoder, TokenizerDecoder};
8use super::inference::{
9 CompletionUsage, FinishReason, InferenceChunk, InferenceFinishReason, PromptUsage,
10};
11use super::state_machine::{OutputAction, OutputActionSegment, ParsingOptions, StateMachine};
12use super::{ChunkGenerator, OutputChunk, StreamError};
13
14pub struct StreamProcessor<G> {
16 generator: G,
17 options: ParsingOptions,
18 decoder: Option<StreamDecoder>,
19}
20
21impl<G> StreamProcessor<G>
22where
23 G: ChunkGenerator,
24{
25 pub fn new(generator: G, options: ParsingOptions) -> Self {
27 Self {
28 generator,
29 options,
30 decoder: None,
31 }
32 }
33
34 pub fn with_tokenizer(mut self, decoder: impl TokenizerDecoder + 'static) -> Self {
42 self.decoder = Some(StreamDecoder::new(Box::new(decoder)));
43 self
44 }
45
46 pub fn process(
56 self,
57 inference: impl Stream<Item = InferenceChunk> + Send,
58 ) -> impl Stream<Item = Result<G::Chunk, StreamError>> + Send
59 where
60 G::Chunk: Send,
61 {
62 let mut generator = self.generator;
63 let options = self.options;
64 let mut decoder = self.decoder;
65 stream! {
66 let mut inference = pin!(inference);
67 let mut state_machine = StateMachine::new(options);
68 let mut stashed = StashedChunks::new();
69
70 let mut started = false;
71 let mut prompt_usage = PromptUsage::default();
72 let mut completion_usage = CompletionUsage::default();
73 let mut backend_finish = None;
74 let mut stop_sequence = None;
75
76 while let Some(chunk) = inference.next().await {
77 let (content, content_tokens) = match chunk {
78 InferenceChunk::Ready {
79 system_fingerprint,
80 prompt_usage: ready_usage,
81 } => {
82 prompt_usage = ready_usage;
83 if !started {
84 for out in generator.generate(OutputChunk::Start { system_fingerprint, usage: prompt_usage }).await {
85 yield Ok(out);
86 }
87 started = true;
88 }
89 continue;
90 }
91 InferenceChunk::Finish { finish_reason } => {
92 if !started {
93 for out in generator.generate(OutputChunk::Start { system_fingerprint: None, usage: prompt_usage }).await {
94 yield Ok(out);
95 }
96 started = true;
97 }
98 backend_finish = Some(finish_reason);
99 break;
100 }
101 InferenceChunk::Text {
102 content,
103 content_tokens,
104 } => (content, content_tokens),
105 InferenceChunk::Token { token_id } => {
106 let Some(decoder) = decoder.as_mut() else {
107 yield Err(StreamError::MissingTokenizer);
108 return;
109 };
110 match decoder.decode(token_id) {
111 Ok(Some(decoded)) => decoded,
112 Ok(None) => continue,
113 Err(error) => {
114 yield Err(error);
115 return;
116 }
117 }
118 }
119 };
120 if !started {
121 for out in generator.generate(OutputChunk::Start { system_fingerprint: None, usage: prompt_usage }).await {
122 yield Ok(out);
123 }
124 started = true;
125 }
126 completion_usage.completion_tokens += content_tokens;
127 let actions = state_machine.feed(&content);
128 stashed.push(Some(content));
129 for out in stashed.apply_actions(actions, &mut generator).await {
130 yield Ok(out);
131 }
132 stop_sequence = stashed.take_stop_sequence();
133 if stop_sequence.is_some() {
134 break;
135 }
136 }
137
138 if !started {
139 for out in generator.generate(OutputChunk::Start { system_fingerprint: None, usage: prompt_usage }).await {
140 yield Ok(out);
141 }
142 }
143 let actions = state_machine.finish();
144 for out in stashed.apply_actions(actions, &mut generator).await {
145 yield Ok(out);
146 }
147 if let Some(tail) = stashed.take_stop_sequence() {
149 stop_sequence.get_or_insert_with(String::new).push_str(&tail);
150 }
151 let reason = if stop_sequence.is_some() {
152 FinishReason::StopSequence
153 } else {
154 match backend_finish {
155 Some(InferenceFinishReason::Stop) if stashed.has_tool_calls => {
156 FinishReason::ToolCalls
157 }
158 Some(InferenceFinishReason::Stop) => FinishReason::Stop,
159 Some(InferenceFinishReason::Length) => FinishReason::Length,
160 Some(InferenceFinishReason::ContentFilter) => FinishReason::ContentFilter,
161 None => FinishReason::EndOfStream,
162 }
163 };
164 let finish = OutputChunk::Finish {
165 reason,
166 stop_sequence,
167 usage: completion_usage,
168 };
169 for out in generator.generate(finish).await {
170 yield Ok(out);
171 }
172 }
173 }
174}
175
176struct StashedChunks {
177 chunks: VecDeque<Option<String>>,
178 last_stashed_action: Option<OutputActionSegment>,
179 stashed_tool_name: String,
180 last_pop_action: OutputAction,
181 stop_sequence: Option<String>,
182 has_tool_calls: bool,
183}
184
185impl StashedChunks {
186 fn new() -> Self {
187 Self {
188 chunks: VecDeque::new(),
189 last_stashed_action: None,
190 stashed_tool_name: String::new(),
191 last_pop_action: OutputAction::Skip,
192 stop_sequence: None,
193 has_tool_calls: false,
194 }
195 }
196
197 fn push(&mut self, content: Option<String>) {
198 self.chunks.push_back(content);
199 }
200
201 fn take_stop_sequence(&mut self) -> Option<String> {
202 self.stop_sequence.take()
203 }
204
205 fn pop(&mut self, action: OutputActionSegment) -> Option<OutputChunk> {
206 let mut front_chunk = self.chunks.pop_front()?;
207 let front_len = front_chunk
208 .as_ref()
209 .map(|content| content.len())
210 .unwrap_or(0);
211 if action.len < front_len {
212 let remaining = front_chunk
213 .as_mut()
214 .map(|content| content.split_off(action.len));
215 self.chunks.push_front(remaining);
216 }
217 let last_pop_action = self.last_pop_action;
218 self.last_pop_action = action.action;
219
220 match action.action {
221 OutputAction::Raw => non_empty(front_chunk).map(|content| OutputChunk::Raw { content }),
222 OutputAction::Reasoning => {
223 non_empty(front_chunk).map(|content| OutputChunk::Reasoning { content })
224 }
225 OutputAction::ToSpace => Some(OutputChunk::Raw {
226 content: " ".to_string(),
227 }),
228 OutputAction::Skip | OutputAction::SkipInvalid { .. } => None,
229 OutputAction::StopSequence => {
230 let content = front_chunk.unwrap_or_default();
231 self.stop_sequence
232 .get_or_insert_with(String::new)
233 .push_str(&content);
234 None
235 }
236 OutputAction::ToolCallBegin => {
237 if last_pop_action != OutputAction::ToolCallBegin {
238 Some(OutputChunk::ToolCallBegin)
239 } else {
240 None
241 }
242 }
243 OutputAction::ToolName => {
244 if let Some(content) = front_chunk {
245 self.stashed_tool_name.push_str(&content);
246 }
247 None
248 }
249 OutputAction::ToolNameEnd => {
250 if last_pop_action != OutputAction::ToolNameEnd {
251 self.has_tool_calls = true;
252 let tool_name = std::mem::take(&mut self.stashed_tool_name);
253 Some(OutputChunk::ToolCall {
254 tool_name,
255 arguments: String::new(),
256 })
257 } else {
258 None
259 }
260 }
261 OutputAction::LabelToolCallArguments { label, .. } => {
262 if last_pop_action != action.action {
263 Some(OutputChunk::ToolArgumentsDelta {
264 content: label.to_string(),
265 })
266 } else {
267 None
268 }
269 }
270 OutputAction::RawToolCallArguments { string } => {
271 let content = if string {
272 escape_json_string(front_chunk.unwrap_or_default())
273 } else {
274 front_chunk.unwrap_or_default()
275 };
276 Some(OutputChunk::ToolArgumentsDelta { content })
277 }
278 OutputAction::ToolCallArgumentsEnd { output } => {
279 if last_pop_action == action.action {
280 return None;
281 }
282 output.map(|output| OutputChunk::ToolArgumentsDelta {
283 content: output.to_string(),
284 })
285 }
286 OutputAction::Label(label) => {
287 if last_pop_action != action.action {
288 Some(OutputChunk::Raw {
289 content: label.to_string(),
290 })
291 } else {
292 None
293 }
294 }
295 }
296 }
297
298 fn apply_whole_chunks(
299 &mut self,
300 action: &mut OutputActionSegment,
301 outputs: &mut Vec<OutputChunk>,
302 ) {
303 loop {
304 let front_len = self
305 .chunks
306 .front()
307 .map(|content| content.as_ref().map(|content| content.len()).unwrap_or(0));
308 if front_len.is_some_and(|front_len| front_len <= action.len) {
309 let front_len = front_len.unwrap();
310 if let Some(chunk) = self.pop(OutputActionSegment::new(action.action, front_len)) {
311 outputs.push(chunk);
312 }
313 action.len -= front_len;
314 } else {
315 return;
316 }
317 }
318 }
319
320 async fn apply_actions<G>(
321 &mut self,
322 actions: Vec<OutputActionSegment>,
323 generator: &mut G,
324 ) -> Vec<G::Chunk>
325 where
326 G: ChunkGenerator,
327 {
328 let mut outputs = Vec::new();
329 let mut last_stashed_action = self.last_stashed_action.take();
330 for action in actions {
331 if let Some(last_stashed_action) = &mut last_stashed_action {
332 if action.action == last_stashed_action.action {
333 last_stashed_action.len += action.len;
334 } else {
335 self.apply_whole_chunks(last_stashed_action, &mut outputs);
336 if last_stashed_action.len > 0
337 && let Some(chunk) = self.pop(last_stashed_action.clone())
338 {
339 outputs.push(chunk);
340 }
341 *last_stashed_action = action;
342 }
343 } else {
344 last_stashed_action = Some(action);
345 }
346 }
347 if let Some(last_stashed_action) = &mut last_stashed_action {
348 self.apply_whole_chunks(last_stashed_action, &mut outputs);
349 }
350 self.last_stashed_action = last_stashed_action;
351
352 let mut chunks = Vec::new();
353 for output in outputs {
354 chunks.extend(generator.generate(output).await);
355 }
356 chunks
357 }
358}
359
360fn non_empty(content: Option<String>) -> Option<String> {
361 content.filter(|content| !content.is_empty())
362}
363
364fn escape_json_string(content: String) -> String {
365 match serde_json::to_string(&content) {
366 Ok(escaped) if escaped.len() > 2 => escaped[1..escaped.len() - 1].to_string(),
367 _ => String::new(),
368 }
369}