Skip to main content

providers_kit/
embedding.rs

1//! Portable embedding requests and results.
2//!
3//! Select one exact Model before submitting an [`EmbedRequest`]. Input order maps
4//! directly to [`EmbedResponse::vectors`](crate::embedding::EmbedResponse::vectors). Requested dimensions are optional and
5//! model support is authoritative at the operation boundary.
6//!
7//! # Embed an ordered collection
8//!
9//! This example needs an OpenAI key and access to the selected embedding model.
10//! Only compare or store vectors using the same selected model/dimension convention.
11//! The OpenAI operation validates vector count and index correlation. This example
12//! separately checks that each vector has the requested dimensions; neither check
13//! establishes semantic retrieval quality.
14//!
15//! ```no_run
16//! use providers_kit::{Provider, ProviderBackend, ProviderConfig, ModelBackend, ModelId};
17//! use providers_kit::config::{ApiKey, OpenAiConfig, ProviderKind, TransportConfig};
18//! use providers_kit::embedding::EmbedRequest;
19//! # #[tokio::main(flavor = "current_thread")]
20//! # async fn main() -> Result<(), providers_kit::KitError> {
21//! let provider = Provider::from_config(ProviderConfig::new(
22//!     ProviderKind::OpenAi(OpenAiConfig::new(ApiKey::new("replace-with-your-key")?)),
23//!     TransportConfig::default(),
24//! ))?;
25//! let work = async {
26//!     let model = provider.model(ModelId::new("text-embedding-3-small")?)?;
27//!     let request = EmbedRequest::new(vec!["first document".into(), "second document".into()])?
28//!         .with_dimensions(std::num::NonZeroU32::new(256).unwrap());
29//!     let response = model.embed(&request).await?;
30//!     assert_eq!(response.vectors.len(), request.inputs().len());
31//!     assert!(response.vectors.iter().all(|vector| vector.len() == 256));
32//!     Ok::<_, providers_kit::KitError>(response)
33//! }.await;
34//! let cleanup = provider.shutdown().await;
35//! let _ordered_vectors = work?;
36//! cleanup?;
37//! # Ok(())
38//! # }
39//! ```
40
41use std::num::NonZeroU32;
42
43use serde::{Deserialize, Deserializer, Serialize};
44
45use crate::KitError;
46use crate::error::ValidationViolation;
47use crate::generation::Usage;
48
49/// Validated text-embedding request.
50///
51/// # Examples
52///
53/// ```
54/// use providers_kit::embedding::EmbedRequest;
55/// let request = EmbedRequest::new(vec!["search document".into()])?
56///     .with_dimensions(std::num::NonZeroU32::new(128).unwrap());
57/// assert_eq!(request.inputs(), &["search document"]);
58/// assert_eq!(request.dimensions().unwrap().get(), 128);
59///
60/// # Ok::<(), providers_kit::KitError>(())
61/// ```
62///
63#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
64#[serde(deny_unknown_fields)]
65pub struct EmbedRequest {
66    inputs: Vec<String>,
67    dimensions: Option<NonZeroU32>,
68}
69
70impl EmbedRequest {
71    /// Creates a request containing non-empty text inputs.
72    ///
73    /// # Errors
74    ///
75    /// Returns an input error if the input list is empty or any text is whitespace-only.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use providers_kit::embedding::EmbedRequest;
81    /// let request = EmbedRequest::new(vec!["search document".into()])?
82    ///     .with_dimensions(std::num::NonZeroU32::new(128).unwrap());
83    /// assert_eq!(request.inputs(), &["search document"]);
84    /// assert_eq!(request.dimensions().unwrap().get(), 128);
85    ///
86    /// # Ok::<(), providers_kit::KitError>(())
87    /// ```
88    ///
89    pub fn new(inputs: Vec<String>) -> Result<Self, KitError> {
90        if inputs.is_empty() || inputs.iter().any(|value| value.trim().is_empty()) {
91            return Err(KitError::invalid_input(
92                ValidationViolation::Empty {
93                    field: "embedding.inputs",
94                },
95                "embedding inputs must contain non-empty text",
96            ));
97        }
98        Ok(Self {
99            inputs,
100            dimensions: None,
101        })
102    }
103
104    /// Requests a non-zero output dimensionality.
105    ///
106    /// The Model is already selected; dimensionality support is checked by the model operation.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use providers_kit::embedding::EmbedRequest;
112    /// let request = EmbedRequest::new(vec!["search document".into()])?
113    ///     .with_dimensions(std::num::NonZeroU32::new(128).unwrap());
114    /// assert_eq!(request.inputs(), &["search document"]);
115    /// assert_eq!(request.dimensions().unwrap().get(), 128);
116    ///
117    /// # Ok::<(), providers_kit::KitError>(())
118    /// ```
119    ///
120    #[must_use]
121    pub fn with_dimensions(mut self, dimensions: NonZeroU32) -> Self {
122        self.dimensions = Some(dimensions);
123        self
124    }
125
126    /// Returns text inputs in caller order.
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use providers_kit::embedding::EmbedRequest;
132    /// let request = EmbedRequest::new(vec!["search document".into()])?
133    ///     .with_dimensions(std::num::NonZeroU32::new(128).unwrap());
134    /// assert_eq!(request.inputs(), &["search document"]);
135    /// assert_eq!(request.dimensions().unwrap().get(), 128);
136    ///
137    /// # Ok::<(), providers_kit::KitError>(())
138    /// ```
139    ///
140    #[must_use]
141    pub fn inputs(&self) -> &[String] {
142        &self.inputs
143    }
144
145    /// Returns the requested output dimensionality.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use providers_kit::embedding::EmbedRequest;
151    /// let request = EmbedRequest::new(vec!["search document".into()])?
152    ///     .with_dimensions(std::num::NonZeroU32::new(128).unwrap());
153    /// assert_eq!(request.inputs(), &["search document"]);
154    /// assert_eq!(request.dimensions().unwrap().get(), 128);
155    ///
156    /// # Ok::<(), providers_kit::KitError>(())
157    /// ```
158    ///
159    #[must_use]
160    pub const fn dimensions(&self) -> Option<NonZeroU32> {
161        self.dimensions
162    }
163}
164
165impl<'de> Deserialize<'de> for EmbedRequest {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: Deserializer<'de>,
169    {
170        #[derive(Deserialize)]
171        #[serde(deny_unknown_fields)]
172        struct Wire {
173            inputs: Vec<String>,
174            dimensions: Option<NonZeroU32>,
175        }
176        let wire = Wire::deserialize(deserializer)?;
177        let mut request = Self::new(wire.inputs).map_err(serde::de::Error::custom)?;
178        request.dimensions = wire.dimensions;
179        Ok(request)
180    }
181}
182
183/// Embedding vectors returned in request-input order.
184///
185/// # Examples
186///
187/// ```
188/// use providers_kit::{embedding::EmbedResponse, generation::Usage};
189/// let response = EmbedResponse { vectors: vec![vec![0.25, 0.75]], usage: Usage::default() };
190/// assert_eq!(response.vectors.len(), 1);
191/// assert!(response.usage.total_tokens.is_none());
192/// ```
193///
194#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
195pub struct EmbedResponse {
196    /// Dense vectors corresponding exactly to request inputs.
197    pub vectors: Vec<Vec<f32>>,
198    /// Provider usage accounting.
199    pub usage: Usage,
200}