pub mod chunk;
pub mod erased;
pub mod error;
pub mod request;
pub mod sse;
pub mod turn;
use async_trait::async_trait;
use futures::stream::BoxStream;
pub use chunk::{Chunk, StopReason, Usage};
pub use erased::{BoxError, DynProvider, ErasedProvider, into_dyn};
pub use error::{LlmError, LlmErrorKind, kind_from_http_status};
pub use request::{
CompletionRequest, Content, ImageRef, JsonSchema, Message, Role, ToolCall, ToolChoice,
ToolResult, ToolSpec, humanize_tool_name,
};
#[async_trait]
pub trait LlmProvider: Send + Sync + 'static {
type Error: LlmError;
async fn complete(
&self,
req: CompletionRequest,
) -> Result<BoxStream<'static, Result<Chunk, Self::Error>>, Self::Error>;
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use futures::{StreamExt, stream};
use super::{Chunk, CompletionRequest, LlmProvider, StopReason, Usage, error::DummyError};
struct EchoProvider;
#[async_trait::async_trait]
impl LlmProvider for EchoProvider {
type Error = DummyError;
async fn complete(
&self,
req: CompletionRequest,
) -> Result<futures::stream::BoxStream<'static, Result<Chunk, Self::Error>>, Self::Error>
{
if req.messages.is_empty() {
return Err(DummyError::Other("no messages".to_owned()));
}
let chunks = vec![
Ok(Chunk::text_delta("echo")),
Ok(Chunk::Usage(Usage {
input_tokens: 3,
output_tokens: 1,
})),
Ok(Chunk::Stop(StopReason::EndTurn)),
];
Ok(stream::iter(chunks).boxed())
}
}
#[tokio::test]
async fn provider_streams_chunks_to_completion() {
let provider = EchoProvider;
let mut req = CompletionRequest::new("test-model");
req.messages.push(super::Message::user("hi"));
let stream = provider.complete(req).await.expect("stream opens");
let collected: Vec<Chunk> = stream.map(Result::unwrap).collect().await;
assert_eq!(collected.len(), 3);
assert_eq!(collected[0], Chunk::text_delta("echo"));
assert!(matches!(collected[2], Chunk::Stop(StopReason::EndTurn)));
}
#[tokio::test]
async fn provider_reports_pre_stream_failure() {
let provider = EchoProvider;
let req = CompletionRequest::new("test-model");
match provider.complete(req).await {
Err(DummyError::Other(_)) => {}
Err(other) => panic!("wrong error: {other}"),
Ok(_) => panic!("expected pre-stream rejection"),
}
}
#[tokio::test]
async fn usable_as_trait_object() {
let provider: Box<dyn LlmProvider<Error = DummyError>> = Box::new(EchoProvider);
let mut req = CompletionRequest::new("m");
req.messages.push(super::Message::user("yo"));
let stream = provider.complete(req).await.expect("stream opens");
let n = stream.count().await;
assert_eq!(n, 3);
}
}