Skip to main content

deepseek_recipe/stream/
processor.rs

1use 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
14/// Parse backend inference chunks and produce protocol response events.
15pub 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    /// Combine a protocol event generator with output parsing options.
26    pub fn new(generator: G, options: ParsingOptions) -> Self {
27        Self {
28            generator,
29            options,
30            decoder: None,
31        }
32    }
33
34    /// Decode `InferenceChunk::Token` ids with the supplied decoder.
35    ///
36    /// Ids that contribute text count as completion tokens, including the ids
37    /// buffered while a multi-token character was incomplete. IDs still buffered
38    /// at the end of input contribute neither text nor completion usage.
39    /// Without a decoder, a token chunk fails the stream with
40    /// [`StreamError::MissingTokenizer`].
41    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    /// Consume inference chunks until a finish chunk, matched stop sequence, or EOF.
47    ///
48    /// A ready chunk received before output starts supplies the initial prompt
49    /// usage and fingerprint. Without it, the start event uses zero prompt usage
50    /// and no fingerprint. Later ready chunks do not update emitted metadata.
51    /// Completion usage accumulates each processed chunk's token count, including
52    /// the entire chunk containing a stop sequence; later chunks are not read.
53    /// Missing tokenizers and decoder failures yield an error and end the stream
54    /// without a normal finish event.
55    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            // Combine the parts of a stop sequence matched across source chunks.
148            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}