Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
97 changes: 96 additions & 1 deletion rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>),
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<f64>,
pub embedding: EmbeddingVector,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -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::<EmbeddingResponse>(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());
}
}
78 changes: 63 additions & 15 deletions rust/tests/ai_integration.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use base64::{engine::general_purpose, Engine as _};
use futures::StreamExt;
use opensecret::{
ChatCompletionRequest, ChatMessage, EmbeddingInput, EmbeddingRequest, Error, Function,
Expand Down Expand Up @@ -401,15 +402,15 @@ 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");

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,
};
Expand All @@ -425,24 +426,64 @@ 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);
assert!(response.usage.total_tokens > 0);

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::<f32>()
);

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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
Loading