use axum::response::sse::{Event, Sse};
use futures::stream::Stream;
use serde::Serialize;
use std::convert::Infallible;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::sync::mpsc;
pub struct ChatStream {
rx: mpsc::Receiver<StreamChunk>,
state: StreamState,
}
#[derive(PartialEq)]
enum StreamState {
Open,
Finishing,
Done,
}
#[derive(Debug, Clone, Serialize)]
pub struct StreamChunk {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<StreamChoice>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StreamChoice {
pub index: u32,
pub delta: StreamDelta,
pub finish_reason: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StreamDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
}
impl ChatStream {
pub fn new(buffer: usize) -> (mpsc::Sender<StreamChunk>, Self) {
let (tx, rx) = mpsc::channel(buffer);
(
tx,
Self {
rx,
state: StreamState::Open,
},
)
}
pub fn into_sse(self) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
Sse::new(SseStream { inner: self })
}
}
struct SseStream {
inner: ChatStream,
}
impl Stream for SseStream {
type Item = Result<Event, Infallible>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.inner.state {
StreamState::Done => return Poll::Ready(None),
StreamState::Finishing => {
self.inner.state = StreamState::Done;
return Poll::Ready(Some(Ok(Event::default().data("[DONE]"))));
}
StreamState::Open => {}
}
match self.inner.rx.poll_recv(cx) {
Poll::Ready(Some(chunk)) => {
let is_finish = chunk
.choices
.first()
.and_then(|c| c.finish_reason.as_deref())
.is_some();
if is_finish {
self.inner.state = StreamState::Finishing;
}
let json = serde_json::to_string(&chunk).unwrap_or_default();
Poll::Ready(Some(Ok(Event::default().data(json))))
}
Poll::Ready(None) => {
self.inner.state = StreamState::Done;
Poll::Ready(Some(Ok(Event::default().data("[DONE]"))))
}
Poll::Pending => Poll::Pending,
}
}
}
pub fn token_chunk(id: &str, model: &str, token: &str, created: u64) -> StreamChunk {
StreamChunk {
id: id.to_string(),
object: "chat.completion.chunk".to_string(),
created,
model: model.to_string(),
choices: vec![StreamChoice {
index: 0,
delta: StreamDelta {
role: None,
content: Some(token.to_string()),
},
finish_reason: None,
}],
}
}
pub fn finish_chunk(id: &str, model: &str, reason: &str, created: u64) -> StreamChunk {
StreamChunk {
id: id.to_string(),
object: "chat.completion.chunk".to_string(),
created,
model: model.to_string(),
choices: vec![StreamChoice {
index: 0,
delta: StreamDelta {
role: None,
content: None,
},
finish_reason: Some(reason.to_string()),
}],
}
}