1use 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
13pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
18
19pub trait Provider: Send + Sync + 'static {
24 fn name(&self) -> &'static str;
26
27 fn default_model(&self) -> &'static str;
29
30 fn complete<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Completion>>;
32
33 fn stream<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<TextStream>>;
35}
36
37#[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 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
75pub type Decoder = fn(Option<&str>, &str) -> Result<StreamDelta>;
80
81enum Wire {
83 Events(SseReader),
85 Lines(LineReader),
87 Scripted(VecDeque<String>),
89}
90
91pub struct TextStream {
100 wire: Wire,
101 decode: Decoder,
102 usage: Usage,
103 stop_reason: Option<StopReason>,
104 finished: bool,
105 call: Option<Call>,
107}
108
109struct Call {
111 provider: &'static str,
112 model: String,
113 started: Instant,
114}
115
116impl TextStream {
117 pub fn events(body: Body, decode: Decoder) -> TextStream {
119 TextStream::wrap(Wire::Events(body.events()), decode)
120 }
121
122 pub fn lines(body: Body, decode: Decoder) -> TextStream {
124 TextStream::wrap(Wire::Lines(LineReader::new(body)), decode)
125 }
126
127 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 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 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 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 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 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
233struct 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 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
269pub(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
294pub(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 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}