use std::sync::Mutex;
use async_trait::async_trait;
use futures::stream::{self, BoxStream, StreamExt};
use crate::audio::{
AudioResponse, SpeechToTextRequest, TextToSpeechRequest, TranscriptionResponse,
};
use crate::embeddings::{EmbeddingsRequest, EmbeddingsResponse};
use crate::error::Error;
use crate::images::{ImagesRequest, ImagesResponse};
use crate::moderation::{ModerationRequest, ModerationResponse};
use crate::provider::Provider;
use crate::rerank::{RerankRequest, RerankResponse};
use crate::stream_event::StreamEvent;
use crate::structured::{StructuredRequest, StructuredResponse, StructuredStreamEvent};
use crate::text::{Step, TextRequest};
const DEFAULT_STREAM_CHUNK_WORDS: usize = 1;
pub struct FakeProvider {
name: String,
text: ResponseQueue<TextRequest, Step>,
structured: ResponseQueue<StructuredRequest, StructuredResponse>,
moderation: ResponseQueue<ModerationRequest, ModerationResponse>,
embeddings: ResponseQueue<EmbeddingsRequest, EmbeddingsResponse>,
rerank: ResponseQueue<RerankRequest, RerankResponse>,
images: ResponseQueue<ImagesRequest, ImagesResponse>,
text_to_speech: ResponseQueue<TextToSpeechRequest, AudioResponse>,
speech_to_text: ResponseQueue<SpeechToTextRequest, TranscriptionResponse>,
stream_chunk_words: usize,
}
impl FakeProvider {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
text: ResponseQueue::new(),
structured: ResponseQueue::new(),
moderation: ResponseQueue::new(),
embeddings: ResponseQueue::new(),
rerank: ResponseQueue::new(),
images: ResponseQueue::new(),
text_to_speech: ResponseQueue::new(),
speech_to_text: ResponseQueue::new(),
stream_chunk_words: DEFAULT_STREAM_CHUNK_WORDS,
}
}
pub fn respond_with(self, step: impl Into<Step>) -> Self {
self.text.push(step.into());
self
}
pub fn respond_with_structured(self, response: impl Into<StructuredResponse>) -> Self {
self.structured.push(response.into());
self
}
pub fn respond_with_moderation(self, response: impl Into<ModerationResponse>) -> Self {
self.moderation.push(response.into());
self
}
pub fn respond_with_embeddings(self, response: impl Into<EmbeddingsResponse>) -> Self {
self.embeddings.push(response.into());
self
}
pub fn respond_with_rerank(self, response: impl Into<RerankResponse>) -> Self {
self.rerank.push(response.into());
self
}
pub fn respond_with_images(self, response: impl Into<ImagesResponse>) -> Self {
self.images.push(response.into());
self
}
pub fn respond_with_audio(self, response: impl Into<AudioResponse>) -> Self {
self.text_to_speech.push(response.into());
self
}
pub fn respond_with_transcription(self, response: impl Into<TranscriptionResponse>) -> Self {
self.speech_to_text.push(response.into());
self
}
pub fn with_stream_chunk_words(mut self, words: usize) -> Self {
self.stream_chunk_words = words.max(1);
self
}
pub fn recorded_requests(&self) -> Vec<TextRequest> {
self.text.recorded()
}
pub fn recorded_structured_requests(&self) -> Vec<StructuredRequest> {
self.structured.recorded()
}
pub fn recorded_moderation_requests(&self) -> Vec<ModerationRequest> {
self.moderation.recorded()
}
pub fn recorded_embeddings_requests(&self) -> Vec<EmbeddingsRequest> {
self.embeddings.recorded()
}
pub fn recorded_rerank_requests(&self) -> Vec<RerankRequest> {
self.rerank.recorded()
}
pub fn recorded_images_requests(&self) -> Vec<ImagesRequest> {
self.images.recorded()
}
pub fn recorded_text_to_speech_requests(&self) -> Vec<TextToSpeechRequest> {
self.text_to_speech.recorded()
}
pub fn recorded_speech_to_text_requests(&self) -> Vec<SpeechToTextRequest> {
self.speech_to_text.recorded()
}
}
#[async_trait]
impl Provider for FakeProvider {
fn name(&self) -> &str {
&self.name
}
async fn text_step(&self, request: &TextRequest) -> Result<Step, Error> {
Ok(self
.text
.next(request.clone(), &self.name, "text", "respond_with"))
}
async fn stream_text_once(
&self,
request: &TextRequest,
) -> Result<BoxStream<'static, Result<StreamEvent, Error>>, Error> {
let step = self
.text
.next(request.clone(), &self.name, "text", "respond_with");
let mut events = vec![StreamEvent::StreamStart {
meta: step.meta.clone(),
}];
if let Some(text) = &step.text {
for chunk in chunk_words(text, self.stream_chunk_words) {
events.push(StreamEvent::TextDelta { text: chunk });
}
}
for call in &step.tool_calls {
events.push(StreamEvent::ToolCall(call.clone()));
}
events.push(StreamEvent::StepFinish {
usage: step.usage,
finish_reason: step.finish_reason,
});
Ok(stream::iter(events.into_iter().map(Ok)).boxed())
}
async fn structured(&self, request: StructuredRequest) -> Result<StructuredResponse, Error> {
Ok(self
.structured
.next(request, &self.name, "structured", "respond_with_structured"))
}
async fn stream_structured_once(
&self,
request: &StructuredRequest,
) -> Result<BoxStream<'static, Result<StructuredStreamEvent, Error>>, Error> {
let response = self.structured.next(
request.clone(),
&self.name,
"structured",
"respond_with_structured",
);
let events = vec![
Ok(StructuredStreamEvent::PartialObject {
data: response.data.clone(),
}),
Ok(StructuredStreamEvent::End { response }),
];
Ok(stream::iter(events).boxed())
}
async fn moderation(&self, request: ModerationRequest) -> Result<ModerationResponse, Error> {
Ok(self
.moderation
.next(request, &self.name, "moderation", "respond_with_moderation"))
}
async fn embeddings(&self, request: EmbeddingsRequest) -> Result<EmbeddingsResponse, Error> {
Ok(self
.embeddings
.next(request, &self.name, "embeddings", "respond_with_embeddings"))
}
async fn rerank(&self, request: RerankRequest) -> Result<RerankResponse, Error> {
Ok(self
.rerank
.next(request, &self.name, "rerank", "respond_with_rerank"))
}
async fn images(&self, request: ImagesRequest) -> Result<ImagesResponse, Error> {
Ok(self
.images
.next(request, &self.name, "images", "respond_with_images"))
}
async fn text_to_speech(&self, request: TextToSpeechRequest) -> Result<AudioResponse, Error> {
Ok(self
.text_to_speech
.next(request, &self.name, "text_to_speech", "respond_with_audio"))
}
async fn speech_to_text(
&self,
request: SpeechToTextRequest,
) -> Result<TranscriptionResponse, Error> {
Ok(self.speech_to_text.next(
request,
&self.name,
"speech_to_text",
"respond_with_transcription",
))
}
}
struct ResponseQueue<Req, Resp> {
responses: Mutex<Vec<Resp>>,
recorded: Mutex<Vec<Req>>,
}
impl<Req: Clone, Resp> ResponseQueue<Req, Resp> {
fn new() -> Self {
Self {
responses: Mutex::new(Vec::new()),
recorded: Mutex::new(Vec::new()),
}
}
fn push(&self, response: Resp) {
self.responses
.lock()
.expect("fake provider mutex poisoned")
.push(response);
}
fn recorded(&self) -> Vec<Req> {
self.recorded
.lock()
.expect("fake provider mutex poisoned")
.clone()
}
fn next(&self, request: Req, provider_name: &str, capability: &str, builder: &str) -> Resp {
self.recorded
.lock()
.expect("fake provider mutex poisoned")
.push(request);
let mut responses = self.responses.lock().expect("fake provider mutex poisoned");
if responses.is_empty() {
panic!(
"FakeProvider '{provider_name}' received a {capability} request but has no \
more canned {capability} responses queued -- call .{builder}(...) for every \
expected call",
);
}
responses.remove(0)
}
}
fn chunk_words(text: &str, words_per_chunk: usize) -> Vec<String> {
let mut chunks = Vec::new();
let mut current = String::new();
let mut count = 0;
for word in text.split_inclusive(' ') {
current.push_str(word);
count += 1;
if count >= words_per_chunk {
chunks.push(std::mem::take(&mut current));
count = 0;
}
}
if !current.is_empty() {
chunks.push(current);
}
chunks
}