Skip to main content

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}