Skip to main content

openai_compat/
streaming.rs

1//! Server-sent events decoding and the async [`EventStream`] adapter,
2//! mirroring `_streaming.py` (`SSEDecoder` and `Stream.__stream__`).
3
4use std::collections::VecDeque;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8use futures_core::Stream;
9use futures_util::StreamExt;
10use serde::de::DeserializeOwned;
11
12use crate::error::OpenAIError;
13
14/// A single decoded server-sent event.
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct ServerSentEvent {
17    pub event: Option<String>,
18    pub data: String,
19    pub id: Option<String>,
20    pub retry: Option<u64>,
21}
22
23/// Incremental SSE parser. Push bytes in with [`SseDecoder::feed`]; complete
24/// events are returned as they are terminated by a blank line.
25///
26/// Mirrors `_streaming.py::SSEDecoder` field handling: `event`, `data`
27/// (multi-line joined with `\n`), `id` (ignored if it contains NUL), `retry`,
28/// comment lines (leading `:`), and a single leading space stripped from
29/// values.
30#[derive(Debug, Default)]
31pub struct SseDecoder {
32    buf: Vec<u8>,
33    event: Option<String>,
34    data: Vec<String>,
35    id: Option<String>,
36    retry: Option<u64>,
37    bom_checked: bool,
38}
39
40/// Upper bound on bytes buffered while waiting for a line terminator —
41/// guards against a broken or malicious server streaming an endless line.
42const MAX_SSE_BUFFER: usize = 16 * 1024 * 1024;
43
44impl SseDecoder {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Bytes currently buffered awaiting a line terminator.
50    pub fn buffered_len(&self) -> usize {
51        self.buf.len()
52    }
53
54    /// Feed a chunk of bytes; returns all events completed by this chunk.
55    pub fn feed(&mut self, chunk: &[u8]) -> Vec<ServerSentEvent> {
56        self.buf.extend_from_slice(chunk);
57
58        // Per the SSE spec, strip one leading U+FEFF byte-order mark.
59        if !self.bom_checked && self.buf.len() >= 3 {
60            if self.buf.starts_with(&[0xEF, 0xBB, 0xBF]) {
61                self.buf.drain(..3);
62            }
63            self.bom_checked = true;
64        }
65
66        let mut events = Vec::new();
67
68        // Consume complete lines; terminators are \r\n, \n, or lone \r.
69        while let Some(pos) = self.buf.iter().position(|&b| b == b'\n' || b == b'\r') {
70            let term_len = if self.buf[pos] == b'\r' {
71                if pos + 1 == self.buf.len() {
72                    // Might be the first half of a \r\n split across chunks.
73                    break;
74                }
75                if self.buf[pos + 1] == b'\n' {
76                    2
77                } else {
78                    1
79                }
80            } else {
81                1
82            };
83
84            let line = String::from_utf8_lossy(&self.buf[..pos]).into_owned();
85            self.buf.drain(..pos + term_len);
86
87            if let Some(event) = self.process_line(&line) {
88                events.push(event);
89            }
90        }
91        events
92    }
93
94    /// Flush any trailing buffered line at end of stream.
95    pub fn flush(&mut self) -> Vec<ServerSentEvent> {
96        let mut events = Vec::new();
97        // A held-back lone \r (possible split \r\n) is a terminator, not data.
98        if self.buf.last() == Some(&b'\r') {
99            self.buf.pop();
100        }
101        if !self.buf.is_empty() {
102            let line = String::from_utf8_lossy(&self.buf).into_owned();
103            self.buf.clear();
104            if let Some(event) = self.process_line(&line) {
105                events.push(event);
106            }
107        }
108        // Dispatch a final unterminated event if any data was accumulated.
109        if !self.data.is_empty() || self.event.is_some() {
110            events.push(self.dispatch());
111        }
112        events
113    }
114
115    fn process_line(&mut self, line: &str) -> Option<ServerSentEvent> {
116        if line.is_empty() {
117            // Blank line: dispatch the accumulated event.
118            if self.event.is_none() && self.data.is_empty() && self.retry.is_none() {
119                return None;
120            }
121            return Some(self.dispatch());
122        }
123        if line.starts_with(':') {
124            return None; // comment
125        }
126
127        let (field, value) = match line.split_once(':') {
128            Some((field, value)) => (field, value.strip_prefix(' ').unwrap_or(value)),
129            None => (line, ""),
130        };
131
132        match field {
133            "event" => self.event = Some(value.to_string()),
134            "data" => self.data.push(value.to_string()),
135            "id" => {
136                if !value.contains('\0') {
137                    self.id = Some(value.to_string());
138                }
139            }
140            "retry" => {
141                if let Ok(retry) = value.parse() {
142                    self.retry = Some(retry);
143                }
144            }
145            _ => {} // unknown field: ignored per spec
146        }
147        None
148    }
149
150    fn dispatch(&mut self) -> ServerSentEvent {
151        ServerSentEvent {
152            event: self.event.take(),
153            data: std::mem::take(&mut self.data).join("\n"),
154            id: self.id.clone(),
155            retry: self.retry.take(),
156        }
157    }
158}
159
160/// An async stream of deserialized SSE items from a streaming API response.
161///
162/// Terminates on the `data: [DONE]` sentinel. A mid-stream `{"error": ...}`
163/// payload is surfaced as [`OpenAIError::Api`]-style [`OpenAIError::Stream`]
164/// and ends the stream, mirroring `_streaming.py::Stream.__stream__`.
165pub struct EventStream<T> {
166    bytes: Pin<Box<dyn Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Send>>,
167    decoder: SseDecoder,
168    pending: VecDeque<ServerSentEvent>,
169    done: bool,
170    bytes_exhausted: bool,
171    _marker: std::marker::PhantomData<fn() -> T>,
172}
173
174impl<T: DeserializeOwned> EventStream<T> {
175    pub(crate) fn new(response: reqwest::Response) -> Self {
176        Self {
177            bytes: Box::pin(response.bytes_stream()),
178            decoder: SseDecoder::new(),
179            pending: VecDeque::new(),
180            done: false,
181            bytes_exhausted: false,
182            _marker: std::marker::PhantomData,
183        }
184    }
185
186    /// Convert one SSE event into a stream item.
187    /// `None` means the `[DONE]` sentinel was seen.
188    fn item_from_event(event: ServerSentEvent) -> Option<Result<T, OpenAIError>> {
189        if event.data.starts_with("[DONE]") {
190            return None;
191        }
192        let value: serde_json::Value = match serde_json::from_str(&event.data) {
193            Ok(value) => value,
194            Err(err) => return Some(Err(OpenAIError::Stream(format!(
195                "invalid JSON in stream: {err}"
196            )))),
197        };
198        if let Some(error) = value.get("error").filter(|e| !e.is_null()) {
199            let message = error
200                .get("message")
201                .and_then(|m| m.as_str())
202                .filter(|m| !m.is_empty())
203                .unwrap_or("An error occurred during streaming");
204            return Some(Err(OpenAIError::Stream(message.to_string())));
205        }
206        match serde_json::from_value(value) {
207            Ok(item) => Some(Ok(item)),
208            Err(err) => Some(Err(OpenAIError::Json(err))),
209        }
210    }
211}
212
213impl<T: DeserializeOwned> Stream for EventStream<T> {
214    type Item = Result<T, OpenAIError>;
215
216    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
217        let this = self.get_mut();
218        loop {
219            if this.done {
220                return Poll::Ready(None);
221            }
222
223            if let Some(event) = this.pending.pop_front() {
224                // Skip events with no data (e.g. keep-alive comments produce none).
225                if event.data.is_empty() {
226                    continue;
227                }
228                match Self::item_from_event(event) {
229                    Some(Ok(item)) => return Poll::Ready(Some(Ok(item))),
230                    Some(Err(err)) => {
231                        this.done = true;
232                        return Poll::Ready(Some(Err(err)));
233                    }
234                    None => {
235                        this.done = true;
236                        return Poll::Ready(None);
237                    }
238                }
239            }
240
241            // Never poll the byte stream again after it has completed.
242            if this.bytes_exhausted {
243                this.done = true;
244                return Poll::Ready(None);
245            }
246
247            match this.bytes.as_mut().poll_next(cx) {
248                Poll::Pending => return Poll::Pending,
249                Poll::Ready(Some(Ok(chunk))) => {
250                    this.pending.extend(this.decoder.feed(&chunk));
251                    if this.decoder.buffered_len() > MAX_SSE_BUFFER {
252                        this.done = true;
253                        return Poll::Ready(Some(Err(OpenAIError::Stream(format!(
254                            "SSE line exceeded the {MAX_SSE_BUFFER} byte buffer limit"
255                        )))));
256                    }
257                }
258                Poll::Ready(Some(Err(err))) => {
259                    this.done = true;
260                    return Poll::Ready(Some(Err(if err.is_timeout() {
261                        OpenAIError::Timeout
262                    } else {
263                        OpenAIError::Connection(err.to_string())
264                    })));
265                }
266                Poll::Ready(None) => {
267                    this.bytes_exhausted = true;
268                    this.pending.extend(this.decoder.flush());
269                }
270            }
271        }
272    }
273}
274
275impl<T: DeserializeOwned + Send + 'static> EventStream<T> {
276    /// Collect all remaining items into a `Vec`, failing on the first error.
277    pub async fn collect_all(mut self) -> Result<Vec<T>, OpenAIError> {
278        let mut items = Vec::new();
279        while let Some(item) = self.next().await {
280            items.push(item?);
281        }
282        Ok(items)
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn decodes_simple_event() {
292        let mut decoder = SseDecoder::new();
293        let events = decoder.feed(b"data: {\"x\":1}\n\n");
294        assert_eq!(events.len(), 1);
295        assert_eq!(events[0].data, "{\"x\":1}");
296        assert_eq!(events[0].event, None);
297    }
298
299    #[test]
300    fn decodes_event_split_across_chunks() {
301        let mut decoder = SseDecoder::new();
302        assert!(decoder.feed(b"data: {\"x\"").is_empty());
303        assert!(decoder.feed(b":1}\n").is_empty());
304        let events = decoder.feed(b"\n");
305        assert_eq!(events.len(), 1);
306        assert_eq!(events[0].data, "{\"x\":1}");
307    }
308
309    #[test]
310    fn handles_crlf_and_cr_terminators() {
311        let mut decoder = SseDecoder::new();
312        let events = decoder.feed(b"data: a\r\n\r\ndata: b\r\rdata: c\n\n");
313        assert_eq!(
314            events.iter().map(|e| e.data.as_str()).collect::<Vec<_>>(),
315            vec!["a", "b", "c"]
316        );
317    }
318
319    #[test]
320    fn crlf_split_across_chunks() {
321        let mut decoder = SseDecoder::new();
322        assert!(decoder.feed(b"data: a\r").is_empty());
323        let events = decoder.feed(b"\n\r\n");
324        assert_eq!(events.len(), 1);
325        assert_eq!(events[0].data, "a");
326    }
327
328    #[test]
329    fn multiline_data_joined_with_newline() {
330        let mut decoder = SseDecoder::new();
331        let events = decoder.feed(b"data: line1\ndata: line2\n\n");
332        assert_eq!(events[0].data, "line1\nline2");
333    }
334
335    #[test]
336    fn parses_event_id_retry_and_comments() {
337        let mut decoder = SseDecoder::new();
338        let events =
339            decoder.feed(b": keep-alive\nevent: message\nid: 42\nretry: 100\ndata: hi\n\n");
340        assert_eq!(events.len(), 1);
341        assert_eq!(events[0].event.as_deref(), Some("message"));
342        assert_eq!(events[0].id.as_deref(), Some("42"));
343        assert_eq!(events[0].retry, Some(100));
344        assert_eq!(events[0].data, "hi");
345    }
346
347    #[test]
348    fn strips_single_leading_space_only() {
349        let mut decoder = SseDecoder::new();
350        let events = decoder.feed(b"data:  padded\ndata:none\n\n");
351        assert_eq!(events[0].data, " padded\nnone");
352    }
353
354    #[test]
355    fn strips_leading_bom() {
356        let mut decoder = SseDecoder::new();
357        let events = decoder.feed(b"\xEF\xBB\xBFdata: first\n\n");
358        assert_eq!(events.len(), 1);
359        assert_eq!(events[0].data, "first");
360        // Only the first BOM is stripped; later ones are data.
361        let events = decoder.feed(b"data: \xEF\xBB\xBFx\n\n");
362        assert_eq!(events[0].data, "\u{FEFF}x");
363    }
364
365    #[test]
366    fn flush_emits_trailing_event() {
367        let mut decoder = SseDecoder::new();
368        assert!(decoder.feed(b"data: tail").is_empty());
369        let events = decoder.flush();
370        assert_eq!(events.len(), 1);
371        assert_eq!(events[0].data, "tail");
372    }
373}