use crate::completion::{Completion, StopReason, Usage};
use crate::request::Request;
use rustlavel_client::stream::{Body, SseReader};
use rustlavel_core::events::Event;
use rustlavel_core::{Json, Result};
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait Provider: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn default_model(&self) -> &'static str;
fn complete<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<Completion>>;
fn stream<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<TextStream>>;
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct StreamDelta {
pub text: Option<String>,
pub usage: Option<Usage>,
pub stop_reason: Option<StopReason>,
pub done: bool,
}
impl StreamDelta {
pub fn text(text: impl Into<String>) -> StreamDelta {
StreamDelta { text: Some(text.into()), ..StreamDelta::default() }
}
pub fn nothing() -> StreamDelta {
StreamDelta::default()
}
pub fn done() -> StreamDelta {
StreamDelta { done: true, ..StreamDelta::default() }
}
pub fn with_usage(mut self, usage: Usage) -> StreamDelta {
self.usage = Some(usage);
self
}
pub fn with_stop_reason(mut self, reason: StopReason) -> StreamDelta {
self.stop_reason = Some(reason);
self
}
}
pub type Decoder = fn(Option<&str>, &str) -> Result<StreamDelta>;
enum Wire {
Events(SseReader),
Lines(LineReader),
Scripted(VecDeque<String>),
}
pub struct TextStream {
wire: Wire,
decode: Decoder,
usage: Usage,
stop_reason: Option<StopReason>,
finished: bool,
call: Option<Call>,
}
struct Call {
provider: &'static str,
model: String,
started: Instant,
}
impl TextStream {
pub fn events(body: Body, decode: Decoder) -> TextStream {
TextStream::wrap(Wire::Events(body.events()), decode)
}
pub fn lines(body: Body, decode: Decoder) -> TextStream {
TextStream::wrap(Wire::Lines(LineReader::new(body)), decode)
}
pub fn scripted(deltas: impl IntoIterator<Item = String>) -> TextStream {
TextStream::wrap(Wire::Scripted(deltas.into_iter().collect()), |_, _| {
Ok(StreamDelta::nothing())
})
}
fn wrap(wire: Wire, decode: Decoder) -> TextStream {
TextStream {
wire,
decode,
usage: Usage::default(),
stop_reason: None,
finished: false,
call: None,
}
}
pub fn measured(mut self, provider: &'static str, model: impl Into<String>) -> TextStream {
self.call = Some(Call { provider, model: model.into(), started: Instant::now() });
self
}
pub async fn next(&mut self) -> Result<Option<String>> {
loop {
if self.finished {
return Ok(None);
}
let frame = match &mut self.wire {
Wire::Scripted(deltas) => match deltas.pop_front() {
Some(text) => return Ok(Some(text)),
None => None,
},
Wire::Events(reader) => reader
.next()
.await?
.map(|event| (event.event.clone(), event.data.clone())),
Wire::Lines(reader) => reader.next_line().await?.map(|line| (None, line)),
};
let Some((name, data)) = frame else {
self.finish();
return Ok(None);
};
if data.trim().is_empty() {
continue;
}
let delta = (self.decode)(name.as_deref(), &data)?;
if let Some(usage) = delta.usage {
self.usage.input_tokens = self.usage.input_tokens.max(usage.input_tokens);
self.usage.output_tokens = self.usage.output_tokens.max(usage.output_tokens);
}
if let Some(reason) = delta.stop_reason {
self.stop_reason = Some(reason);
}
if delta.done {
self.finish();
return Ok(None);
}
match delta.text {
Some(text) if !text.is_empty() => return Ok(Some(text)),
_ => continue,
}
}
}
pub async fn collect(&mut self) -> Result<String> {
let mut out = String::new();
while let Some(delta) = self.next().await? {
out.push_str(&delta);
}
Ok(out)
}
pub fn usage(&self) -> Usage {
self.usage
}
pub fn stop_reason(&self) -> Option<&StopReason> {
self.stop_reason.as_ref()
}
fn finish(&mut self) {
self.finished = true;
if let Some(call) = self.call.take() {
record_call(call.provider, &call.model, self.usage, call.started.elapsed(), true);
}
}
}
struct LineReader {
body: Body,
pending: String,
drained: bool,
}
impl LineReader {
fn new(body: Body) -> LineReader {
LineReader { body, pending: String::new(), drained: false }
}
async fn next_line(&mut self) -> Result<Option<String>> {
loop {
if let Some(at) = self.pending.find('\n') {
let line: String = self.pending.drain(..at + 1).collect();
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
return Ok(Some(line));
}
if self.drained {
let rest = std::mem::take(&mut self.pending);
let rest = rest.trim().to_string();
return Ok(if rest.is_empty() { None } else { Some(rest) });
}
match self.body.chunk().await? {
Some(bytes) => self.pending.push_str(&String::from_utf8_lossy(&bytes)),
None => self.drained = true,
}
}
}
}
pub(crate) fn record_call(
provider: &str,
model: &str,
usage: Usage,
elapsed: Duration,
streamed: bool,
) {
if !rustlavel_core::events::has_subscribers() {
return;
}
Event::new("ai.call")
.with("provider", provider)
.with("model", model)
.with("input_tokens", usage.input_tokens)
.with("output_tokens", usage.output_tokens)
.with("streamed", streamed)
.took(elapsed)
.dispatch();
}
pub(crate) fn token_count(value: &Json, path: &str) -> u32 {
value.get(path).and_then(Json::as_i64).unwrap_or(0).max(0) as u32
}
#[cfg(test)]
mod tests {
use super::*;
use rustlavel_http::{Headers, Status};
fn body(text: &str) -> Body {
Body::from_bytes(Status::OK, Headers::new(), text.as_bytes().to_vec())
}
fn decode(_name: Option<&str>, data: &str) -> Result<StreamDelta> {
let value = Json::parse(data)?;
if value.get("done").and_then(Json::as_bool) == Some(true) {
return Ok(StreamDelta::done().with_usage(Usage::new(11, 22)));
}
Ok(StreamDelta::text(value.get("t").and_then(Json::as_str).unwrap_or("")))
}
#[tokio::test]
async fn a_scripted_stream_hands_back_its_deltas_in_order() {
let mut stream = TextStream::scripted(["Hel".to_string(), "lo".to_string()]);
assert_eq!(stream.next().await.unwrap().as_deref(), Some("Hel"));
assert_eq!(stream.next().await.unwrap().as_deref(), Some("lo"));
assert!(stream.next().await.unwrap().is_none());
}
#[tokio::test]
async fn an_event_stream_skips_bookkeeping_frames_and_keeps_the_usage() {
let source = "data: {\"t\":\"Hel\"}\n\ndata: {\"t\":\"\"}\n\ndata: {\"t\":\"lo\"}\n\n\
data: {\"done\":true}\n\n";
let mut stream = TextStream::events(body(source), decode);
assert_eq!(stream.collect().await.unwrap(), "Hello");
assert_eq!(stream.usage(), Usage::new(11, 22));
}
#[tokio::test]
async fn a_line_stream_reads_newline_delimited_json_including_an_unterminated_last_line() {
let source = "{\"t\":\"one \"}\n{\"t\":\"two\"}\n\n{\"done\":true}";
let mut stream = TextStream::lines(body(source), decode);
assert_eq!(stream.collect().await.unwrap(), "one two");
assert_eq!(stream.usage().total(), 33);
}
#[tokio::test]
async fn a_finished_stream_stays_finished() {
let mut stream = TextStream::scripted(["only".to_string()]);
assert!(stream.next().await.unwrap().is_some());
assert!(stream.next().await.unwrap().is_none());
assert!(stream.next().await.unwrap().is_none());
}
#[test]
fn token_counts_default_to_zero_rather_than_failing() {
let value = Json::object([("usage", Json::object([("input_tokens", 12.into())]))]);
assert_eq!(token_count(&value, "usage.input_tokens"), 12);
assert_eq!(token_count(&value, "usage.output_tokens"), 0);
}
}