Skip to main content

rig_core/embeddings/
embedding.rs

1//! The module defines the [EmbeddingModel] trait, which represents an embedding model that can
2//! generate embeddings for documents.
3//!
4//! The module also defines the [Embedding] struct, which represents a single document embedding.
5//!
6//! Finally, the module defines the [EmbeddingError] enum, which represents various errors that
7//! can occur during embedding generation or processing.
8
9use crate::{
10    completion::Usage,
11    http_client, provider_response,
12    wasm_compat::{WasmCompatSend, WasmCompatSync},
13};
14use serde::{Deserialize, Serialize};
15
16/// Errors returned by embedding models.
17///
18/// Inspect provider failures with [`Self::provider_response_body`],
19/// [`Self::provider_response_json`], and [`Self::provider_response_status`].
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum EmbeddingError {
23    /// Http error (e.g.: connection error, timeout, etc.)
24    #[error("HttpError: {0}")]
25    HttpError(#[from] http_client::Error),
26
27    /// Json error (e.g.: serialization, deserialization)
28    #[error("JsonError: {0}")]
29    JsonError(#[from] serde_json::Error),
30
31    /// URL construction or parsing failed while preparing a provider request.
32    #[error("UrlError: {0}")]
33    UrlError(#[from] url::ParseError),
34
35    #[cfg(not(target_family = "wasm"))]
36    /// Error processing the document for embedding
37    #[error("DocumentError: {0}")]
38    DocumentError(Box<dyn std::error::Error + Send + Sync + 'static>),
39
40    #[cfg(target_family = "wasm")]
41    /// Error processing the document for embedding
42    #[error("DocumentError: {0}")]
43    DocumentError(Box<dyn std::error::Error + 'static>),
44
45    /// Error parsing the completion response
46    #[error("ResponseError: {0}")]
47    ResponseError(String),
48
49    /// Error returned by the embedding model provider
50    #[error("ProviderError: {0}")]
51    ProviderError(String),
52
53    /// Raw error response preserved from the embedding model provider
54    #[error("ProviderResponseError: {0}")]
55    ProviderResponse(provider_response::ProviderResponseError),
56}
57
58crate::provider_response::impl_provider_response_helpers!(EmbeddingError);
59
60/// Trait for embedding models that can generate embeddings for documents.
61pub trait EmbeddingModel: WasmCompatSend + WasmCompatSync {
62    /// The maximum number of documents that can be embedded in a single request.
63    const MAX_DOCUMENTS: usize;
64
65    /// Provider client type used to construct this embedding model.
66    type Client;
67
68    /// Construct a model handle from a provider client, model identifier, and optional dimensions.
69    fn make(client: &Self::Client, model: impl Into<String>, dims: Option<usize>) -> Self;
70
71    /// The number of dimensions in the embedding vector.
72    fn ndims(&self) -> usize;
73
74    /// Embed multiple text documents in a single request
75    fn embed_texts(
76        &self,
77        texts: impl IntoIterator<Item = String> + WasmCompatSend,
78    ) -> impl std::future::Future<Output = Result<Vec<Embedding>, EmbeddingError>> + WasmCompatSend;
79
80    /// Embed a single text document.
81    fn embed_text(
82        &self,
83        text: &str,
84    ) -> impl std::future::Future<Output = Result<Embedding, EmbeddingError>> + WasmCompatSend {
85        async {
86            let mut embeddings = self.embed_texts(vec![text.to_string()]).await?;
87            embeddings.pop().ok_or_else(|| {
88                EmbeddingError::ResponseError(
89                    "embedding provider returned an empty response for embed_text".to_string(),
90                )
91            })
92        }
93    }
94
95    /// Embed multiple text documents in a single request and return token usage.
96    ///
97    /// The default implementation delegates to [`EmbeddingModel::embed_texts`] and returns
98    /// zero-valued usage. Providers that expose usage information from their embedding API
99    /// should override this method.
100    fn embed_texts_with_usage(
101        &self,
102        texts: impl IntoIterator<Item = String> + WasmCompatSend,
103    ) -> impl std::future::Future<Output = Result<EmbeddingResponse, EmbeddingError>> + WasmCompatSend
104    {
105        async {
106            let embeddings = self.embed_texts(texts).await?;
107            Ok(EmbeddingResponse {
108                embeddings,
109                usage: Usage::default(),
110            })
111        }
112    }
113
114    /// Embed a single text document and return token usage.
115    ///
116    /// The default implementation delegates to
117    /// [`EmbeddingModel::embed_texts_with_usage`].
118    fn embed_text_with_usage(
119        &self,
120        text: &str,
121    ) -> impl std::future::Future<Output = Result<EmbeddingResponse, EmbeddingError>> + WasmCompatSend
122    {
123        async {
124            let response = self.embed_texts_with_usage(vec![text.to_string()]).await?;
125            if response.embeddings.is_empty() {
126                return Err(EmbeddingError::ResponseError(
127                    "embedding provider returned an empty response for embed_text_with_usage"
128                        .to_string(),
129                ));
130            }
131            Ok(response)
132        }
133    }
134}
135
136/// Response from an embedding request containing the embeddings and token usage.
137#[derive(Debug, Clone)]
138pub struct EmbeddingResponse {
139    /// The embeddings returned by the provider, one per input text.
140    pub embeddings: Vec<Embedding>,
141    /// Token usage for this embedding request.
142    pub usage: Usage,
143}
144
145/// Trait for embedding models that can generate embeddings for images.
146pub trait ImageEmbeddingModel: Clone + WasmCompatSend + WasmCompatSync {
147    /// The maximum number of images that can be embedded in a single request.
148    const MAX_DOCUMENTS: usize;
149
150    /// The number of dimensions in the embedding vector.
151    fn ndims(&self) -> usize;
152
153    /// Embed multiple images in a single request from bytes.
154    ///
155    /// Implementations should preserve input order in the returned embeddings.
156    fn embed_images(
157        &self,
158        images: impl IntoIterator<Item = Vec<u8>> + WasmCompatSend,
159    ) -> impl std::future::Future<Output = Result<Vec<Embedding>, EmbeddingError>> + Send;
160
161    /// Embed a single image from bytes.
162    fn embed_image<'a>(
163        &'a self,
164        bytes: &'a [u8],
165    ) -> impl std::future::Future<Output = Result<Embedding, EmbeddingError>> + WasmCompatSend {
166        async move {
167            let mut embeddings = self.embed_images(vec![bytes.to_owned()]).await?;
168            embeddings.pop().ok_or_else(|| {
169                EmbeddingError::ResponseError(
170                    "embedding provider returned an empty response for embed_image".to_string(),
171                )
172            })
173        }
174    }
175}
176
177/// Struct that holds a single document and its embedding.
178#[derive(Clone, Default, Deserialize, Serialize, Debug)]
179pub struct Embedding {
180    /// The document that was embedded. Used for debugging.
181    pub document: String,
182    /// The embedding vector
183    pub vec: Vec<f64>,
184}
185
186impl PartialEq for Embedding {
187    fn eq(&self, other: &Self) -> bool {
188        self.document == other.document
189    }
190}
191
192impl Eq for Embedding {}
193
194#[cfg(test)]
195mod provider_response_tests {
196    use super::*;
197    use http::StatusCode;
198
199    #[test]
200    fn embedding_error_provider_response_helpers_with_preserved_json_body() {
201        let body = r#"{"error":{"message":"rate limited"}}"#;
202        let error = EmbeddingError::ProviderResponse(provider_response::ProviderResponseError {
203            status: None,
204            body: body.to_string(),
205        });
206
207        assert_eq!(error.provider_response_body(), Some(body));
208        assert_eq!(error.provider_response_status(), None);
209        assert_eq!(
210            error.provider_response_json().expect("valid JSON"),
211            Some(serde_json::json!({ "error": { "message": "rate limited" } }))
212        );
213    }
214
215    #[test]
216    fn embedding_error_provider_error_is_not_a_provider_response() {
217        let error = EmbeddingError::ProviderError("internal diagnostic".to_string());
218
219        assert_eq!(error.provider_response_body(), None);
220        assert_eq!(error.provider_response_status(), None);
221        assert_eq!(error.provider_response_json().expect("no body"), None);
222    }
223
224    #[test]
225    fn embedding_error_provider_response_helpers_with_http_non_success() {
226        let body = r#"{"error":{"message":"bad request"}}"#;
227        let error = EmbeddingError::HttpError(http_client::Error::InvalidStatusCodeWithMessage(
228            StatusCode::BAD_REQUEST,
229            body.to_string(),
230        ));
231
232        assert_eq!(error.provider_response_body(), Some(body));
233        assert_eq!(
234            error.provider_response_status(),
235            Some(StatusCode::BAD_REQUEST)
236        );
237        assert_eq!(
238            error.provider_response_json().expect("valid JSON"),
239            Some(serde_json::json!({ "error": { "message": "bad request" } }))
240        );
241    }
242
243    #[test]
244    fn embedding_error_provider_response_helpers_with_preserved_plain_text_body() {
245        let error = EmbeddingError::ProviderResponse(provider_response::ProviderResponseError {
246            status: None,
247            body: "not json".to_string(),
248        });
249
250        assert_eq!(error.provider_response_body(), Some("not json"));
251        assert!(error.provider_response_json().is_err());
252    }
253
254    #[test]
255    fn embedding_error_provider_response_helpers_with_unrelated_variant() {
256        let error = EmbeddingError::ResponseError("parse failed".to_string());
257
258        assert_eq!(error.provider_response_body(), None);
259        assert_eq!(error.provider_response_status(), None);
260        assert_eq!(error.provider_response_json().expect("no body"), None);
261    }
262}