use std::collections::VecDeque;
use std::pin::pin;
use async_stream::stream;
use tokio_stream::{Stream, StreamExt};
use super::decoder::{StreamDecoder, TokenizerDecoder};
use super::inference::{
CompletionUsage, FinishReason, InferenceChunk, InferenceFinishReason, PromptUsage,
};
use super::state_machine::{OutputAction, OutputActionSegment, ParsingOptions, StateMachine};
use super::{ChunkGenerator, OutputChunk, StreamError};
pub struct StreamProcessor<G> {
generator: G,
options: ParsingOptions,
decoder: Option<StreamDecoder>,
}
impl<G> StreamProcessor<G>
where
G: ChunkGenerator,
{
pub fn new(generator: G, options: ParsingOptions) -> Self {
Self {
generator,
options,
decoder: None,
}
}
pub fn with_tokenizer(mut self, decoder: impl TokenizerDecoder + 'static) -> Self {
self.decoder = Some(StreamDecoder::new(Box::new(decoder)));
self
}
pub fn process(
self,
inference: impl Stream<Item = InferenceChunk> + Send,
) -> impl Stream<Item = Result<G::Chunk, StreamError>> + Send
where
G::Chunk: Send,
{
let mut generator = self.generator;
let options = self.options;
let mut decoder = self.decoder;
stream! {
let mut inference = pin!(inference);
let mut state_machine = StateMachine::new(options);
let mut stashed = StashedChunks::new();
let mut started = false;
let mut prompt_usage = PromptUsage::default();
let mut completion_usage = CompletionUsage::default();
let mut backend_finish = None;
let mut stop_sequence = None;
while let Some(chunk) = inference.next().await {
let (content, content_tokens) = match chunk {
InferenceChunk::Ready {
system_fingerprint,
prompt_usage: ready_usage,
} => {
prompt_usage = ready_usage;
if !started {
for out in generator.generate(OutputChunk::Start { system_fingerprint, usage: prompt_usage }).await {
yield Ok(out);
}
started = true;
}
continue;
}
InferenceChunk::Finish { finish_reason } => {
if !started {
for out in generator.generate(OutputChunk::Start { system_fingerprint: None, usage: prompt_usage }).await {
yield Ok(out);
}
started = true;
}
backend_finish = Some(finish_reason);
break;
}
InferenceChunk::Text {
content,
content_tokens,
} => (content, content_tokens),
InferenceChunk::Token { token_id } => {
let Some(decoder) = decoder.as_mut() else {
yield Err(StreamError::MissingTokenizer);
return;
};
match decoder.decode(token_id) {
Ok(Some(decoded)) => decoded,
Ok(None) => continue,
Err(error) => {
yield Err(error);
return;
}
}
}
};
if !started {
for out in generator.generate(OutputChunk::Start { system_fingerprint: None, usage: prompt_usage }).await {
yield Ok(out);
}
started = true;
}
completion_usage.completion_tokens += content_tokens;
let actions = state_machine.feed(&content);
stashed.push(Some(content));
for out in stashed.apply_actions(actions, &mut generator).await {
yield Ok(out);
}
stop_sequence = stashed.take_stop_sequence();
if stop_sequence.is_some() {
break;
}
}
if !started {
for out in generator.generate(OutputChunk::Start { system_fingerprint: None, usage: prompt_usage }).await {
yield Ok(out);
}
}
let actions = state_machine.finish();
for out in stashed.apply_actions(actions, &mut generator).await {
yield Ok(out);
}
if let Some(tail) = stashed.take_stop_sequence() {
stop_sequence.get_or_insert_with(String::new).push_str(&tail);
}
let reason = if stop_sequence.is_some() {
FinishReason::StopSequence
} else {
match backend_finish {
Some(InferenceFinishReason::Stop) if stashed.has_tool_calls => {
FinishReason::ToolCalls
}
Some(InferenceFinishReason::Stop) => FinishReason::Stop,
Some(InferenceFinishReason::Length) => FinishReason::Length,
Some(InferenceFinishReason::ContentFilter) => FinishReason::ContentFilter,
None => FinishReason::EndOfStream,
}
};
let finish = OutputChunk::Finish {
reason,
stop_sequence,
usage: completion_usage,
};
for out in generator.generate(finish).await {
yield Ok(out);
}
}
}
}
struct StashedChunks {
chunks: VecDeque<Option<String>>,
last_stashed_action: Option<OutputActionSegment>,
stashed_tool_name: String,
last_pop_action: OutputAction,
stop_sequence: Option<String>,
has_tool_calls: bool,
}
impl StashedChunks {
fn new() -> Self {
Self {
chunks: VecDeque::new(),
last_stashed_action: None,
stashed_tool_name: String::new(),
last_pop_action: OutputAction::Skip,
stop_sequence: None,
has_tool_calls: false,
}
}
fn push(&mut self, content: Option<String>) {
self.chunks.push_back(content);
}
fn take_stop_sequence(&mut self) -> Option<String> {
self.stop_sequence.take()
}
fn pop(&mut self, action: OutputActionSegment) -> Option<OutputChunk> {
let mut front_chunk = self.chunks.pop_front()?;
let front_len = front_chunk
.as_ref()
.map(|content| content.len())
.unwrap_or(0);
if action.len < front_len {
let remaining = front_chunk
.as_mut()
.map(|content| content.split_off(action.len));
self.chunks.push_front(remaining);
}
let last_pop_action = self.last_pop_action;
self.last_pop_action = action.action;
match action.action {
OutputAction::Raw => non_empty(front_chunk).map(|content| OutputChunk::Raw { content }),
OutputAction::Reasoning => {
non_empty(front_chunk).map(|content| OutputChunk::Reasoning { content })
}
OutputAction::ToSpace => Some(OutputChunk::Raw {
content: " ".to_string(),
}),
OutputAction::Skip | OutputAction::SkipInvalid { .. } => None,
OutputAction::StopSequence => {
let content = front_chunk.unwrap_or_default();
self.stop_sequence
.get_or_insert_with(String::new)
.push_str(&content);
None
}
OutputAction::ToolCallBegin => {
if last_pop_action != OutputAction::ToolCallBegin {
Some(OutputChunk::ToolCallBegin)
} else {
None
}
}
OutputAction::ToolName => {
if let Some(content) = front_chunk {
self.stashed_tool_name.push_str(&content);
}
None
}
OutputAction::ToolNameEnd => {
if last_pop_action != OutputAction::ToolNameEnd {
self.has_tool_calls = true;
let tool_name = std::mem::take(&mut self.stashed_tool_name);
Some(OutputChunk::ToolCall {
tool_name,
arguments: String::new(),
})
} else {
None
}
}
OutputAction::LabelToolCallArguments { label, .. } => {
if last_pop_action != action.action {
Some(OutputChunk::ToolArgumentsDelta {
content: label.to_string(),
})
} else {
None
}
}
OutputAction::RawToolCallArguments { string } => {
let content = if string {
escape_json_string(front_chunk.unwrap_or_default())
} else {
front_chunk.unwrap_or_default()
};
Some(OutputChunk::ToolArgumentsDelta { content })
}
OutputAction::ToolCallArgumentsEnd { output } => {
if last_pop_action == action.action {
return None;
}
output.map(|output| OutputChunk::ToolArgumentsDelta {
content: output.to_string(),
})
}
OutputAction::Label(label) => {
if last_pop_action != action.action {
Some(OutputChunk::Raw {
content: label.to_string(),
})
} else {
None
}
}
}
}
fn apply_whole_chunks(
&mut self,
action: &mut OutputActionSegment,
outputs: &mut Vec<OutputChunk>,
) {
loop {
let front_len = self
.chunks
.front()
.map(|content| content.as_ref().map(|content| content.len()).unwrap_or(0));
if front_len.is_some_and(|front_len| front_len <= action.len) {
let front_len = front_len.unwrap();
if let Some(chunk) = self.pop(OutputActionSegment::new(action.action, front_len)) {
outputs.push(chunk);
}
action.len -= front_len;
} else {
return;
}
}
}
async fn apply_actions<G>(
&mut self,
actions: Vec<OutputActionSegment>,
generator: &mut G,
) -> Vec<G::Chunk>
where
G: ChunkGenerator,
{
let mut outputs = Vec::new();
let mut last_stashed_action = self.last_stashed_action.take();
for action in actions {
if let Some(last_stashed_action) = &mut last_stashed_action {
if action.action == last_stashed_action.action {
last_stashed_action.len += action.len;
} else {
self.apply_whole_chunks(last_stashed_action, &mut outputs);
if last_stashed_action.len > 0
&& let Some(chunk) = self.pop(last_stashed_action.clone())
{
outputs.push(chunk);
}
*last_stashed_action = action;
}
} else {
last_stashed_action = Some(action);
}
}
if let Some(last_stashed_action) = &mut last_stashed_action {
self.apply_whole_chunks(last_stashed_action, &mut outputs);
}
self.last_stashed_action = last_stashed_action;
let mut chunks = Vec::new();
for output in outputs {
chunks.extend(generator.generate(output).await);
}
chunks
}
}
fn non_empty(content: Option<String>) -> Option<String> {
content.filter(|content| !content.is_empty())
}
fn escape_json_string(content: String) -> String {
match serde_json::to_string(&content) {
Ok(escaped) if escaped.len() > 2 => escaped[1..escaped.len() - 1].to_string(),
_ => String::new(),
}
}