use std::pin::Pin;
use std::sync::Arc;
use async_trait::async_trait;
use futures::Stream as FutStream;
use rskit_ai::chat::{Message, count_tokens_approx};
use rskit_ai::{
Capabilities, FinishReason, MessageStart, MessageStop, Role, StreamEventRef, TextDelta,
UsageDelta, text_of,
};
use rskit_errors::{AppError, AppResult};
use crate::types::{CompletionRequest, CompletionResponse};
#[async_trait]
pub trait Provider: rskit_provider::RequestResponse<CompletionRequest, CompletionResponse> {
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse, AppError>;
async fn stream(
&self,
request: CompletionRequest,
) -> Result<Pin<Box<dyn FutStream<Item = StreamEventRef> + Send>>, AppError> {
let resp = self.complete(request).await?;
let text = text_of(&resp.message.content);
let model = resp.model.clone();
let usage = resp.usage;
let finish_reason = resp.stop_reason.unwrap_or(FinishReason::Stop);
let mut events: Vec<StreamEventRef> = Vec::with_capacity(4);
events.push(Arc::new(MessageStart {
role: Role::Assistant,
model,
request_id: None,
}));
if !text.is_empty() {
events.push(Arc::new(TextDelta { text }));
}
events.push(Arc::new(UsageDelta { usage }));
events.push(Arc::new(MessageStop { finish_reason }));
Ok(Box::pin(futures::stream::iter(events)))
}
fn capabilities(&self) -> Capabilities {
Capabilities::default()
}
fn count_tokens(&self, messages: &[Message]) -> usize {
count_tokens_approx(messages)
}
}
pub struct LlmRequestResponse<P: Provider>(pub Arc<P>);
#[async_trait]
impl<P: Provider + 'static> rskit_provider::Provider for LlmRequestResponse<P> {
fn name(&self) -> &'static str {
self.0.name()
}
}
#[async_trait]
impl<P: Provider + 'static> rskit_provider::RequestResponse<CompletionRequest, CompletionResponse>
for LlmRequestResponse<P>
{
async fn execute(&self, input: CompletionRequest) -> AppResult<CompletionResponse> {
self.0.complete(input).await
}
}
type ProviderBoxStream<O> = Pin<Box<dyn FutStream<Item = AppResult<O>> + Send + 'static>>;
pub struct LlmStream<P: Provider>(pub Arc<P>);
#[async_trait]
impl<P: Provider + 'static> rskit_provider::Provider for LlmStream<P> {
fn name(&self) -> &'static str {
self.0.name()
}
}
impl<P: Provider + 'static> rskit_provider::Stream<CompletionRequest, StreamEventRef>
for LlmStream<P>
{
async fn execute(
&self,
input: CompletionRequest,
) -> AppResult<ProviderBoxStream<StreamEventRef>> {
use futures::StreamExt;
let raw = Provider::stream(&*self.0, input).await?;
Ok(Box::pin(raw.map(Ok)) as ProviderBoxStream<StreamEventRef>)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{self as llm, types};
use futures::StreamExt;
use rskit_provider::RequestResponse;
#[test]
fn test_capabilities_default() {
let cap = Capabilities::default();
assert!(!cap.tool_use);
assert!(!cap.vision);
assert!(!cap.reasoning_tokens);
assert!(!cap.streaming);
assert_eq!(cap.max_input_tokens.unwrap_or_default(), 0);
assert!(cap.max_output_tokens.is_none());
}
#[test]
fn test_count_tokens_approx_user() {
let msgs = vec![types::user("hello world")];
assert!(count_tokens_approx(&msgs) > 0);
}
#[test]
fn test_count_tokens_approx_empty() {
let msgs: Vec<Message> = vec![];
assert_eq!(count_tokens_approx(&msgs), 0);
}
struct MockProvider;
#[async_trait]
impl rskit_provider::Provider for MockProvider {
fn name(&self) -> &'static str {
"mock"
}
}
#[async_trait]
impl rskit_provider::RequestResponse<CompletionRequest, CompletionResponse> for MockProvider {
async fn execute(&self, input: CompletionRequest) -> AppResult<CompletionResponse> {
self.complete(input).await
}
}
#[async_trait]
impl Provider for MockProvider {
async fn complete(
&self,
_request: CompletionRequest,
) -> Result<CompletionResponse, AppError> {
Ok(CompletionResponse {
message: llm::AssistantMessage {
content: llm::text_content("Hi"),
tool_calls: vec![],
usage: None,
},
model: "mock".to_string(),
usage: rskit_ai::Usage {
input_tokens: 1,
output_tokens: 1,
cached_tokens: 0,
reasoning_tokens: 0,
},
stop_reason: Some(FinishReason::Stop),
})
}
}
#[tokio::test]
async fn test_mock_provider_complete() {
let provider = MockProvider;
let request = CompletionRequest {
model: "mock".to_string(),
messages: vec![types::user("hi")],
max_tokens: None,
temperature: None,
stream: false,
tools: None,
tool_choice: None,
};
let resp = provider.complete(request).await.unwrap();
assert_eq!(resp.model, "mock");
}
#[tokio::test]
async fn test_default_stream_synthesizes_from_complete() {
let provider = MockProvider;
let request = CompletionRequest {
model: "mock".to_string(),
messages: vec![types::user("hi")],
max_tokens: None,
temperature: None,
stream: true,
tools: None,
tool_choice: None,
};
let mut stream = provider.stream(request).await.unwrap();
let mut event_types = vec![];
while let Some(event) = stream.next().await {
event_types.push(event.event_type());
}
assert_eq!(
event_types,
vec!["message.start", "text.delta", "usage.delta", "message.stop"]
);
}
#[tokio::test]
async fn test_default_count_tokens_uses_approx() {
let provider = MockProvider;
let msgs = vec![types::user("hello world")];
assert_eq!(provider.count_tokens(&msgs), count_tokens_approx(&msgs));
}
#[tokio::test]
async fn test_llm_request_response_adapter() {
let provider = Arc::new(MockProvider);
let adapter = LlmRequestResponse(provider);
let request = CompletionRequest {
model: "mock".to_string(),
messages: vec![types::user("hi")],
max_tokens: None,
temperature: None,
stream: false,
tools: None,
tool_choice: None,
};
let resp = adapter.execute(request).await.unwrap();
assert_eq!(resp.model, "mock");
}
}