Skip to main content

openai_interface/embeddings/
response.rs

1use serde::Deserialize;
2
3/// The response of an embedding request.
4#[derive(Debug, Deserialize, Clone)]
5pub struct CreateEmbeddingResponse {
6    /// The list of embeddings generated by the model.
7    pub data: Vec<Embedding>,
8    /// The name of the model used to generate the embedding.
9    pub model: String,
10    /// The object type, which is always `list`.
11    pub object: String,
12    /// The usage information for the request.
13    pub usage: Usage,
14}
15
16/// Represents an embedding vector returned by the embedding endpoint.
17#[derive(Debug, Deserialize, Clone)]
18pub struct Embedding {
19    /// The embedding vector.
20    ///
21    /// With `encoding_format: "base64"`, the API returns the vector as a
22    /// base64 encoded string instead of a list of floats.
23    pub embedding: EmbeddingVector,
24    /// The index of the embedding in the list of embeddings.
25    pub index: usize,
26    /// The object type, which is always `embedding`.
27    pub object: String,
28}
29
30/// The embedding vector, either as a list of floats or (with
31/// `encoding_format: "base64"`) as a base64 encoded string.
32#[derive(Debug, Deserialize, Clone, PartialEq)]
33#[serde(untagged)]
34pub enum EmbeddingVector {
35    Floats(Vec<f32>),
36    Base64(String),
37}
38
39/// The usage information for an embedding request.
40#[derive(Debug, Deserialize, Clone)]
41pub struct Usage {
42    /// The number of tokens used by the prompt.
43    pub prompt_tokens: usize,
44    /// The total number of tokens used by the request.
45    pub total_tokens: usize,
46}
47
48crate::impl_from_str!(CreateEmbeddingResponse);
49
50#[cfg(test)]
51mod tests {
52    /// Deserializes a float embedding response.
53    ///
54    /// Fixture is a verbatim response captured from
55    /// `POST https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings`
56    /// with `text-embedding-v4` (2026-09-01). The 1024-element embedding
57    /// vector is abbreviated to its first 16 captured values to keep the
58    /// test readable; `usage`, `model` and `id` are verbatim. The top-level
59    /// `id` is a Qwen extension ignored by this type.
60    #[test]
61    fn parse_float_response() {
62        let content = r#"{
63            "data": [
64                {
65                    "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],
66                    "index": 0,
67                    "object": "embedding"
68                }
69            ],
70            "object": "list",
71            "model": "text-embedding-v4",
72            "usage": {"prompt_tokens": 2, "total_tokens": 2},
73            "id": "7010976a-5a36-955a-b837-0eedda118144"
74        }"#;
75
76        let response: super::CreateEmbeddingResponse = content.parse().unwrap();
77        assert_eq!(response.object, "list");
78        assert_eq!(response.data.len(), 1);
79        assert_eq!(response.data[0].index, 0);
80        assert_eq!(response.data[0].object, "embedding");
81        assert_eq!(response.model, "text-embedding-v4");
82        assert_eq!(response.usage.prompt_tokens, 2);
83        assert_eq!(response.usage.total_tokens, 2);
84        let super::EmbeddingVector::Floats(vector) = &response.data[0].embedding else {
85            panic!("expected float embedding vector");
86        };
87        assert_eq!(vector.len(), 16);
88        #[allow(clippy::excessive_precision)]
89        {
90            assert!((vector[0] - 0.033480461686849594).abs() < 1e-6);
91        }
92    }
93
94    /// Deserializes a base64 embedding response.
95    ///
96    /// No accessible provider returns this shape (Qwen only supports
97    /// `encoding_format: "float"`), so this fixture is NOT captured from a
98    /// live response. The structure follows the schema of
99    /// openai-python `types/embedding.py` + `types/create_embedding_response.py`
100    /// and the `encoding_format: "base64"` parameter documentation; the base64
101    /// payload decodes to little-endian f32 `1.0` followed by zeros.
102    #[test]
103    fn parse_base64_response() {
104        let content = r#"{
105            "object": "list",
106            "data": [
107                {
108                    "object": "embedding",
109                    "index": 0,
110                    "embedding": "AACAPwAAAAAAAAAAAAAAAAAAAAA="
111                }
112            ],
113            "model": "text-embedding-3-small",
114            "usage": {"prompt_tokens": 5, "total_tokens": 5}
115        }"#;
116
117        let response: super::CreateEmbeddingResponse = content.parse().unwrap();
118        assert_eq!(
119            response.data[0].embedding,
120            super::EmbeddingVector::Base64("AACAPwAAAAAAAAAAAAAAAAAAAAA=".to_string())
121        );
122    }
123}