use std::borrow::Cow;
use futures_core::Stream;
use super::check::check_vectors;
use crate::{
ChunkedEmbeddingProgress, EmbeddingProvider, EmbeddingRequest, EmbeddingResponse, Error,
ProviderIdentity, Result, Subject,
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EmbeddingChunk {
pub offset: usize,
pub response: EmbeddingResponse,
}
impl EmbeddingChunk {
#[must_use]
pub fn end(&self) -> usize {
self.offset + self.response.embeddings.len()
}
}
pub(crate) fn chunks<'a, P>(
provider: &'a P,
request: &'a EmbeddingRequest,
max_chunk_inputs: usize,
) -> impl Stream<Item = Result<EmbeddingChunk>> + Send + 'a
where
P: EmbeddingProvider + Sync + ?Sized,
{
let chunk_size = chunk_size(provider, request, max_chunk_inputs);
let state = State {
offset: 0,
size: None,
progress: ChunkedEmbeddingProgress::default(),
done: false,
};
futures_util::stream::unfold(state, move |mut state| async move {
let total = request.inputs.len();
if state.done || (state.offset > 0 && state.offset >= total) {
return None;
}
let offset = state.offset;
let end = total.min(offset + chunk_size);
let part = if offset == 0 && end == total {
Cow::Borrowed(request)
} else {
Cow::Owned(slice(request, offset, end))
};
let result = match provider.embed(&part).await {
Ok(response) => check_chunk(
provider,
&part,
response,
&mut state.size,
&mut state.progress,
),
Err(err) => Err(state.progress.attach_to(err)),
};
let item = result.map(|response| {
state.offset = end;
chunk_at(offset, response)
});
state.done = item.is_err() || end >= total;
Some((item, state))
})
}
fn chunk_size<P: EmbeddingProvider + ?Sized>(
provider: &P,
request: &EmbeddingRequest,
max_chunk_inputs: usize,
) -> usize {
provider
.embedding_limits(&request.model)
.max_inputs_per_call
.map_or(max_chunk_inputs, |max| max.min(max_chunk_inputs))
.max(1)
}
fn chunk_at(offset: usize, mut response: EmbeddingResponse) -> EmbeddingChunk {
for warning in &mut response.warnings {
if let Subject::EmbeddingInput { input, .. } = &mut warning.subject {
*input += offset;
}
}
EmbeddingChunk { offset, response }
}
struct State {
offset: usize,
size: Option<usize>,
progress: ChunkedEmbeddingProgress,
done: bool,
}
fn slice(request: &EmbeddingRequest, start: usize, end: usize) -> EmbeddingRequest {
EmbeddingRequest {
model: request.model.clone(),
inputs: request.inputs[start..end].to_vec(),
dimensions: request.dimensions,
input_type: request.input_type,
unsupported_policy: request.unsupported_policy,
options: request.options.clone(),
}
}
fn check_chunk<P: ProviderIdentity + ?Sized>(
provider: &P,
part: &EmbeddingRequest,
response: EmbeddingResponse,
size: &mut Option<usize>,
progress: &mut ChunkedEmbeddingProgress,
) -> Result<EmbeddingResponse> {
progress.add_usage(response.usage.as_ref());
let inputs = part.inputs.len();
let fail = |err: Error, progress: &ChunkedEmbeddingProgress| {
progress.attach_to(err.with_provider(provider.provider_name().to_owned()))
};
if response.embeddings.len() != inputs {
let err = Error::unexpected_response(format!(
"the response holds {} embeddings for {inputs} inputs (inputs {}..{}) of a chunked \
embedding request",
response.embeddings.len(),
progress.completed_inputs,
progress.completed_inputs + inputs,
))
.authored();
return Err(fail(err, progress));
}
*size = check_vectors(
&part.model,
part.dimensions,
*size,
progress.completed_inputs,
&response.embeddings,
)
.map_err(|err| fail(err, progress))?;
progress.completed_inputs += inputs;
Ok(response)
}
#[cfg(test)]
mod tests {
use futures_util::StreamExt;
use super::*;
use crate::mock::MockEmbeddingProvider;
use crate::{EmbeddingProviderExt, Usage, Warning, WarningKind};
fn answer(vectors: &[f32], tokens: u64) -> Result<EmbeddingResponse> {
Ok(
EmbeddingResponse::new(vectors.iter().map(|&v| vec![v, v]).collect())
.usage(Usage::new().input_tokens(tokens)),
)
}
async fn collect(
provider: &MockEmbeddingProvider,
request: &EmbeddingRequest,
max: usize,
) -> Vec<Result<EmbeddingChunk>> {
provider.embed_chunks(request, max).collect().await
}
#[tokio::test]
async fn a_failure_mid_way_can_be_resumed() {
let request = EmbeddingRequest::new("m").inputs(["a", "b", "c", "d", "e"]);
let provider = MockEmbeddingProvider::new([
answer(&[1.0, 2.0], 3),
Err(Error::rate_limited("slow down")),
answer(&[3.0, 4.0], 4),
]);
let chunks = collect(&provider, &request, 2).await;
assert_eq!(chunks.len(), 2, "the stream ends after the error");
let first = chunks[0].as_ref().unwrap();
assert_eq!((first.offset, first.end()), (0, 2));
let err = chunks[1].as_ref().unwrap_err();
assert!(matches!(err, Error::RateLimited { .. }), "{err}");
let progress = ChunkedEmbeddingProgress::from_error(err).unwrap();
assert_eq!(progress.completed_inputs, 2);
assert_eq!(progress.usage.unwrap().input_tokens, Some(3));
assert_eq!(provider.requests().len(), 2);
let mut rest = request.clone();
rest.inputs.drain(..progress.completed_inputs);
provider.push_response(answer(&[5.0], 1));
let resumed = collect(&provider, &rest, 2).await;
let resumed: Vec<_> = resumed.into_iter().map(Result::unwrap).collect();
assert_eq!(resumed.iter().map(|c| c.offset).collect::<Vec<_>>(), [0, 2]);
let sent: Vec<_> = provider.requests()[2..]
.iter()
.map(|r| r.inputs.clone())
.collect();
assert_eq!(sent, [rest.inputs[..2].to_vec(), rest.inputs[2..].to_vec()]);
}
#[tokio::test]
async fn chunks_carry_their_offset_and_the_request_fields() {
let hint = Warning::unsupported(crate::RequestField::InputType, "no input type")
.with_provider("x");
let image = |input| {
Warning::new(
WarningKind::Approximated,
Subject::EmbeddingInput {
input,
part: Some(0),
},
"image resized",
)
};
let provider = MockEmbeddingProvider::new([
answer(&[1.0, 2.0], 1).map(|r| r.warning(hint.clone())),
answer(&[3.0], 1).map(|r| r.warning(hint.clone()).warning(image(0))),
])
.with_max_inputs_per_call(2);
let request = EmbeddingRequest::new("m")
.inputs(["a", "b", "c"])
.dimensions(2)
.input_type(crate::EmbeddingInputType::Query);
let chunks: Vec<_> = collect(&provider, &request, 10)
.await
.into_iter()
.map(Result::unwrap)
.collect();
assert_eq!(chunks.len(), 2);
assert_eq!((chunks[1].offset, chunks[1].end()), (2, 3));
assert_eq!(chunks[1].response.warnings, vec![hint, image(2)]);
for sent in provider.requests() {
assert_eq!(sent.dimensions, Some(2));
assert_eq!(sent.input_type, Some(crate::EmbeddingInputType::Query));
}
}
#[tokio::test]
async fn a_chunk_of_another_size_than_the_first_ends_the_stream() {
let provider = MockEmbeddingProvider::new([
answer(&[1.0, 2.0], 2),
Ok(EmbeddingResponse::new(vec![vec![0.1, 0.2, 0.3]; 2])),
]);
let request = EmbeddingRequest::new("m").inputs(["a", "b", "c", "d"]);
let chunks = collect(&provider, &request, 2).await;
assert!(chunks[0].is_ok());
let err = chunks[1].as_ref().unwrap_err();
assert!(matches!(err, Error::UnexpectedResponse { .. }), "{err}");
assert_eq!(
ChunkedEmbeddingProgress::from_error(err)
.unwrap()
.completed_inputs,
2
);
}
#[tokio::test]
async fn a_request_without_inputs_is_sent_once() {
let provider =
MockEmbeddingProvider::new([Err(Error::invalid_request("no inputs").authored())]);
let chunks = collect(&provider, &EmbeddingRequest::new("m"), 2).await;
assert_eq!(chunks.len(), 1);
assert!(matches!(chunks[0], Err(Error::InvalidRequest { .. })));
}
#[tokio::test]
async fn a_single_chunk_is_the_request_itself() {
let provider = MockEmbeddingProvider::new([answer(&[1.0, 2.0], 1)]);
let request = EmbeddingRequest::new("m").inputs(["a", "b"]);
let chunks = collect(&provider, &request, 2).await;
assert_eq!(chunks.len(), 1);
assert_eq!(provider.requests(), [request]);
}
}