use crabllm_core::{
BoxStream, ChatCompletionChunk, ChatCompletionRequest, ChatCompletionResponse, Choice,
ChunkChoice, Delta, Error, FinishReason, FunctionCallDelta, Message, Provider, Role, ToolCall,
ToolCallDelta, ToolType,
};
use parking_lot::Mutex;
use serde_json::{Map, Value};
use std::{collections::VecDeque, sync::Arc};
#[derive(Clone, Default, Debug)]
pub struct TestProvider {
responses: Arc<Mutex<VecDeque<ChatCompletionResponse>>>,
chunks: Arc<Mutex<VecDeque<Vec<ChatCompletionChunk>>>>,
}
impl TestProvider {
pub fn new(responses: Vec<ChatCompletionResponse>) -> Self {
Self {
responses: Arc::new(Mutex::new(responses.into())),
chunks: Arc::new(Mutex::new(VecDeque::new())),
}
}
pub fn with_chunks(chunks: Vec<Vec<ChatCompletionChunk>>) -> Self {
Self {
responses: Arc::new(Mutex::new(VecDeque::new())),
chunks: Arc::new(Mutex::new(chunks.into())),
}
}
pub fn with_both(
responses: Vec<ChatCompletionResponse>,
chunks: Vec<Vec<ChatCompletionChunk>>,
) -> Self {
Self {
responses: Arc::new(Mutex::new(responses.into())),
chunks: Arc::new(Mutex::new(chunks.into())),
}
}
}
impl Provider for TestProvider {
async fn chat_completion(
&self,
_request: &ChatCompletionRequest,
) -> Result<ChatCompletionResponse, Error> {
let mut responses = self.responses.lock();
responses.pop_front().ok_or_else(|| {
Error::Internal("TestProvider: no more scripted responses for chat_completion".into())
})
}
async fn chat_completion_stream(
&self,
_request: &ChatCompletionRequest,
) -> Result<BoxStream<'static, Result<ChatCompletionChunk, Error>>, Error> {
let batch = {
let mut all = self.chunks.lock();
all.pop_front()
};
match batch {
Some(chunks) => {
let stream = async_stream::stream! {
for chunk in chunks {
yield Ok(chunk);
}
};
Ok(Box::pin(stream))
}
None => Err(Error::Internal(
"TestProvider: no more scripted chunks for chat_completion_stream".into(),
)),
}
}
}
pub fn text_response(content: &str) -> ChatCompletionResponse {
ChatCompletionResponse {
choices: vec![Choice {
index: 0,
message: Message::assistant(content),
finish_reason: Some(FinishReason::Stop),
logprobs: None,
}],
..Default::default()
}
}
pub fn tool_response(calls: Vec<ToolCall>) -> ChatCompletionResponse {
ChatCompletionResponse {
choices: vec![Choice {
index: 0,
message: Message {
role: Role::Assistant,
content: Some(Value::Null),
tool_calls: Some(calls),
tool_call_id: None,
name: None,
reasoning_content: None,
extra: Map::new(),
},
finish_reason: Some(FinishReason::ToolCalls),
logprobs: None,
}],
..Default::default()
}
}
pub fn text_chunk(content: &str) -> ChatCompletionChunk {
ChatCompletionChunk {
choices: vec![ChunkChoice {
delta: Delta {
content: Some(content.into()),
..Default::default()
},
..Default::default()
}],
..Default::default()
}
}
pub fn thinking_chunk(content: &str) -> ChatCompletionChunk {
ChatCompletionChunk {
choices: vec![ChunkChoice {
delta: Delta {
reasoning_content: Some(content.into()),
..Default::default()
},
..Default::default()
}],
..Default::default()
}
}
pub fn mixed_chunk(content: &str, reasoning: &str) -> ChatCompletionChunk {
ChatCompletionChunk {
choices: vec![ChunkChoice {
delta: Delta {
content: Some(content.into()),
reasoning_content: Some(reasoning.into()),
..Default::default()
},
..Default::default()
}],
..Default::default()
}
}
pub fn finish_chunk(reason: FinishReason) -> ChatCompletionChunk {
ChatCompletionChunk {
choices: vec![ChunkChoice {
finish_reason: Some(reason),
..Default::default()
}],
..Default::default()
}
}
pub fn tool_call_delta(tc: &ToolCall) -> ToolCallDelta {
ToolCallDelta {
index: tc.index.unwrap_or(0),
id: Some(tc.id.clone()),
kind: Some(ToolType::Function),
function: Some(FunctionCallDelta {
name: Some(tc.function.name.clone()),
arguments: Some(tc.function.arguments.clone()),
}),
}
}
pub fn tool_chunks(calls: Vec<ToolCall>) -> Vec<ChatCompletionChunk> {
let deltas: Vec<ToolCallDelta> = calls.iter().map(tool_call_delta).collect();
vec![
ChatCompletionChunk {
choices: vec![ChunkChoice {
delta: Delta {
tool_calls: Some(deltas),
..Default::default()
},
..Default::default()
}],
..Default::default()
},
finish_chunk(FinishReason::ToolCalls),
]
}
pub fn text_chunks(text: &str) -> Vec<ChatCompletionChunk> {
let mut chunks: Vec<ChatCompletionChunk> =
text.chars().map(|c| text_chunk(&c.to_string())).collect();
chunks.push(finish_chunk(FinishReason::Stop));
chunks
}