Skip to main content

rustlavel_ai/
provider.rs

1//! The provider trait and the text stream every provider produces.
2
3use crate::completion::{Completion, StopReason, Usage};
4use crate::request::Request;
5use rustlavel_client::stream::{Body, SseReader};
6use rustlavel_core::events::Event;
7use rustlavel_core::{Json, Result};
8use std::collections::VecDeque;
9use std::future::Future;
10use std::pin::Pin;
11use std::time::{Duration, Instant};
12
13/// A boxed future, which is how this crate spells "async trait method".
14///
15/// `async fn` in a trait would be neater, but it is not object-safe, and the
16/// whole point here is choosing a provider at runtime from configuration.
17pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
18
19/// Something that can answer a [`Request`].
20///
21/// Three implementations ship with the framework and a fourth, [`crate::Fake`],
22/// is what an application's tests talk to.
23pub trait Provider: Send + Sync + 'static {
24    /// A short, stable name — it goes into the `ai.call` event.
25    fn name(&self) -> &'static str;
26
27    /// What to use when the caller did not name a model.
28    fn default_model(&self) -> &'static str;
29
30    /// Ask for a complete answer.
31    fn complete<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Completion>>;
32
33    /// Ask for an answer as it is written.
34    fn stream<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<TextStream>>;
35}
36
37/// One decoded piece of a streaming answer.
38///
39/// Providers interleave text with bookkeeping — token counts arrive at the
40/// end, stop reasons in their own frame — so a decoded frame may carry any
41/// combination of them, or nothing at all.
42#[derive(Debug, Clone, Default, PartialEq)]
43pub struct StreamDelta {
44    pub text: Option<String>,
45    pub usage: Option<Usage>,
46    pub stop_reason: Option<StopReason>,
47    /// The provider said this was the last frame.
48    pub done: bool,
49}
50
51impl StreamDelta {
52    pub fn text(text: impl Into<String>) -> StreamDelta {
53        StreamDelta { text: Some(text.into()), ..StreamDelta::default() }
54    }
55
56    pub fn nothing() -> StreamDelta {
57        StreamDelta::default()
58    }
59
60    pub fn done() -> StreamDelta {
61        StreamDelta { done: true, ..StreamDelta::default() }
62    }
63
64    pub fn with_usage(mut self, usage: Usage) -> StreamDelta {
65        self.usage = Some(usage);
66        self
67    }
68
69    pub fn with_stop_reason(mut self, reason: StopReason) -> StreamDelta {
70        self.stop_reason = Some(reason);
71        self
72    }
73}
74
75/// Turns one wire frame into a [`StreamDelta`].
76///
77/// The first argument is the SSE event name where there is one; Ollama sends
78/// bare JSON lines and passes `None`.
79pub type Decoder = fn(Option<&str>, &str) -> Result<StreamDelta>;
80
81/// Where the frames come from.
82enum Wire {
83    /// Named server-sent events, as Anthropic and OpenAI send them.
84    Events(SseReader),
85    /// Newline-delimited JSON, as Ollama sends it.
86    Lines(LineReader),
87    /// Already-decoded deltas, from a fake.
88    Scripted(VecDeque<String>),
89}
90
91/// A streaming answer, read one delta at a time.
92///
93/// ```ignore
94/// let mut stream = ai.prompt("Tell me a story").stream().await?;
95/// while let Some(delta) = stream.next().await? {
96///     print!("{delta}");
97/// }
98/// ```
99pub struct TextStream {
100    wire: Wire,
101    decode: Decoder,
102    usage: Usage,
103    stop_reason: Option<StopReason>,
104    finished: bool,
105    /// Set when this stream should report itself to the event bus once done.
106    call: Option<Call>,
107}
108
109/// What a finished stream reports to the instrumentation bus.
110struct Call {
111    provider: &'static str,
112    model: String,
113    started: Instant,
114}
115
116impl TextStream {
117    /// A stream over server-sent events.
118    pub fn events(body: Body, decode: Decoder) -> TextStream {
119        TextStream::wrap(Wire::Events(body.events()), decode)
120    }
121
122    /// A stream over newline-delimited JSON.
123    pub fn lines(body: Body, decode: Decoder) -> TextStream {
124        TextStream::wrap(Wire::Lines(LineReader::new(body)), decode)
125    }
126
127    /// A stream of deltas decided in advance, for fakes and tests.
128    pub fn scripted(deltas: impl IntoIterator<Item = String>) -> TextStream {
129        TextStream::wrap(Wire::Scripted(deltas.into_iter().collect()), |_, _| {
130            Ok(StreamDelta::nothing())
131        })
132    }
133
134    fn wrap(wire: Wire, decode: Decoder) -> TextStream {
135        TextStream {
136            wire,
137            decode,
138            usage: Usage::default(),
139            stop_reason: None,
140            finished: false,
141            call: None,
142        }
143    }
144
145    /// Report an `ai.call` event when this stream runs out.
146    ///
147    /// Only then are the token counts known: providers put them in the last
148    /// frame, so a stream cannot be measured when it is opened.
149    pub fn measured(mut self, provider: &'static str, model: impl Into<String>) -> TextStream {
150        self.call = Some(Call { provider, model: model.into(), started: Instant::now() });
151        self
152    }
153
154    /// The next piece of text, or `None` at the end of the answer.
155    ///
156    /// Frames that carry only bookkeeping are consumed silently, so a caller
157    /// never has to know what a `message_delta` is.
158    pub async fn next(&mut self) -> Result<Option<String>> {
159        loop {
160            if self.finished {
161                return Ok(None);
162            }
163
164            let frame = match &mut self.wire {
165                Wire::Scripted(deltas) => match deltas.pop_front() {
166                    Some(text) => return Ok(Some(text)),
167                    None => None,
168                },
169                Wire::Events(reader) => reader
170                    .next()
171                    .await?
172                    .map(|event| (event.event.clone(), event.data.clone())),
173                Wire::Lines(reader) => reader.next_line().await?.map(|line| (None, line)),
174            };
175
176            let Some((name, data)) = frame else {
177                self.finish();
178                return Ok(None);
179            };
180
181            if data.trim().is_empty() {
182                continue;
183            }
184
185            let delta = (self.decode)(name.as_deref(), &data)?;
186            if let Some(usage) = delta.usage {
187                // Providers report input and output counts in different frames;
188                // taking the larger of each keeps a late zero from erasing one.
189                self.usage.input_tokens = self.usage.input_tokens.max(usage.input_tokens);
190                self.usage.output_tokens = self.usage.output_tokens.max(usage.output_tokens);
191            }
192            if let Some(reason) = delta.stop_reason {
193                self.stop_reason = Some(reason);
194            }
195            if delta.done {
196                self.finish();
197                return Ok(None);
198            }
199            match delta.text {
200                Some(text) if !text.is_empty() => return Ok(Some(text)),
201                _ => continue,
202            }
203        }
204    }
205
206    /// Read the whole answer, for a caller who wanted streaming for the
207    /// latency rather than for the display.
208    pub async fn collect(&mut self) -> Result<String> {
209        let mut out = String::new();
210        while let Some(delta) = self.next().await? {
211            out.push_str(&delta);
212        }
213        Ok(out)
214    }
215
216    /// The tokens reported so far. Complete only once the stream has ended.
217    pub fn usage(&self) -> Usage {
218        self.usage
219    }
220
221    pub fn stop_reason(&self) -> Option<&StopReason> {
222        self.stop_reason.as_ref()
223    }
224
225    fn finish(&mut self) {
226        self.finished = true;
227        if let Some(call) = self.call.take() {
228            record_call(call.provider, &call.model, self.usage, call.started.elapsed(), true);
229        }
230    }
231}
232
233/// Reads a body as newline-delimited JSON.
234struct LineReader {
235    body: Body,
236    pending: String,
237    drained: bool,
238}
239
240impl LineReader {
241    fn new(body: Body) -> LineReader {
242        LineReader { body, pending: String::new(), drained: false }
243    }
244
245    async fn next_line(&mut self) -> Result<Option<String>> {
246        loop {
247            if let Some(at) = self.pending.find('\n') {
248                let line: String = self.pending.drain(..at + 1).collect();
249                let line = line.trim().to_string();
250                if line.is_empty() {
251                    continue;
252                }
253                return Ok(Some(line));
254            }
255            if self.drained {
256                // Ollama's last object may arrive without a trailing newline.
257                let rest = std::mem::take(&mut self.pending);
258                let rest = rest.trim().to_string();
259                return Ok(if rest.is_empty() { None } else { Some(rest) });
260            }
261            match self.body.chunk().await? {
262                Some(bytes) => self.pending.push_str(&String::from_utf8_lossy(&bytes)),
263                None => self.drained = true,
264            }
265        }
266    }
267}
268
269/// Report a call on the instrumentation bus.
270///
271/// Deliberately narrow: provider, model, token counts and duration. The prompt
272/// and the API key never appear here — an event goes to Telescope's storage and
273/// to whatever else subscribed, and neither is a place for either.
274pub(crate) fn record_call(
275    provider: &str,
276    model: &str,
277    usage: Usage,
278    elapsed: Duration,
279    streamed: bool,
280) {
281    if !rustlavel_core::events::has_subscribers() {
282        return;
283    }
284    Event::new("ai.call")
285        .with("provider", provider)
286        .with("model", model)
287        .with("input_tokens", usage.input_tokens)
288        .with("output_tokens", usage.output_tokens)
289        .with("streamed", streamed)
290        .took(elapsed)
291        .dispatch();
292}
293
294/// Read a number from a provider's usage block, whatever it calls it.
295pub(crate) fn token_count(value: &Json, path: &str) -> u32 {
296    value.get(path).and_then(Json::as_i64).unwrap_or(0).max(0) as u32
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use rustlavel_http::{Headers, Status};
303
304    fn body(text: &str) -> Body {
305        Body::from_bytes(Status::OK, Headers::new(), text.as_bytes().to_vec())
306    }
307
308    /// A decoder in the shape every real provider's is: text in `t`, counts in
309    /// `n`, and a `done` flag.
310    fn decode(_name: Option<&str>, data: &str) -> Result<StreamDelta> {
311        let value = Json::parse(data)?;
312        if value.get("done").and_then(Json::as_bool) == Some(true) {
313            return Ok(StreamDelta::done().with_usage(Usage::new(11, 22)));
314        }
315        Ok(StreamDelta::text(value.get("t").and_then(Json::as_str).unwrap_or("")))
316    }
317
318    #[tokio::test]
319    async fn a_scripted_stream_hands_back_its_deltas_in_order() {
320        let mut stream = TextStream::scripted(["Hel".to_string(), "lo".to_string()]);
321
322        assert_eq!(stream.next().await.unwrap().as_deref(), Some("Hel"));
323        assert_eq!(stream.next().await.unwrap().as_deref(), Some("lo"));
324        assert!(stream.next().await.unwrap().is_none());
325    }
326
327    #[tokio::test]
328    async fn an_event_stream_skips_bookkeeping_frames_and_keeps_the_usage() {
329        let source = "data: {\"t\":\"Hel\"}\n\ndata: {\"t\":\"\"}\n\ndata: {\"t\":\"lo\"}\n\n\
330                      data: {\"done\":true}\n\n";
331        let mut stream = TextStream::events(body(source), decode);
332
333        assert_eq!(stream.collect().await.unwrap(), "Hello");
334        assert_eq!(stream.usage(), Usage::new(11, 22));
335    }
336
337    #[tokio::test]
338    async fn a_line_stream_reads_newline_delimited_json_including_an_unterminated_last_line() {
339        let source = "{\"t\":\"one \"}\n{\"t\":\"two\"}\n\n{\"done\":true}";
340        let mut stream = TextStream::lines(body(source), decode);
341
342        assert_eq!(stream.collect().await.unwrap(), "one two");
343        assert_eq!(stream.usage().total(), 33);
344    }
345
346    #[tokio::test]
347    async fn a_finished_stream_stays_finished() {
348        let mut stream = TextStream::scripted(["only".to_string()]);
349
350        assert!(stream.next().await.unwrap().is_some());
351        assert!(stream.next().await.unwrap().is_none());
352        assert!(stream.next().await.unwrap().is_none());
353    }
354
355    #[test]
356    fn token_counts_default_to_zero_rather_than_failing() {
357        let value = Json::object([("usage", Json::object([("input_tokens", 12.into())]))]);
358
359        assert_eq!(token_count(&value, "usage.input_tokens"), 12);
360        assert_eq!(token_count(&value, "usage.output_tokens"), 0);
361    }
362}