From d675372d726620f2ad63e3f6083426de435f4048 Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:51:23 +0000 Subject: [PATCH 1/2] Support base64 embedding responses --- rust/src/types.rs | 97 +++++++++++++++++++++++++++++++++++- rust/tests/ai_integration.rs | 78 +++++++++++++++++++++++------ 2 files changed, 159 insertions(+), 16 deletions(-) diff --git a/rust/src/types.rs b/rust/src/types.rs index 6e109a9..8159059 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -853,11 +853,35 @@ pub struct EmbeddingResponse { pub usage: EmbeddingUsage, } +/// An embedding vector in the representation requested by `encoding_format`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EmbeddingVector { + Float(Vec), + Base64(String), +} + +impl EmbeddingVector { + pub fn as_floats(&self) -> Option<&[f64]> { + match self { + Self::Float(values) => Some(values), + Self::Base64(_) => None, + } + } + + pub fn as_base64(&self) -> Option<&str> { + match self { + Self::Float(_) => None, + Self::Base64(value) => Some(value), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmbeddingData { pub object: String, pub index: i32, - pub embedding: Vec, + pub embedding: EmbeddingVector, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1143,4 +1167,75 @@ mod tests { assert_eq!(response.access_token.as_deref(), Some("new-access")); assert_eq!(response.refresh_token, None); } + + #[test] + fn embedding_response_deserializes_float_vectors() { + let response: EmbeddingResponse = serde_json::from_value(json!({ + "object": "list", + "data": [{ + "object": "embedding", + "index": 0, + "embedding": [0.1, 0.2, 0.3] + }], + "model": "nomic-embed-text", + "usage": { "prompt_tokens": 4, "total_tokens": 4 } + })) + .unwrap(); + + assert_eq!( + response.data[0].embedding, + EmbeddingVector::Float(vec![0.1, 0.2, 0.3]) + ); + assert_eq!( + response.data[0].embedding.as_floats(), + Some([0.1, 0.2, 0.3].as_slice()) + ); + assert_eq!(response.data[0].embedding.as_base64(), None); + assert_eq!( + serde_json::to_value(&response).unwrap()["data"][0]["embedding"], + json!([0.1, 0.2, 0.3]) + ); + } + + #[test] + fn embedding_response_deserializes_base64_vectors() { + let response: EmbeddingResponse = serde_json::from_value(json!({ + "object": "list", + "data": [{ + "object": "embedding", + "index": 0, + "embedding": "AQIDBA==" + }], + "model": "nomic-embed-text", + "usage": { "prompt_tokens": 4, "total_tokens": 4 } + })) + .unwrap(); + + assert_eq!( + response.data[0].embedding, + EmbeddingVector::Base64("AQIDBA==".to_string()) + ); + assert_eq!(response.data[0].embedding.as_floats(), None); + assert_eq!(response.data[0].embedding.as_base64(), Some("AQIDBA==")); + assert_eq!( + serde_json::to_value(&response).unwrap()["data"][0]["embedding"], + json!("AQIDBA==") + ); + } + + #[test] + fn embedding_response_rejects_unknown_vector_representations() { + let response = serde_json::from_value::(json!({ + "object": "list", + "data": [{ + "object": "embedding", + "index": 0, + "embedding": { "values": [0.1, 0.2, 0.3] } + }], + "model": "nomic-embed-text", + "usage": { "prompt_tokens": 4, "total_tokens": 4 } + })); + + assert!(response.is_err()); + } } diff --git a/rust/tests/ai_integration.rs b/rust/tests/ai_integration.rs index 0f35397..0ddb8a2 100644 --- a/rust/tests/ai_integration.rs +++ b/rust/tests/ai_integration.rs @@ -1,3 +1,4 @@ +use base64::{engine::general_purpose, Engine as _}; use futures::StreamExt; use opensecret::{ ChatCompletionRequest, ChatMessage, EmbeddingInput, EmbeddingRequest, Error, Function, @@ -401,7 +402,7 @@ async fn test_guest_user_cannot_use_ai() { } #[tokio::test] -async fn test_create_embeddings_single_input() { +async fn test_create_embeddings_float_encoding() { let client = setup_authenticated_client() .await .expect("Failed to setup client"); @@ -409,7 +410,7 @@ async fn test_create_embeddings_single_input() { let request = EmbeddingRequest { input: EmbeddingInput::Single("Hello, world!".to_string()), model: embedding_model(), - encoding_format: None, + encoding_format: Some("float".to_string()), dimensions: None, user: None, }; @@ -425,12 +426,12 @@ async fn test_create_embeddings_single_input() { assert_eq!(response.data[0].object, "embedding"); assert_eq!(response.data[0].index, 0); - let expected_dimensions = embedding_dimensions(); - assert_eq!( - response.data[0].embedding.len(), - expected_dimensions, - "Unexpected embedding dimensions" - ); + let embedding = response.data[0] + .embedding + .as_floats() + .expect("Expected float embedding response"); + assert_eq!(embedding.len(), embedding_dimensions()); + assert!(embedding.iter().all(|value| value.is_finite())); // Verify usage assert!(response.usage.prompt_tokens > 0); @@ -438,11 +439,51 @@ async fn test_create_embeddings_single_input() { println!( "Embedding created with {} dimensions, {} tokens used", - response.data[0].embedding.len(), + embedding.len(), response.usage.total_tokens ); } +#[tokio::test] +async fn test_create_embeddings_base64_encoding() { + let client = setup_authenticated_client() + .await + .expect("Failed to setup client"); + + let request = EmbeddingRequest { + input: EmbeddingInput::Single("Hello, world!".to_string()), + model: embedding_model(), + encoding_format: Some("base64".to_string()), + dimensions: None, + user: None, + }; + + let response = client + .create_embeddings(request) + .await + .expect("Failed to create base64 embeddings"); + + assert_eq!(response.object, "list"); + assert_eq!(response.data.len(), 1); + assert_eq!(response.data[0].object, "embedding"); + assert_eq!(response.data[0].index, 0); + + let embedding = response.data[0] + .embedding + .as_base64() + .expect("Expected base64 embedding response"); + let decoded = general_purpose::STANDARD + .decode(embedding) + .expect("Embedding should contain valid base64"); + assert_eq!( + decoded.len(), + embedding_dimensions() * std::mem::size_of::() + ); + + assert!(response.usage.prompt_tokens > 0); + assert!(response.usage.total_tokens > 0); +} + #[tokio::test] async fn test_create_embeddings_multiple_inputs() { let client = setup_authenticated_client() @@ -474,11 +515,11 @@ async fn test_create_embeddings_multiple_inputs() { for (i, embedding_data) in response.data.iter().enumerate() { assert_eq!(embedding_data.object, "embedding"); assert_eq!(embedding_data.index as usize, i); - assert_eq!( - embedding_data.embedding.len(), - embedding_dimensions(), - "Unexpected embedding dimensions" - ); + let embedding = embedding_data + .embedding + .as_floats() + .expect("Expected default float embedding response"); + assert_eq!(embedding.len(), embedding_dimensions()); } // Verify usage accounts for all inputs @@ -512,7 +553,14 @@ async fn test_embeddings_from_string_conversion() { .expect("Failed to create embeddings"); assert_eq!(response.data.len(), 1); - assert_eq!(response.data[0].embedding.len(), embedding_dimensions()); + assert_eq!( + response.data[0] + .embedding + .as_floats() + .expect("Expected default float embedding response") + .len(), + embedding_dimensions() + ); } #[tokio::test] From 3ba56e63dba8b302bedaf9c7fc2f6e7ad4ac6b31 Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Sat, 11 Jul 2026 19:52:44 +0000 Subject: [PATCH 2/2] Bump Rust SDK version to 3.3.0 --- rust/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 4ff20d9..5a217ad 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opensecret" -version = "3.2.0" +version = "3.3.0" edition = "2021" authors = ["OpenSecret"] description = "Rust SDK for OpenSecret - secure AI API interactions with nitro attestation"