1use axum::response::sse::{Event, Sse};
4use futures::stream::Stream;
5use serde::Serialize;
6use std::convert::Infallible;
7use std::pin::Pin;
8use std::task::{Context, Poll};
9use tokio::sync::mpsc;
10
11pub struct ChatStream {
13 rx: mpsc::Receiver<StreamChunk>,
14 state: StreamState,
15}
16
17#[derive(PartialEq)]
20enum StreamState {
21 Open,
22 Finishing,
23 Done,
24}
25
26#[derive(Debug, Clone, Serialize)]
28pub struct StreamChunk {
29 pub id: String,
30 pub object: String,
31 pub created: u64,
32 pub model: String,
33 pub choices: Vec<StreamChoice>,
34 #[serde(skip_serializing_if = "Option::is_none")]
37 pub usage: Option<StreamUsage>,
38}
39
40#[derive(Debug, Clone, Serialize)]
41pub struct StreamUsage {
42 pub prompt_tokens: u32,
43 pub completion_tokens: u32,
44 pub total_tokens: u32,
45}
46
47#[derive(Debug, Clone, Serialize)]
48pub struct StreamChoice {
49 pub index: u32,
50 pub delta: StreamDelta,
51 pub finish_reason: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub struct StreamDelta {
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub role: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub content: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
63 pub tool_calls: Option<serde_json::Value>,
64}
65
66impl ChatStream {
67 pub fn new(buffer: usize) -> (mpsc::Sender<StreamChunk>, Self) {
69 let (tx, rx) = mpsc::channel(buffer);
70 (
71 tx,
72 Self {
73 rx,
74 state: StreamState::Open,
75 },
76 )
77 }
78
79 pub fn into_sse(self) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
81 Sse::new(SseStream { inner: self })
82 }
83}
84
85struct SseStream {
86 inner: ChatStream,
87}
88
89impl Stream for SseStream {
90 type Item = Result<Event, Infallible>;
91
92 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
93 match self.inner.state {
94 StreamState::Done => return Poll::Ready(None),
95 StreamState::Finishing => {
96 self.inner.state = StreamState::Done;
97 return Poll::Ready(Some(Ok(Event::default().data("[DONE]"))));
98 }
99 StreamState::Open => {}
100 }
101
102 match self.inner.rx.poll_recv(cx) {
103 Poll::Ready(Some(chunk)) => {
104 let is_finish = chunk
105 .choices
106 .first()
107 .and_then(|c| c.finish_reason.as_deref())
108 .is_some();
109 if is_finish {
110 self.inner.state = StreamState::Finishing;
111 }
112 let json = serde_json::to_string(&chunk).unwrap_or_default();
113 Poll::Ready(Some(Ok(Event::default().data(json))))
114 }
115 Poll::Ready(None) => {
116 self.inner.state = StreamState::Done;
119 Poll::Ready(Some(Ok(Event::default().data("[DONE]"))))
120 }
121 Poll::Pending => Poll::Pending,
122 }
123 }
124}
125
126pub fn tool_calls_chunk(
131 id: &str,
132 model: &str,
133 calls: serde_json::Value,
134 created: u64,
135) -> StreamChunk {
136 StreamChunk {
137 id: id.to_string(),
138 object: "chat.completion.chunk".to_string(),
139 created,
140 model: model.to_string(),
141 choices: vec![StreamChoice {
142 index: 0,
143 delta: StreamDelta {
144 role: None,
145 content: None,
146 tool_calls: Some(calls),
147 },
148 finish_reason: None,
149 }],
150 usage: None,
151 }
152}
153
154pub fn token_chunk(id: &str, model: &str, token: &str, created: u64) -> StreamChunk {
155 StreamChunk {
156 id: id.to_string(),
157 object: "chat.completion.chunk".to_string(),
158 created,
159 model: model.to_string(),
160 choices: vec![StreamChoice {
161 index: 0,
162 delta: StreamDelta {
163 role: None,
164 content: Some(token.to_string()),
165 tool_calls: None,
166 },
167 finish_reason: None,
168 }],
169 usage: None,
170 }
171}
172
173pub fn usage_chunk(
176 id: &str,
177 model: &str,
178 created: u64,
179 prompt_tokens: u32,
180 completion_tokens: u32,
181) -> StreamChunk {
182 StreamChunk {
183 id: id.to_string(),
184 object: "chat.completion.chunk".to_string(),
185 created,
186 model: model.to_string(),
187 choices: Vec::new(),
188 usage: Some(StreamUsage {
189 prompt_tokens,
190 completion_tokens,
191 total_tokens: prompt_tokens + completion_tokens,
192 }),
193 }
194}
195
196pub fn finish_chunk(id: &str, model: &str, reason: &str, created: u64) -> StreamChunk {
198 StreamChunk {
199 id: id.to_string(),
200 object: "chat.completion.chunk".to_string(),
201 created,
202 model: model.to_string(),
203 choices: vec![StreamChoice {
204 index: 0,
205 delta: StreamDelta {
206 role: None,
207 content: None,
208 tool_calls: None,
209 },
210 finish_reason: Some(reason.to_string()),
211 }],
212 usage: None,
213 }
214}