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}