openai-interface 0.7.0

A low-level Rust interface for the OpenAI API
Documentation
use serde::Deserialize;

/// The response of an embedding request.
#[derive(Debug, Deserialize, Clone)]
pub struct CreateEmbeddingResponse {
    /// The list of embeddings generated by the model.
    pub data: Vec<Embedding>,
    /// The name of the model used to generate the embedding.
    pub model: String,
    /// The object type, which is always `list`.
    pub object: String,
    /// The usage information for the request.
    pub usage: Usage,
}

/// Represents an embedding vector returned by the embedding endpoint.
#[derive(Debug, Deserialize, Clone)]
pub struct Embedding {
    /// The embedding vector.
    ///
    /// With `encoding_format: "base64"`, the API returns the vector as a
    /// base64 encoded string instead of a list of floats.
    pub embedding: EmbeddingVector,
    /// The index of the embedding in the list of embeddings.
    pub index: usize,
    /// The object type, which is always `embedding`.
    pub object: String,
}

/// The embedding vector, either as a list of floats or (with
/// `encoding_format: "base64"`) as a base64 encoded string.
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum EmbeddingVector {
    Floats(Vec<f32>),
    Base64(String),
}

/// The usage information for an embedding request.
#[derive(Debug, Deserialize, Clone)]
pub struct Usage {
    /// The number of tokens used by the prompt.
    pub prompt_tokens: usize,
    /// The total number of tokens used by the request.
    pub total_tokens: usize,
}

crate::impl_from_str!(CreateEmbeddingResponse);

#[cfg(test)]
mod tests {
    /// Deserializes a float embedding response.
    ///
    /// Fixture is a verbatim response captured from
    /// `POST https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings`
    /// with `text-embedding-v4` (2026-09-01). The 1024-element embedding
    /// vector is abbreviated to its first 16 captured values to keep the
    /// test readable; `usage`, `model` and `id` are verbatim. The top-level
    /// `id` is a Qwen extension ignored by this type.
    #[test]
    fn parse_float_response() {
        let content = r#"{
            "data": [
                {
                    "embedding": [0.033480461686849594,0.00393357127904892,0.008528660982847214,0.03501124680042267,0.053708694875240326,-0.013427173718610081,0.003826963249593973,-0.0037968941032886505,-0.059481941163539886,0.21693414449691772,0.023467724293470383,-0.011972927488386631,0.017680570483207703,0.006883066613227129,0.04200912266969681,-0.04117812216281891],
                    "index": 0,
                    "object": "embedding"
                }
            ],
            "object": "list",
            "model": "text-embedding-v4",
            "usage": {"prompt_tokens": 2, "total_tokens": 2},
            "id": "7010976a-5a36-955a-b837-0eedda118144"
        }"#;

        let response: super::CreateEmbeddingResponse = content.parse().unwrap();
        assert_eq!(response.object, "list");
        assert_eq!(response.data.len(), 1);
        assert_eq!(response.data[0].index, 0);
        assert_eq!(response.data[0].object, "embedding");
        assert_eq!(response.model, "text-embedding-v4");
        assert_eq!(response.usage.prompt_tokens, 2);
        assert_eq!(response.usage.total_tokens, 2);
        let super::EmbeddingVector::Floats(vector) = &response.data[0].embedding else {
            panic!("expected float embedding vector");
        };
        assert_eq!(vector.len(), 16);
        #[allow(clippy::excessive_precision)]
        {
            assert!((vector[0] - 0.033480461686849594).abs() < 1e-6);
        }
    }

    /// Deserializes a base64 embedding response.
    ///
    /// No accessible provider returns this shape (Qwen only supports
    /// `encoding_format: "float"`), so this fixture is NOT captured from a
    /// live response. The structure follows the schema of
    /// openai-python `types/embedding.py` + `types/create_embedding_response.py`
    /// and the `encoding_format: "base64"` parameter documentation; the base64
    /// payload decodes to little-endian f32 `1.0` followed by zeros.
    #[test]
    fn parse_base64_response() {
        let content = r#"{
            "object": "list",
            "data": [
                {
                    "object": "embedding",
                    "index": 0,
                    "embedding": "AACAPwAAAAAAAAAAAAAAAAAAAAA="
                }
            ],
            "model": "text-embedding-3-small",
            "usage": {"prompt_tokens": 5, "total_tokens": 5}
        }"#;

        let response: super::CreateEmbeddingResponse = content.parse().unwrap();
        assert_eq!(
            response.data[0].embedding,
            super::EmbeddingVector::Base64("AACAPwAAAAAAAAAAAAAAAAAAAAA=".to_string())
        );
    }
}