use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use futures_util::stream::{BoxStream, StreamExt};
use rucora_core::error::ProviderError;
use rucora_core::provider::LlmProvider;
use rucora_core::provider::types::{
ChatMessage, ChatRequest, ChatResponse, ChatStreamChunk, FinishReason, Usage,
};
use rucora_core::tool::types::ToolDefinition;
#[derive(Debug, Default, Clone)]
pub struct CallRecord {
pub messages: Vec<ChatMessage>,
pub model: Option<String>,
pub tools: Option<Vec<ToolDefinition>>,
}
#[derive(Debug, Default, Clone)]
pub struct StreamCallRecord {
pub messages: Vec<ChatMessage>,
pub model: Option<String>,
pub tools: Option<Vec<ToolDefinition>>,
pub chunks: Vec<ChatStreamChunk>,
}
#[derive(Clone)]
pub struct MockProvider {
response: String,
error: Option<String>,
stream_chunks: Vec<String>,
records: Arc<Mutex<Vec<CallRecord>>>,
stream_records: Arc<Mutex<Vec<StreamCallRecord>>>,
}
impl MockProvider {
pub fn new(response: impl Into<String>) -> Self {
Self {
response: response.into(),
error: None,
stream_chunks: Vec::new(),
records: Arc::new(Mutex::new(Vec::new())),
stream_records: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn with_error(mut self, error: impl Into<String>) -> Self {
self.error = Some(error.into());
self
}
pub fn with_stream_chunks(mut self, chunks: Vec<impl Into<String>>) -> Self {
self.stream_chunks = chunks.into_iter().map(|c| c.into()).collect();
self
}
pub fn records(&self) -> Vec<CallRecord> {
self.records.lock().unwrap().clone()
}
pub fn stream_records(&self) -> Vec<StreamCallRecord> {
self.stream_records.lock().unwrap().clone()
}
pub fn clear_records(&self) {
self.records.lock().unwrap().clear();
self.stream_records.lock().unwrap().clear();
}
pub fn call_count(&self) -> usize {
self.records.lock().unwrap().len()
}
pub fn stream_call_count(&self) -> usize {
self.stream_records.lock().unwrap().len()
}
}
#[async_trait]
impl LlmProvider for MockProvider {
async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
self.records.lock().unwrap().push(CallRecord {
messages: request.messages.clone(),
model: request.model.clone(),
tools: request.tools,
});
if let Some(err) = &self.error {
return Err(ProviderError::Message(err.clone()));
}
Ok(ChatResponse {
message: ChatMessage::assistant(self.response.clone()),
usage: None,
finish_reason: Some(FinishReason::Stop),
})
}
fn stream_chat(
&self,
request: ChatRequest,
) -> Result<BoxStream<'static, Result<ChatStreamChunk, ProviderError>>, ProviderError> {
let record = StreamCallRecord {
messages: request.messages.clone(),
model: request.model.clone(),
tools: request.tools,
chunks: Vec::new(),
};
self.stream_records.lock().unwrap().push(record);
if let Some(err) = &self.error {
return Err(ProviderError::Message(err.clone()));
}
let chunks = self.stream_chunks.clone();
let usage = Usage {
prompt_tokens: 10,
completion_tokens: chunks.iter().map(|c| c.len() as u32).sum(),
total_tokens: 0,
};
let stream = futures_util::stream::iter(chunks.into_iter().map(move |chunk| {
Ok(ChatStreamChunk {
delta: Some(chunk),
tool_calls: vec![],
usage: None,
finish_reason: None,
})
}))
.chain(futures_util::stream::once(async move {
Ok(ChatStreamChunk {
delta: None,
tool_calls: vec![],
usage: Some(usage),
finish_reason: Some(FinishReason::Stop),
})
}));
Ok(Box::pin(stream))
}
}