Skip to main content

allama_http/
framing.rs

1//! Streaming for any wire framing: the chat stream driver and vendor
2//! frames.
3//!
4//! A streamed response arrives in *frames* of its wire framing: SSE events
5//! ([`sse`](crate::sse)), NDJSON lines ([`ndjson`](crate::ndjson)), or the
6//! messages of another framing an adapter frames itself. What turns frames
7//! into chat stream events is the same loop for all of them, and so are the
8//! rules a hand-written loop gets wrong, so they live here:
9//!
10//! - **The driver.** [`FramedChatStream`] runs an adapter's
11//!   [`ChatFrameDecoder`] over a stream of frames: it ends the stream after
12//!   the first error, runs the decoder's end-of-body or failure hook once,
13//!   classifies a failed read (a stream timeout stays a timeout), and never
14//!   polls the body after the stream ended.
15//! - **Vendor frames.** [`VendorFrames`] surfaces frames of a type an
16//!   adapter does not model (`event: search_results`, an NDJSON line of an
17//!   unknown `type`) as response-level provider data under the adapter's
18//!   namespace, with one warning per frame type, so they are never decoded
19//!   as the adapter's own and never dropped.
20//!
21//! Framing the bytes stays with a framer ([`SseStream`](crate::sse::SseStream),
22//! [`NdjsonStream`](crate::ndjson::NdjsonStream), or your own stream of
23//! frames), and decoding the frames' data with each adapter: the helpers
24//! frame, drive and surface, they do not interpret.
25
26use std::borrow::Cow;
27use std::collections::{BTreeMap, VecDeque};
28use std::fmt;
29use std::pin::Pin;
30use std::task::{Context, Poll, ready};
31
32use allama::{ProviderData, Result, StreamEvent, Subject, Warning, WarningKind};
33use futures_core::{Stream, TryStream};
34use serde_json::Value;
35
36/// An adapter's decoder for one streamed response: frames of type `F` in
37/// (an [`SseEvent`](crate::sse::SseEvent), an NDJSON line), chat stream
38/// events out. [`FramedChatStream`] drives it.
39///
40/// The decoder holds what only the adapter knows: its wire format, when its
41/// response starts and ends, and its error classification. The driver holds
42/// the loop every streaming adapter needs, and its guarantees:
43///
44/// - each frame goes to [`frame`](Self::frame), in order;
45/// - the stream ends after the first error, from the decoder or from the
46///   body: the items after it are dropped;
47/// - when the body ends, [`end`](Self::end) runs; when reading or framing
48///   it fails, [`fail`](Self::fail) runs, with the failure already
49///   classified. At most one of them runs, once, and only while the decoder
50///   is not done (it has yielded no error and [`is_done`](Self::is_done) is
51///   `false`);
52/// - the stream ends once the decoder is done, without waiting for the
53///   body to close, and the body is never polled after the stream ended.
54///
55/// A decoder yields warnings and response-level provider data as they
56/// arrive, even before its `ResponseStart`:
57/// [`Prepared::finish_stream`](allama::adapter::chat::Prepared::finish_stream) puts
58/// them after the start ([`start_first`](allama::adapter::chat::start_first)).
59///
60/// An SSE adapter implements `ChatFrameDecoder<SseEvent>`, and
61/// [`SseChatStream`](crate::sse::SseChatStream) drives it.
62///
63/// Each method returns the items of one frame as a `Vec`, and this shape is
64/// deliberate and stable. An empty `Vec` does not allocate, and a frame that
65/// yields events was decoded from JSON anyway, so an `out: &mut Vec<_>` sink
66/// parameter would save one small allocation per frame and cost every decoder
67/// a harder signature: early returns become pushes, and a decoder that
68/// forgets to push loses an error silently.
69///
70/// ```
71/// use allama::{Error, Result, StreamEvent, StreamErrorKind};
72/// use allama_http::TransportError;
73/// use allama_http::framing::{ChatFrameDecoder, FramedChatStream};
74/// use allama_http::ndjson::NdjsonStream;
75/// use futures_util::{FutureExt, StreamExt, stream};
76///
77/// /// `{"text": ..}` lines, then `{"done": true}`.
78/// struct Lines {
79///     started: bool,
80///     done: bool,
81/// }
82///
83/// impl ChatFrameDecoder<String> for Lines {
84///     fn frame(&mut self, line: String) -> Vec<Result<StreamEvent>> {
85///         let json: serde_json::Value = match serde_json::from_str(&line) {
86///             Ok(json) => json,
87///             Err(error) => return vec![Err(Error::stream_decode(error.to_string()))],
88///         };
89///         let mut out = Vec::new();
90///         if !self.started {
91///             self.started = true;
92///             out.push(Ok(StreamEvent::response_start(None, None)));
93///             out.push(Ok(StreamEvent::block_start(0, allama::StreamBlockType::Text)));
94///         }
95///         if json["done"] == true {
96///             self.done = true;
97///             out.push(Ok(StreamEvent::block_stop(0)));
98///             out.push(Ok(StreamEvent::ResponseStop));
99///         } else {
100///             out.push(Ok(StreamEvent::text_delta(0, json["text"].as_str().unwrap_or_default())));
101///         }
102///         out
103///     }
104///
105///     fn end(&mut self) -> Vec<Result<StreamEvent>> {
106///         let error = Error::stream(StreamErrorKind::Incomplete, "the stream ended early");
107///         vec![Err(error.authored().with_provider("lines"))]
108///     }
109///
110///     fn fail(&mut self, error: Error) -> Vec<Result<StreamEvent>> {
111///         vec![Err(error.with_provider("lines"))]
112///     }
113///
114///     fn is_done(&self) -> bool {
115///         self.done
116///     }
117/// }
118///
119/// let body = stream::iter([Ok::<_, TransportError>(
120///     &b"{\"text\":\"Hel\"}\n{\"text\":\"lo\"}\n{\"done\":true}\n"[..],
121/// )]);
122/// let lines = NdjsonStream::new(body);
123/// let stream = FramedChatStream::new(lines, Lines { started: false, done: false });
124/// let events: Vec<_> = stream.collect().now_or_never().unwrap();
125/// assert_eq!(events.len(), 6);
126/// assert!(matches!(events[5], Ok(StreamEvent::ResponseStop)));
127/// ```
128pub trait ChatFrameDecoder<F>: Send + 'static {
129    /// Decode one frame. An error among the items is the stream's last
130    /// item: the driver drops the items after it and ends the stream.
131    fn frame(&mut self, frame: F) -> Vec<Result<StreamEvent>>;
132
133    /// The body ended before the decoder was done: what the stream still
134    /// yields, typically a
135    /// [`StreamErrorKind::Incomplete`](allama::StreamErrorKind::Incomplete)
136    /// error, since the terminal frame never arrived. The stream ends after
137    /// it, whatever it returns, nothing included.
138    fn end(&mut self) -> Vec<Result<StreamEvent>>;
139
140    /// Reading or framing the body failed before the decoder was done.
141    /// `error` is the classified failure (the framer's error converted
142    /// into an [`allama::Error`], or the driver's error map). The default
143    /// yields it as it is; override it to close open blocks first, to
144    /// attach the provider and the request id, or to add what the adapter
145    /// knows about where to resume. The stream ends after it.
146    fn fail(&mut self, error: allama::Error) -> Vec<Result<StreamEvent>> {
147        vec![Err(error)]
148    }
149
150    /// Whether the decoder is done: its terminal frame arrived. The driver
151    /// then ends the stream without waiting for the body to close, and
152    /// calls neither [`end`](Self::end) nor [`fail`](Self::fail). The
153    /// default is `false`: the stream ends with the body.
154    fn is_done(&self) -> bool {
155        false
156    }
157}
158
159/// A chat stream decoded from a response's frames by an adapter's
160/// [`ChatFrameDecoder`], with the driver guarantees the decoder trait
161/// lists.
162///
163/// `frames` is any stream of `Result<frame, error>`: an
164/// [`SseStream`](crate::sse::SseStream), an
165/// [`NdjsonStream`](crate::ndjson::NdjsonStream), or a framer of your own.
166/// It is unboxed and `Send` (for a `Send` body); box it into a
167/// [`ChatStream`](allama::ChatStream) with
168/// [`Prepared::finish_stream`](allama::adapter::chat::Prepared::finish_stream):
169///
170/// ```ignore
171/// let lines = NdjsonStream::new(response.into_body()).max_line_size(max_stream_event_size);
172/// Ok(prepared.finish_stream(FramedChatStream::new(lines, StreamDecoder::new(request_id))))
173/// ```
174pub struct FramedChatStream<S: TryStream, D> {
175    /// The frames; `None` once the stream ended, so the body is dropped
176    /// and never polled again.
177    frames: Option<S>,
178    map: ErrorMap<S::Error>,
179    driver: Driver<D>,
180}
181
182impl<S: TryStream, D: ChatFrameDecoder<S::Ok>> FramedChatStream<S, D> {
183    /// Drive `decoder` over `frames`, classifying a failure with its
184    /// conversion into an [`allama::Error`]. The bundled framers' errors
185    /// convert for a body whose error converts into a
186    /// [`TransportError`](crate::TransportError), as an
187    /// [`HttpResponse`](crate::HttpResponse) body's does
188    /// ([`SseError::into_error`](crate::sse::SseError::into_error),
189    /// [`NdjsonError::into_error`](crate::ndjson::NdjsonError::into_error)).
190    #[must_use]
191    pub fn new(frames: S, decoder: D) -> Self
192    where
193        S::Error: Into<allama::Error> + 'static,
194    {
195        Self::with_error_map(frames, decoder, Into::into)
196    }
197
198    /// Drive `decoder` over `frames`, classifying a failure with `map`, for
199    /// a body whose error is not a [`TransportError`](crate::TransportError)
200    /// (a platform's own byte stream). `map` must keep the rules of the
201    /// framer's own classification: a timeout stays a timeout, bytes the
202    /// framer cannot decode are a stream decode error, a frame past the
203    /// size cap a stream protocol error, and the messages are the
204    /// adapter's own.
205    #[must_use]
206    pub fn with_error_map<M>(frames: S, decoder: D, map: M) -> Self
207    where
208        S::Error: 'static,
209        M: FnMut(S::Error) -> allama::Error + Send + 'static,
210    {
211        Self {
212            frames: Some(frames),
213            map: Box::new(map),
214            driver: Driver::new(decoder),
215        }
216    }
217}
218
219// The driver never pins its fields: it polls the frames through
220// `Pin::new`, which needs them `Unpin` itself.
221impl<S: TryStream, D> Unpin for FramedChatStream<S, D> {}
222
223impl<S, F, E, D> Stream for FramedChatStream<S, D>
224where
225    S: Stream<Item = std::result::Result<F, E>> + Unpin,
226    D: ChatFrameDecoder<F>,
227{
228    type Item = Result<StreamEvent>;
229
230    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
231        let this = self.get_mut();
232        this.driver.poll(&mut this.frames, &mut this.map, cx)
233    }
234}
235
236impl<S: TryStream, D> fmt::Debug for FramedChatStream<S, D> {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        f.debug_struct("FramedChatStream")
239            .field("ended", &self.frames.is_none())
240            .field("queued", &self.driver.queued())
241            .finish_non_exhaustive()
242    }
243}
244
245/// Surfaces vendor frames: stream frames of a type an adapter does not
246/// model.
247///
248/// Endpoints and proxies add frames to standard streams: a named SSE event
249/// (`event: search_results`), an NDJSON line of a `type` the API does not
250/// define. An adapter never decodes them as its own and never drops them:
251/// [`surface`](Self::surface) turns each one into events, as it arrives,
252/// and [`finish`](Self::finish) completes them when the response ends.
253///
254/// # Events
255///
256/// - The first frame of a type is preceded by one [`Warning`]
257///   (`WarningKind::Other`, `Subject::Response`, with the provider name).
258/// - Each frame yields a
259///   [`ResponseProviderData`](StreamEvent::ResponseProviderData) event
260///   under the adapter's namespace and the frame type, holding that frame's
261///   payload alone, as a one-element array: a live consumer sees each
262///   payload once, as it arrives, in constant work per frame.
263/// - [`finish`](Self::finish), called by the decoder just before its
264///   `ResponseStop`, yields one more `ResponseProviderData` event holding
265///   the whole array, in arrival order, of every type that arrived more
266///   than once. [`ProviderData::merge`] replaces a value by key, so a
267///   collector that merges the events ends with every payload of every
268///   type: the collected response's provider data holds the full arrays.
269///
270/// Before 2026-10-07 each event carried the whole array so far, which made a
271/// stream with a vendor frame per chunk quadratic in time and in what a
272/// recording holds. An adapter that forgets [`finish`](Self::finish) keeps
273/// only the last payload of a repeated type in its collected response.
274///
275/// A vendor frame never starts the response. Yield the returned events as
276/// they come, also before `ResponseStart`:
277/// [`Prepared::finish_stream`](allama::adapter::chat::Prepared::finish_stream) holds
278/// them until the start ([`start_first`](allama::adapter::chat::start_first)).
279/// `allama_conformance::stream_frames::assert_unknown_stream_frames_surfaced`
280/// checks the result.
281///
282/// ```
283/// use allama::{StreamEvent, WarningKind};
284/// use allama_http::framing::VendorFrames;
285///
286/// let mut frames = VendorFrames::new("demo", "openai-compat:demo");
287/// let first = frames.surface("search_results", r#"{"url":"https://a"}"#);
288/// assert!(matches!(
289///     &first[0],
290///     StreamEvent::Warning { warning, .. } if warning.kind == WarningKind::Other
291/// ));
292/// let second = frames.surface("search_results", "not json");
293/// let StreamEvent::ResponseProviderData { provider_data, .. } = &second[0] else {
294///     panic!("{second:?}")
295/// };
296/// assert_eq!(
297///     provider_data.get_raw("openai-compat:demo", "search_results"),
298///     Some(&serde_json::json!(["not json"])),
299/// );
300///
301/// // Before `ResponseStop`: the whole array of each repeated type.
302/// let Some(StreamEvent::ResponseProviderData { provider_data, .. }) = frames.finish() else {
303///     panic!()
304/// };
305/// assert_eq!(
306///     provider_data.get_raw("openai-compat:demo", "search_results"),
307///     Some(&serde_json::json!([{"url": "https://a"}, "not json"])),
308/// );
309/// ```
310#[derive(Debug, Clone)]
311pub struct VendorFrames {
312    provider: Cow<'static, str>,
313    namespace: Cow<'static, str>,
314    /// Payloads so far, by frame type.
315    payloads: BTreeMap<String, Vec<Value>>,
316}
317
318impl VendorFrames {
319    /// Vendor frames of a `provider` (for the warnings), surfaced under its
320    /// `namespace`.
321    #[must_use]
322    pub fn new(
323        provider: impl Into<Cow<'static, str>>,
324        namespace: impl Into<Cow<'static, str>>,
325    ) -> Self {
326        Self {
327            provider: provider.into(),
328            namespace: namespace.into(),
329            payloads: BTreeMap::new(),
330        }
331    }
332
333    /// Surface the raw `data` of a frame of type `event`: kept as JSON when
334    /// it parses, as the trimmed text otherwise. Empty data surfaces
335    /// nothing.
336    #[must_use]
337    pub fn surface(&mut self, event: &str, data: &str) -> Vec<StreamEvent> {
338        let data = data.trim();
339        if data.is_empty() {
340            return Vec::new();
341        }
342        let payload = serde_json::from_str(data).unwrap_or_else(|_| Value::String(data.to_owned()));
343        self.surface_payload(event, payload)
344    }
345
346    /// Surface a payload already decoded, for a frame whose type arrives
347    /// inside its data (an NDJSON line's `type`, an SSE event's JSON
348    /// `type`) rather than as an SSE event name.
349    #[must_use]
350    pub fn surface_payload(&mut self, event: &str, payload: Value) -> Vec<StreamEvent> {
351        let payloads = self.payloads.entry(event.to_owned()).or_default();
352        let mut events = Vec::with_capacity(2);
353        if payloads.is_empty() {
354            events.push(StreamEvent::warning(
355                Warning::new(
356                    WarningKind::Other,
357                    Subject::Response,
358                    format!(
359                        "unrecognized stream event `{event}`; its data is kept in the \
360                         response's provider data under `{}`",
361                        self.namespace
362                    ),
363                )
364                .with_provider(self.provider.clone()),
365            ));
366        }
367        events.push(StreamEvent::response_provider_data(
368            ProviderData::new().with_raw(
369                self.namespace.clone(),
370                event,
371                Value::Array(vec![payload.clone()]),
372            ),
373        ));
374        payloads.push(payload);
375        events
376    }
377
378    /// The event that completes the surfaced frames, for the decoder to
379    /// yield just before its `ResponseStop`: one
380    /// [`ResponseProviderData`](StreamEvent::ResponseProviderData) holding
381    /// the whole array of every type that arrived more than once, which
382    /// replaces the single payloads a collector merged. `None` when no type
383    /// repeated (the events already carry every payload). The frames are
384    /// taken: a second call yields nothing.
385    #[must_use]
386    pub fn finish(&mut self) -> Option<StreamEvent> {
387        let mut provider_data = ProviderData::new();
388        let mut any = false;
389        for (event, payloads) in std::mem::take(&mut self.payloads) {
390            if payloads.len() > 1 {
391                provider_data.insert_raw(self.namespace.clone(), event, Value::Array(payloads));
392                any = true;
393            }
394        }
395        any.then(|| StreamEvent::response_provider_data(provider_data))
396    }
397}
398
399/// Classifies a framer's failure.
400type ErrorMap<E> = Box<dyn FnMut(E) -> allama::Error + Send>;
401
402/// The loop of [`FramedChatStream`]: a decoder and the items it yielded
403/// that the stream has not.
404pub(crate) struct Driver<D> {
405    decoder: D,
406    queue: VecDeque<Result<StreamEvent>>,
407}
408
409impl<D> Driver<D> {
410    pub(crate) fn new(decoder: D) -> Self {
411        Self {
412            decoder,
413            queue: VecDeque::new(),
414        }
415    }
416
417    pub(crate) fn queued(&self) -> usize {
418        self.queue.len()
419    }
420
421    /// The next item of the stream decoded from `frames`, which is set to
422    /// `None` once the stream ended.
423    pub(crate) fn poll<S, F, E>(
424        &mut self,
425        frames: &mut Option<S>,
426        map: &mut ErrorMap<E>,
427        cx: &mut Context<'_>,
428    ) -> Poll<Option<Result<StreamEvent>>>
429    where
430        S: Stream<Item = std::result::Result<F, E>> + Unpin,
431        D: ChatFrameDecoder<F>,
432    {
433        loop {
434            if let Some(item) = self.queue.pop_front() {
435                return Poll::Ready(Some(item));
436            }
437            let Some(stream) = frames.as_mut() else {
438                return Poll::Ready(None);
439            };
440            let (items, last) = match ready!(Pin::new(stream).poll_next(cx)) {
441                Some(Ok(frame)) => (self.decoder.frame(frame), false),
442                Some(Err(error)) => (self.decoder.fail(map(error)), true),
443                None => (self.decoder.end(), true),
444            };
445            if self.queue(items) || last || self.decoder.is_done() {
446                *frames = None;
447            }
448        }
449    }
450
451    /// Queue `items` up to and including the first error, and say whether
452    /// there was one.
453    fn queue(&mut self, items: Vec<Result<StreamEvent>>) -> bool {
454        for item in items {
455            let failed = item.is_err();
456            self.queue.push_back(item);
457            if failed {
458                return true;
459            }
460        }
461        false
462    }
463}
464
465#[cfg(all(test, not(target_arch = "wasm32")))]
466mod tests;