openai_tools/embedding/request.rs
1//! OpenAI Embeddings API Request Module
2//!
3//! This module provides the functionality to build and send requests to the OpenAI Embeddings API.
4//! It offers a builder pattern for constructing embedding requests, allowing you to convert text
5//! into numerical vector representations that capture semantic meaning.
6//!
7//! # Key Features
8//!
9//! - **Builder Pattern**: Fluent API for constructing embedding requests
10//! - **Single & Batch Input**: Support for single text or multiple texts at once
11//! - **Encoding Formats**: Support for `float` and `base64` output formats
12//! - **Error Handling**: Robust error management and validation
13//!
14//! # Quick Start
15//!
16//! ```rust,no_run
17//! use openai_tools::embedding::request::Embedding;
18//! use openai_tools::common::models::EmbeddingModel;
19//!
20//! #[tokio::main]
21//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! // Initialize the embedding client
23//! let mut embedding = Embedding::new()?;
24//!
25//! // Generate embedding for a single text
26//! let response = embedding
27//! .model(EmbeddingModel::TextEmbedding3Small)
28//! .input_text("Hello, world!")
29//! .embed()
30//! .await?;
31//!
32//! let vector = response.data[0].embedding.as_1d().unwrap();
33//! println!("Embedding dimension: {}", vector.len());
34//! Ok(())
35//! }
36//! ```
37//!
38//! # Batch Processing
39//!
40//! ```rust,no_run
41//! use openai_tools::embedding::request::Embedding;
42//! use openai_tools::common::models::EmbeddingModel;
43//!
44//! #[tokio::main]
45//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
46//! let mut embedding = Embedding::new()?;
47//!
48//! // Embed multiple texts in a single request
49//! let texts = vec!["First text", "Second text", "Third text"];
50//!
51//! let response = embedding
52//! .model(EmbeddingModel::TextEmbedding3Small)
53//! .input_text_array(texts)
54//! .embed()
55//! .await?;
56//!
57//! for data in &response.data {
58//! println!("Index {}: {} dimensions",
59//! data.index,
60//! data.embedding.as_1d().unwrap().len());
61//! }
62//! Ok(())
63//! }
64//! ```
65
66use crate::common::auth::{AuthProvider, OpenAIAuth};
67use crate::common::client::create_http_client;
68use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
69use crate::common::models::EmbeddingModel;
70use crate::embedding::response::Response;
71use core::str;
72use serde::{Deserialize, Serialize};
73use std::time::Duration;
74
75/// Internal structure for handling input text in embedding requests.
76///
77/// This struct supports two input formats:
78/// - Single text string (`input_text`)
79/// - Array of text strings (`input_text_array`)
80///
81/// The custom `Serialize` implementation ensures proper JSON formatting
82/// based on which input type is provided.
83#[derive(Debug, Clone, Deserialize, Default)]
84struct Input {
85 /// Single input text for embedding
86 #[serde(skip_serializing_if = "String::is_empty")]
87 input_text: String,
88 /// Array of input texts for batch embedding
89 #[serde(skip_serializing_if = "Vec::is_empty")]
90 input_text_array: Vec<String>,
91}
92
93impl Input {
94 /// Creates an Input from a single text string.
95 ///
96 /// # Arguments
97 ///
98 /// * `input_text` - The text to embed
99 ///
100 /// # Returns
101 ///
102 /// A new `Input` instance with the single text set
103 pub fn from_text(input_text: &str) -> Self {
104 Self { input_text: input_text.to_string(), input_text_array: vec![] }
105 }
106
107 /// Creates an Input from an array of text strings.
108 ///
109 /// # Arguments
110 ///
111 /// * `input_text_array` - Vector of texts to embed
112 ///
113 /// # Returns
114 ///
115 /// A new `Input` instance with the text array set
116 pub fn from_text_array(input_text_array: Vec<String>) -> Self {
117 Self { input_text: String::new(), input_text_array }
118 }
119}
120
121/// Custom serialization for Input to match OpenAI API format.
122///
123/// The OpenAI Embeddings API accepts either a single string or an array of strings
124/// for the `input` field. This implementation serializes to the appropriate format
125/// based on which field is populated.
126impl Serialize for Input {
127 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
128 where
129 S: serde::Serializer,
130 {
131 if !self.input_text.is_empty() && self.input_text_array.is_empty() {
132 self.input_text.serialize(serializer)
133 } else if self.input_text.is_empty() && !self.input_text_array.is_empty() {
134 self.input_text_array.serialize(serializer)
135 } else {
136 // Default to empty string if both are empty
137 "".serialize(serializer)
138 }
139 }
140}
141
142/// Request body structure for the OpenAI Embeddings API.
143///
144/// Contains all parameters that can be sent to the API endpoint.
145#[derive(Debug, Clone, Deserialize, Serialize, Default)]
146struct Body {
147 /// The model to use for embedding generation
148 model: EmbeddingModel,
149 /// The input text(s) to embed
150 input: Input,
151 /// The format for the output embeddings ("float" or "base64")
152 encoding_format: Option<String>,
153}
154
155/// Default API path for Embeddings
156const EMBEDDINGS_PATH: &str = "embeddings";
157
158/// Main struct for building and sending embedding requests to the OpenAI API.
159///
160/// This struct provides a builder pattern interface for constructing embedding
161/// requests with various parameters. Use [`Embedding::new()`] to create a new
162/// instance, then chain methods to configure the request before calling
163/// [`embed()`](Embedding::embed).
164///
165/// # Providers
166///
167/// The client supports two providers:
168/// - **OpenAI**: Standard OpenAI API (default)
169/// - **Azure**: Azure OpenAI Service
170///
171/// # Example
172///
173/// ```rust,no_run
174/// use openai_tools::embedding::request::Embedding;
175/// use openai_tools::common::models::EmbeddingModel;
176///
177/// #[tokio::main]
178/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
179/// let mut embedding = Embedding::new()?;
180///
181/// let response = embedding
182/// .model(EmbeddingModel::TextEmbedding3Small)
183/// .input_text("Sample text")
184/// .embed()
185/// .await?;
186///
187/// Ok(())
188/// }
189/// ```
190pub struct Embedding {
191 /// Authentication provider (OpenAI or Azure)
192 auth: AuthProvider,
193 /// Request body containing model and input parameters
194 body: Body,
195 /// Optional request timeout duration
196 timeout: Option<Duration>,
197}
198
199impl Embedding {
200 /// Creates a new Embedding instance for OpenAI API.
201 ///
202 /// Initializes the embedding client by loading the OpenAI API key from
203 /// the environment variable `OPENAI_API_KEY`. Supports `.env` file loading
204 /// via dotenvy.
205 ///
206 /// # Returns
207 ///
208 /// * `Ok(Embedding)` - A new embedding instance ready for configuration
209 /// * `Err(OpenAIToolError)` - If the API key is not found in the environment
210 ///
211 /// # Example
212 ///
213 /// ```rust,no_run
214 /// use openai_tools::embedding::request::Embedding;
215 ///
216 /// let embedding = Embedding::new().expect("API key should be set");
217 /// ```
218 pub fn new() -> Result<Self> {
219 let auth = AuthProvider::openai_from_env()?;
220 let body = Body::default();
221 Ok(Self { auth, body, timeout: None })
222 }
223
224 /// Creates a new Embedding instance with a custom authentication provider
225 ///
226 /// Use this to explicitly configure OpenAI or Azure authentication.
227 ///
228 /// # Arguments
229 ///
230 /// * `auth` - The authentication provider
231 ///
232 /// # Returns
233 ///
234 /// A new Embedding instance with the specified auth provider
235 pub fn with_auth(auth: AuthProvider) -> Self {
236 Self { auth, body: Body::default(), timeout: None }
237 }
238
239 /// Creates a new Embedding instance for Azure OpenAI API
240 ///
241 /// Loads configuration from Azure-specific environment variables.
242 ///
243 /// # Returns
244 ///
245 /// `Result<Embedding>` - Configured for Azure or error if env vars missing
246 pub fn azure() -> Result<Self> {
247 let auth = AuthProvider::azure_from_env()?;
248 Ok(Self { auth, body: Body::default(), timeout: None })
249 }
250
251 /// Creates a new Embedding instance by auto-detecting the provider
252 ///
253 /// Tries Azure first (if AZURE_OPENAI_API_KEY is set), then falls back to OpenAI.
254 pub fn detect_provider() -> Result<Self> {
255 let auth = AuthProvider::from_env()?;
256 Ok(Self { auth, body: Body::default(), timeout: None })
257 }
258
259 /// Creates a new Embedding instance with URL-based provider detection
260 ///
261 /// Analyzes the URL pattern to determine the provider:
262 /// - URLs containing `.openai.azure.com` → Azure
263 /// - All other URLs → OpenAI-compatible
264 ///
265 /// # Arguments
266 ///
267 /// * `base_url` - The complete base URL for API requests
268 /// * `api_key` - The API key or token
269 pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
270 let auth = AuthProvider::from_url_with_key(base_url, api_key);
271 Self { auth, body: Body::default(), timeout: None }
272 }
273
274 /// Creates a new Embedding instance from URL using environment variables
275 ///
276 /// Analyzes the URL pattern to determine the provider, then loads
277 /// credentials from the appropriate environment variables.
278 pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
279 let auth = AuthProvider::from_url(url)?;
280 Ok(Self { auth, body: Body::default(), timeout: None })
281 }
282
283 /// Returns the authentication provider
284 pub fn auth(&self) -> &AuthProvider {
285 &self.auth
286 }
287
288 /// Sets a custom API endpoint URL (OpenAI only)
289 ///
290 /// Use this to point to alternative OpenAI-compatible APIs.
291 ///
292 /// # Arguments
293 ///
294 /// * `url` - The base URL (e.g., `https://my-proxy.example.com/v1`)
295 ///
296 /// # Returns
297 ///
298 /// A mutable reference to self for method chaining
299 pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
300 if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
301 let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
302 self.auth = AuthProvider::OpenAI(new_auth);
303 } else {
304 tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
305 }
306 self
307 }
308
309 /// Sets the model to use for embedding generation.
310 ///
311 /// # Arguments
312 ///
313 /// * `model` - The embedding model to use
314 ///
315 /// # Returns
316 ///
317 /// A mutable reference to self for method chaining
318 ///
319 /// # Example
320 ///
321 /// ```rust,no_run
322 /// use openai_tools::embedding::request::Embedding;
323 /// use openai_tools::common::models::EmbeddingModel;
324 ///
325 /// let mut embedding = Embedding::new().unwrap();
326 /// embedding.model(EmbeddingModel::TextEmbedding3Small);
327 /// ```
328 pub fn model(&mut self, model: EmbeddingModel) -> &mut Self {
329 self.body.model = model;
330 self
331 }
332
333 /// Sets the model using a string ID (for backward compatibility).
334 ///
335 /// Prefer using [`model`](Self::model) with `EmbeddingModel` enum for type safety.
336 ///
337 /// # Arguments
338 ///
339 /// * `model_id` - The model identifier string (e.g., "text-embedding-3-small")
340 ///
341 /// # Returns
342 ///
343 /// A mutable reference to self for method chaining
344 #[deprecated(since = "0.2.0", note = "Use `model(EmbeddingModel)` instead for type safety")]
345 pub fn model_id<T: AsRef<str>>(&mut self, model_id: T) -> &mut Self {
346 self.body.model = EmbeddingModel::from(model_id.as_ref());
347 self
348 }
349
350 /// Sets the request timeout duration.
351 ///
352 /// # Arguments
353 ///
354 /// * `timeout` - The maximum time to wait for a response
355 ///
356 /// # Returns
357 ///
358 /// A mutable reference to self for method chaining
359 ///
360 /// # Example
361 ///
362 /// ```rust,no_run
363 /// use std::time::Duration;
364 /// use openai_tools::embedding::request::Embedding;
365 /// use openai_tools::common::models::EmbeddingModel;
366 ///
367 /// let mut embedding = Embedding::new().unwrap();
368 /// embedding.model(EmbeddingModel::TextEmbedding3Small)
369 /// .timeout(Duration::from_secs(30));
370 /// ```
371 pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
372 self.timeout = Some(timeout);
373 self
374 }
375
376 /// Sets a single text input for embedding.
377 ///
378 /// Use this method when you want to embed a single piece of text.
379 /// For multiple texts, use [`input_text_array`](Self::input_text_array) instead.
380 ///
381 /// # Arguments
382 ///
383 /// * `input_text` - The text to convert into an embedding vector
384 ///
385 /// # Returns
386 ///
387 /// A mutable reference to self for method chaining
388 ///
389 /// # Example
390 ///
391 /// ```rust,no_run
392 /// # use openai_tools::embedding::request::Embedding;
393 /// # let mut embedding = Embedding::new().unwrap();
394 /// embedding.input_text("Hello, world!");
395 /// ```
396 pub fn input_text<T: AsRef<str>>(&mut self, input_text: T) -> &mut Self {
397 self.body.input = Input::from_text(input_text.as_ref());
398 self
399 }
400
401 /// Sets multiple text inputs for batch embedding.
402 ///
403 /// Use this method when you want to embed multiple texts in a single API call.
404 /// This is more efficient than making separate requests for each text.
405 ///
406 /// # Arguments
407 ///
408 /// * `input_text_array` - Vector of texts to convert into embedding vectors
409 ///
410 /// # Returns
411 ///
412 /// A mutable reference to self for method chaining
413 ///
414 /// # Example
415 ///
416 /// ```rust,no_run
417 /// # use openai_tools::embedding::request::Embedding;
418 /// # let mut embedding = Embedding::new().unwrap();
419 /// let texts = vec!["First text", "Second text", "Third text"];
420 /// embedding.input_text_array(texts);
421 /// ```
422 pub fn input_text_array<T: AsRef<str>>(&mut self, input_text_array: Vec<T>) -> &mut Self {
423 let input_strings = input_text_array.into_iter().map(|s| s.as_ref().to_string()).collect();
424 self.body.input = Input::from_text_array(input_strings);
425 self
426 }
427
428 /// Sets the encoding format for the output embeddings.
429 ///
430 /// # Arguments
431 ///
432 /// * `encoding_format` - Either "float" (default) or "base64"
433 /// - `"float"`: Returns embeddings as arrays of floating point numbers
434 /// - `"base64"`: Returns embeddings as base64-encoded strings (more compact)
435 ///
436 /// # Returns
437 ///
438 /// A mutable reference to self for method chaining
439 ///
440 /// # Panics
441 ///
442 /// Panics if `encoding_format` is not "float" or "base64"
443 ///
444 /// # Example
445 ///
446 /// ```rust,no_run
447 /// # use openai_tools::embedding::request::Embedding;
448 /// # let mut embedding = Embedding::new().unwrap();
449 /// embedding.encoding_format("float");
450 /// ```
451 pub fn encoding_format<T: AsRef<str>>(&mut self, encoding_format: T) -> &mut Self {
452 let encoding_format = encoding_format.as_ref();
453 assert!(encoding_format == "float" || encoding_format == "base64", "encoding_format must be either 'float' or 'base64'");
454 self.body.encoding_format = Some(encoding_format.to_string());
455 self
456 }
457
458 /// Sends the embedding request to the OpenAI API.
459 ///
460 /// This method validates the request parameters, constructs the HTTP request,
461 /// sends it to the OpenAI Embeddings API endpoint, and parses the response.
462 ///
463 /// # Returns
464 ///
465 /// * `Ok(Response)` - The embedding response containing vectors and metadata
466 /// * `Err(OpenAIToolError)` - If validation fails, the request fails, or parsing fails
467 ///
468 /// # Errors
469 ///
470 /// Returns an error if:
471 /// - API key is not set
472 /// - Model ID is not set
473 /// - Input text is not set
474 /// - Network request fails
475 /// - Response parsing fails
476 ///
477 /// # Example
478 ///
479 /// ```rust,no_run
480 /// # use openai_tools::embedding::request::Embedding;
481 /// # use openai_tools::common::models::EmbeddingModel;
482 /// # #[tokio::main]
483 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
484 /// let mut embedding = Embedding::new()?;
485 /// let response = embedding
486 /// .model(EmbeddingModel::TextEmbedding3Small)
487 /// .input_text("Hello, world!")
488 /// .embed()
489 /// .await?;
490 /// # Ok(())
491 /// # }
492 /// ```
493 pub async fn embed(&self) -> Result<Response> {
494 // Validate that input text is set
495 if self.body.input.input_text.is_empty() && self.body.input.input_text_array.is_empty() {
496 return Err(OpenAIToolError::Error("Input text is not set.".into()));
497 }
498
499 let body = serde_json::to_string(&self.body)?;
500
501 let client = create_http_client(self.timeout)?;
502 let mut headers = request::header::HeaderMap::new();
503 headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
504 headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
505
506 // Apply provider-specific authentication headers
507 self.auth.apply_headers(&mut headers)?;
508
509 if cfg!(test) {
510 // Replace API key with a placeholder in debug mode
511 let body_for_debug = serde_json::to_string_pretty(&self.body).unwrap().replace(self.auth.api_key(), "*************");
512 tracing::info!("Request body: {}", body_for_debug);
513 }
514
515 // Get the endpoint URL from the auth provider
516 let endpoint = self.auth.endpoint(EMBEDDINGS_PATH);
517
518 let response = client.post(&endpoint).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
519 let status = response.status();
520 let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
521
522 if cfg!(test) {
523 tracing::info!("Response content: {}", content);
524 }
525
526 if !status.is_success() {
527 if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
528 return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
529 }
530 return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
531 }
532
533 serde_json::from_str::<Response>(&content).map_err(OpenAIToolError::SerdeJsonError)
534 }
535}