use bytes::Bytes;
use futures_core::Stream;
use futures_util::StreamExt;
use lellm_core::LlmError;
use super::{
EventSink, SseFrame, SseParser, StreamEvent, ToolCallAccumulator, ToolCallDelta,
UsageAccumulator, UsageDelta,
};
use crate::providers::codec::{ChatCodec, StreamChunk};
struct FrameResult {
text: Option<String>,
thinking: Option<String>,
thinking_redacted: Option<String>,
tool_call_delta: Option<ToolCallDelta>,
usage_delta: Option<UsageDelta>,
is_done: bool,
}
pub async fn process_stream<S, A, E>(sink: &mut E, codec: &A, model: String, mut bytes_stream: S)
where
S: Stream<Item = Result<Bytes, LlmError>> + Unpin,
A: ChatCodec,
E: EventSink,
{
if !sink.emit(StreamEvent::Start { model }).await {
return;
}
let mut parser = SseParser::new();
let mut tool_call_acc = ToolCallAccumulator::new();
let mut usage_acc = UsageAccumulator::new();
let mut is_done = false;
let stream_start = std::time::Instant::now();
while let Some(result) = bytes_stream.next().await {
if sink.is_closed() {
return;
}
match result {
Ok(bytes) => {
let frames = parser.feed(&bytes);
for frame in frames {
let fr = handle_frame(codec, &frame);
if let Some(text) = fr.text
&& !sink.emit(StreamEvent::Token { token: text }).await
{
return;
}
if let Some(thinking) = fr.thinking
&& !sink
.emit(StreamEvent::ThinkingDelta {
thinking,
redacted: fr.thinking_redacted,
})
.await
{
return;
}
if let Some(delta) = fr.tool_call_delta {
tool_call_acc.push(&delta);
}
if let Some(delta) = fr.usage_delta {
usage_acc.push(&delta);
}
if fr.is_done {
is_done = true;
}
}
}
Err(e) => {
tracing::error!(
elapsed = ?stream_start.elapsed(),
error = %e,
"stream error"
);
sink.emit(StreamEvent::Error(e)).await;
return;
}
}
if is_done {
break;
}
}
if sink.is_closed() {
return;
}
let tool_calls = tool_call_acc.finalize().unwrap_or_default();
let final_usage = usage_acc.finalize();
sink.emit(StreamEvent::ResponseComplete {
tool_calls,
usage: final_usage,
})
.await;
}
fn handle_frame<A: ChatCodec>(codec: &A, frame: &SseFrame) -> FrameResult {
let mut result = FrameResult {
text: None,
thinking: None,
thinking_redacted: None,
tool_call_delta: None,
usage_delta: None,
is_done: false,
};
match codec.decode_sse(frame) {
Ok(parse_result) => {
for chunk in parse_result.chunks {
match chunk {
StreamChunk::TextDelta(text) => {
result.text = Some(text);
}
StreamChunk::ThinkingDelta { thinking, redacted } => {
result.thinking = Some(thinking);
result.thinking_redacted = redacted;
}
StreamChunk::ToolCallDelta(delta) => {
result.tool_call_delta = Some(ToolCallDelta {
index: delta.index,
id: delta.id.clone(),
name: delta.name.clone(),
arguments_delta: delta.arguments_delta.clone(),
});
}
StreamChunk::Usage(u) => {
result.usage_delta = Some(UsageDelta::Full(u));
}
StreamChunk::InputTokens(it) => {
result.usage_delta = Some(UsageDelta::InputTokens(it));
}
StreamChunk::OutputTokens(ot) => {
result.usage_delta = Some(UsageDelta::OutputTokens(ot));
}
StreamChunk::Done => {
result.is_done = true;
}
}
}
}
Err(e) => {
if frame.data != "[DONE]" && !frame.data.is_empty() {
tracing::warn!(
error = %e,
data_len = frame.data.len(),
"failed to parse provider SSE frame"
);
}
}
}
result
}