Skip to main content

openrouter/
stream.rs

1//! SSE parser + generic [`EventStream<T>`].
2//!
3//! The parser is hand-rolled — no extra dependency beyond what `reqwest`'s
4//! `stream` feature already provides (`Stream<Item = Result<Bytes>>`). It
5//! handles `data:` accumulation, the `data: [DONE]` terminator, comment
6//! lines (`:` prefix), `\r\n` and bare `\r` line endings, and events split
7//! across chunk boundaries.
8//!
9//! Reconnection: when the underlying body stream errors on a transient
10//! failure, the stream re-opens via the caller-supplied closure with
11//! exponential backoff capped at [`MAX_RECONNECT_BACKOFF`]. The reconnect
12//! counter resets after the first successful chunk arrives post-reconnect.
13//! Non-transient errors and exhausted budget surface as `Err` and terminate
14//! the stream.
15//!
16//! Cancellation: dropping the `EventStream` drops the underlying
17//! `reqwest::Response::bytes_stream`, which closes the connection. No
18//! explicit `CancellationToken` is required — combine with `tokio::select!`
19//! on the consumer side for timeout/cancellation patterns.
20
21use std::future::Future;
22use std::marker::PhantomData;
23use std::pin::Pin;
24use std::sync::Arc;
25use std::task::{Context, Poll};
26use std::time::Duration;
27
28use bytes::Bytes;
29use futures::future::BoxFuture;
30use futures::stream::BoxStream;
31use futures::{Stream, StreamExt};
32use reqwest::Response;
33use serde::de::DeserializeOwned;
34
35use crate::error::{Error, Result};
36use crate::retry::MAX_RECONNECT_BACKOFF;
37
38/// Async factory that re-opens the underlying HTTP response after a
39/// transient failure. Returned by callers in `crate::client` so the stream
40/// can resume the same request body on reconnect.
41pub(crate) type Reopen =
42    Arc<dyn Fn() -> BoxFuture<'static, Result<Response>> + Send + Sync + 'static>;
43
44type ByteStream = BoxStream<'static, std::result::Result<Bytes, reqwest::Error>>;
45type SleepFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
46type ReopenFuture = BoxFuture<'static, Result<Response>>;
47
48/// A stream of deserialized SSE events.
49///
50/// Implements [`futures::Stream`] with `Item = Result<T>`. Yields `None` on
51/// the `data: [DONE]` terminator or when the underlying body finishes.
52pub struct EventStream<T: DeserializeOwned> {
53    state: State,
54    buf: SseBuffer,
55    reopen: Option<Reopen>,
56    reconnect_attempt: u32,
57    _marker: PhantomData<fn() -> T>,
58}
59
60impl<T: DeserializeOwned> std::fmt::Debug for EventStream<T> {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("EventStream")
63            .field("reconnect_attempt", &self.reconnect_attempt)
64            .field("buffered", &self.buf.pending_len())
65            .field("state", &self.state.tag())
66            .finish()
67    }
68}
69
70enum State {
71    /// Active body stream — poll it for the next chunk.
72    Reading(ByteStream),
73    /// Sleeping before the next reconnect attempt.
74    Backoff(SleepFuture),
75    /// Re-opening the body via the caller's `reopen` closure.
76    Reopening(ReopenFuture),
77    /// Stream terminated (success or fatal error).
78    Done,
79}
80
81impl State {
82    fn tag(&self) -> &'static str {
83        match self {
84            State::Reading(_) => "reading",
85            State::Backoff(_) => "backoff",
86            State::Reopening(_) => "reopening",
87            State::Done => "done",
88        }
89    }
90}
91
92impl<T: DeserializeOwned> EventStream<T> {
93    /// Build a new event stream from an already-opened `Response` and a
94    /// reconnect closure. The closure is invoked on transient mid-stream
95    /// failures with exponential backoff.
96    #[allow(dead_code)] // Consumed by the streaming endpoints (HRA-123).
97    pub(crate) fn new(initial: Response, reopen: Reopen) -> Self {
98        Self {
99            state: State::Reading(initial.bytes_stream().boxed()),
100            buf: SseBuffer::default(),
101            reopen: Some(reopen),
102            reconnect_attempt: 0,
103            _marker: PhantomData,
104        }
105    }
106
107    /// Build a non-reconnecting stream (used by tests and any caller that
108    /// doesn't want resume semantics).
109    #[cfg(test)]
110    pub(crate) fn from_bytes_stream(bytes: ByteStream) -> Self {
111        Self {
112            state: State::Reading(bytes),
113            buf: SseBuffer::default(),
114            reopen: None,
115            reconnect_attempt: 0,
116            _marker: PhantomData,
117        }
118    }
119
120    fn reconnect_delay(&self) -> Duration {
121        // Exponential: 100ms, 200ms, 400ms, …, capped at MAX_RECONNECT_BACKOFF.
122        let base_ms = 100u64.saturating_mul(1u64 << self.reconnect_attempt.min(8));
123        let computed = Duration::from_millis(base_ms);
124        computed.min(MAX_RECONNECT_BACKOFF)
125    }
126}
127
128impl<T: DeserializeOwned> Stream for EventStream<T> {
129    type Item = Result<T>;
130
131    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
132        loop {
133            // First drain any complete events already buffered.
134            match self.buf.next_event() {
135                Some(SseEvent::Data(payload)) => {
136                    let decoded: Result<T> = serde_json::from_slice(&payload)
137                        .map_err(|e| Error::Stream(format!("malformed SSE payload: {e}")));
138                    return Poll::Ready(Some(decoded));
139                }
140                Some(SseEvent::Done) => {
141                    self.state = State::Done;
142                    return Poll::Ready(None);
143                }
144                None => {}
145            }
146
147            // Drive the state machine to produce more bytes/events.
148            // Take the current state out so we can replace it after polling.
149            let cur = std::mem::replace(&mut self.state, State::Done);
150            match cur {
151                State::Reading(mut s) => match s.poll_next_unpin(cx) {
152                    Poll::Ready(Some(Ok(chunk))) => {
153                        self.reconnect_attempt = 0;
154                        self.buf.push(&chunk);
155                        self.state = State::Reading(s);
156                        continue;
157                    }
158                    Poll::Ready(Some(Err(e))) => {
159                        let err: Error = e.into();
160                        if err.is_transient() && self.reopen.is_some() {
161                            // Schedule a reconnect.
162                            self.reconnect_attempt = self.reconnect_attempt.saturating_add(1);
163                            let delay = self.reconnect_delay();
164                            self.state = State::Backoff(Box::pin(tokio::time::sleep(delay)));
165                            continue;
166                        }
167                        self.state = State::Done;
168                        return Poll::Ready(Some(Err(err)));
169                    }
170                    Poll::Ready(None) => {
171                        // Body finished without [DONE]. Flush any final event,
172                        // then complete.
173                        if let Some(ev) = self.buf.finish() {
174                            self.state = State::Done;
175                            return match ev {
176                                SseEvent::Data(payload) => {
177                                    let decoded: Result<T> = serde_json::from_slice(&payload)
178                                        .map_err(|e| {
179                                            Error::Stream(format!("malformed SSE payload: {e}"))
180                                        });
181                                    Poll::Ready(Some(decoded))
182                                }
183                                SseEvent::Done => Poll::Ready(None),
184                            };
185                        }
186                        self.state = State::Done;
187                        return Poll::Ready(None);
188                    }
189                    Poll::Pending => {
190                        self.state = State::Reading(s);
191                        return Poll::Pending;
192                    }
193                },
194                State::Backoff(mut fut) => match fut.as_mut().poll(cx) {
195                    Poll::Ready(()) => {
196                        let reopen = self
197                            .reopen
198                            .clone()
199                            .expect("Backoff state requires a reopen closure");
200                        let f = (reopen)();
201                        self.state = State::Reopening(f);
202                        continue;
203                    }
204                    Poll::Pending => {
205                        self.state = State::Backoff(fut);
206                        return Poll::Pending;
207                    }
208                },
209                State::Reopening(mut fut) => match fut.as_mut().poll(cx) {
210                    Poll::Ready(Ok(resp)) => {
211                        self.state = State::Reading(resp.bytes_stream().boxed());
212                        continue;
213                    }
214                    Poll::Ready(Err(err)) => {
215                        if err.is_transient() {
216                            // Try again, subject to the cap.
217                            self.reconnect_attempt = self.reconnect_attempt.saturating_add(1);
218                            let delay = self.reconnect_delay();
219                            self.state = State::Backoff(Box::pin(tokio::time::sleep(delay)));
220                            continue;
221                        }
222                        self.state = State::Done;
223                        return Poll::Ready(Some(Err(err)));
224                    }
225                    Poll::Pending => {
226                        self.state = State::Reopening(fut);
227                        return Poll::Pending;
228                    }
229                },
230                State::Done => {
231                    self.state = State::Done;
232                    return Poll::Ready(None);
233                }
234            }
235        }
236    }
237}
238
239/// A parsed SSE event.
240#[derive(Debug, PartialEq, Eq)]
241enum SseEvent {
242    /// Concatenated `data:` payload of a single event.
243    Data(Vec<u8>),
244    /// `data: [DONE]` terminator.
245    Done,
246}
247
248/// Incremental SSE parser. Accumulates bytes and yields complete events as
249/// `data:` lines are joined and blank lines flush them.
250#[derive(Default)]
251struct SseBuffer {
252    /// Raw bytes not yet consumed as full lines (no trailing `\n` seen).
253    pending: Vec<u8>,
254    /// Lines that belong to the in-progress event (each is one `data:` payload
255    /// without the `data:` prefix or trailing newline).
256    current_data: Vec<Vec<u8>>,
257    /// Whether the current event contained at least one `data:` line.
258    has_data: bool,
259}
260
261impl SseBuffer {
262    fn pending_len(&self) -> usize {
263        self.pending.len()
264    }
265
266    fn push(&mut self, chunk: &[u8]) {
267        self.pending.extend_from_slice(chunk);
268    }
269
270    /// Pop the next complete event from the buffer, if one is available.
271    fn next_event(&mut self) -> Option<SseEvent> {
272        loop {
273            let idx = self.pending.iter().position(|&b| b == b'\n')?;
274            // Take the line (excluding the `\n`); trim any trailing `\r`.
275            let mut line: Vec<u8> = self.pending.drain(..=idx).collect();
276            line.pop(); // remove the `\n`
277            if line.last() == Some(&b'\r') {
278                line.pop();
279            }
280            if let Some(ev) = self.process_line(line) {
281                return Some(ev);
282            }
283        }
284    }
285
286    /// Called when the upstream byte stream is exhausted. Flushes any
287    /// pending bytes as a final line.
288    fn finish(&mut self) -> Option<SseEvent> {
289        if !self.pending.is_empty() {
290            let mut line = std::mem::take(&mut self.pending);
291            if line.last() == Some(&b'\r') {
292                line.pop();
293            }
294            if let Some(ev) = self.process_line(line) {
295                return Some(ev);
296            }
297        }
298        self.flush_event()
299    }
300
301    fn process_line(&mut self, line: Vec<u8>) -> Option<SseEvent> {
302        if line.is_empty() {
303            return self.flush_event();
304        }
305        // Comment line.
306        if line.first() == Some(&b':') {
307            return None;
308        }
309        // Strip the field name. SSE allows arbitrary fields; we only care about `data`.
310        if let Some(rest) = strip_field(&line, b"data") {
311            self.current_data.push(rest);
312            self.has_data = true;
313        }
314        // Other fields (event:, id:, retry:) are intentionally ignored — the
315        // OpenRouter SSE stream uses only `data:`.
316        None
317    }
318
319    fn flush_event(&mut self) -> Option<SseEvent> {
320        if !self.has_data {
321            return None;
322        }
323        self.has_data = false;
324        let lines = std::mem::take(&mut self.current_data);
325        // Per the SSE spec, multi-line `data:` payloads are joined by `\n`.
326        let mut payload: Vec<u8> = Vec::new();
327        for (i, l) in lines.iter().enumerate() {
328            if i > 0 {
329                payload.push(b'\n');
330            }
331            payload.extend_from_slice(l);
332        }
333        // `[DONE]` terminator is treated specially.
334        if payload == b"[DONE]" {
335            return Some(SseEvent::Done);
336        }
337        Some(SseEvent::Data(payload))
338    }
339}
340
341/// If `line` starts with `field:`, return the value (with at most one leading
342/// space trimmed, per the SSE spec).
343fn strip_field(line: &[u8], field: &[u8]) -> Option<Vec<u8>> {
344    if line.len() < field.len() + 1 {
345        return None;
346    }
347    if &line[..field.len()] != field {
348        return None;
349    }
350    if line[field.len()] != b':' {
351        return None;
352    }
353    let mut rest = &line[field.len() + 1..];
354    if rest.first() == Some(&b' ') {
355        rest = &rest[1..];
356    }
357    Some(rest.to_vec())
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use futures::stream;
364    use pretty_assertions::assert_eq;
365
366    fn drain_buffer(buf: &mut SseBuffer) -> Vec<SseEvent> {
367        let mut out = Vec::new();
368        while let Some(ev) = buf.next_event() {
369            out.push(ev);
370        }
371        if let Some(ev) = buf.finish() {
372            out.push(ev);
373        }
374        out
375    }
376
377    #[test]
378    fn parses_single_event() {
379        let mut b = SseBuffer::default();
380        b.push(b"data: {\"x\":1}\n\n");
381        let events = drain_buffer(&mut b);
382        assert_eq!(events, vec![SseEvent::Data(b"{\"x\":1}".to_vec())]);
383    }
384
385    #[test]
386    fn parses_done_terminator() {
387        let mut b = SseBuffer::default();
388        b.push(b"data: [DONE]\n\n");
389        let events = drain_buffer(&mut b);
390        assert_eq!(events, vec![SseEvent::Done]);
391    }
392
393    #[test]
394    fn ignores_comment_lines() {
395        let mut b = SseBuffer::default();
396        b.push(b": heartbeat\ndata: {\"a\":1}\n\n");
397        let events = drain_buffer(&mut b);
398        assert_eq!(events, vec![SseEvent::Data(b"{\"a\":1}".to_vec())]);
399    }
400
401    #[test]
402    fn joins_multi_line_data() {
403        let mut b = SseBuffer::default();
404        b.push(b"data: line1\ndata: line2\n\n");
405        let events = drain_buffer(&mut b);
406        assert_eq!(events, vec![SseEvent::Data(b"line1\nline2".to_vec())]);
407    }
408
409    #[test]
410    fn handles_crlf_line_endings() {
411        let mut b = SseBuffer::default();
412        b.push(b"data: {\"x\":1}\r\n\r\n");
413        let events = drain_buffer(&mut b);
414        assert_eq!(events, vec![SseEvent::Data(b"{\"x\":1}".to_vec())]);
415    }
416
417    #[test]
418    fn handles_chunk_boundaries() {
419        let mut b = SseBuffer::default();
420        b.push(b"data: {\"x");
421        assert!(b.next_event().is_none());
422        b.push(b"\":1}\n");
423        // No terminating blank line yet — event not flushed.
424        assert!(b.next_event().is_none());
425        b.push(b"\n");
426        let events = drain_buffer(&mut b);
427        assert_eq!(events, vec![SseEvent::Data(b"{\"x\":1}".to_vec())]);
428    }
429
430    #[test]
431    fn ignores_non_data_fields() {
432        let mut b = SseBuffer::default();
433        b.push(b"event: ping\nid: 42\nretry: 1000\ndata: {\"x\":1}\n\n");
434        let events = drain_buffer(&mut b);
435        assert_eq!(events, vec![SseEvent::Data(b"{\"x\":1}".to_vec())]);
436    }
437
438    #[test]
439    fn flushes_trailing_event_without_blank_line() {
440        let mut b = SseBuffer::default();
441        b.push(b"data: {\"x\":1}\n");
442        // No second \n; finish() flushes.
443        let events = drain_buffer(&mut b);
444        assert_eq!(events, vec![SseEvent::Data(b"{\"x\":1}".to_vec())]);
445    }
446
447    #[test]
448    fn handles_empty_data_payload() {
449        let mut b = SseBuffer::default();
450        b.push(b"data: \n\n");
451        let events = drain_buffer(&mut b);
452        assert_eq!(events, vec![SseEvent::Data(Vec::new())]);
453    }
454
455    #[derive(serde::Deserialize, Debug, PartialEq)]
456    struct Sample {
457        x: i32,
458    }
459
460    #[tokio::test]
461    async fn event_stream_yields_decoded_events_then_done() {
462        let chunks: Vec<std::result::Result<Bytes, reqwest::Error>> = vec![
463            Ok(Bytes::from_static(b"data: {\"x\":1}\n\n")),
464            Ok(Bytes::from_static(b"data: {\"x\":2}\n\n")),
465            Ok(Bytes::from_static(b"data: [DONE]\n\n")),
466        ];
467        let body: ByteStream = stream::iter(chunks).boxed();
468        let mut s: EventStream<Sample> = EventStream::from_bytes_stream(body);
469        let a = s.next().await.unwrap().unwrap();
470        let b = s.next().await.unwrap().unwrap();
471        assert_eq!(a, Sample { x: 1 });
472        assert_eq!(b, Sample { x: 2 });
473        assert!(s.next().await.is_none());
474    }
475
476    #[tokio::test]
477    async fn event_stream_surfaces_malformed_payload_as_error() {
478        let chunks: Vec<std::result::Result<Bytes, reqwest::Error>> =
479            vec![Ok(Bytes::from_static(b"data: not-json\n\n"))];
480        let body: ByteStream = stream::iter(chunks).boxed();
481        let mut s: EventStream<Sample> = EventStream::from_bytes_stream(body);
482        let item = s.next().await.unwrap();
483        assert!(matches!(item, Err(Error::Stream(_))));
484    }
485
486    #[tokio::test]
487    async fn event_stream_handles_split_event_across_chunks() {
488        let chunks: Vec<std::result::Result<Bytes, reqwest::Error>> = vec![
489            Ok(Bytes::from_static(b"data: {\"x")),
490            Ok(Bytes::from_static(b"\":7}\n\n")),
491            Ok(Bytes::from_static(b"data: [DONE]\n\n")),
492        ];
493        let body: ByteStream = stream::iter(chunks).boxed();
494        let mut s: EventStream<Sample> = EventStream::from_bytes_stream(body);
495        let a = s.next().await.unwrap().unwrap();
496        assert_eq!(a, Sample { x: 7 });
497        assert!(s.next().await.is_none());
498    }
499}