use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Deserialize, ToSchema)]
#[schema(example = json!({
"input": "The food was delicious and the waiter was friendly.",
"model": "text-embedding-ada-002",
"encoding_format": "float",
"dimensions": 1024
}))]
pub struct OpenAIEmbedRequest {
#[schema(value_type = String, example = "The food was delicious and the waiter was friendly.")]
pub input: OpenAIInput,
#[schema(example = "text-embedding-ada-002")]
pub model: String,
#[schema(example = "float")]
#[serde(default)]
pub encoding_format: Option<String>,
#[schema(example = 1024)]
#[serde(default)]
pub dimensions: Option<usize>,
#[serde(default)]
pub user: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum OpenAIInput {
Single(String),
Multiple(Vec<String>),
}
impl OpenAIInput {
pub fn is_single(&self) -> bool {
matches!(self, OpenAIInput::Single(_))
}
pub fn is_multiple(&self) -> bool {
matches!(self, OpenAIInput::Multiple(_))
}
pub fn as_single(&self) -> Option<&str> {
match self {
OpenAIInput::Single(s) => Some(s),
_ => None,
}
}
pub fn as_multiple(&self) -> Option<&Vec<String>> {
match self {
OpenAIInput::Multiple(v) => Some(v),
_ => None,
}
}
pub fn to_vec(&self) -> Vec<String> {
match self {
OpenAIInput::Single(s) => vec![s.clone()],
OpenAIInput::Multiple(v) => v.clone(),
}
}
pub fn len(&self) -> usize {
match self {
OpenAIInput::Single(_) => 1,
OpenAIInput::Multiple(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Serialize, ToSchema)]
#[schema(example = json!({
"object": "list",
"data": [
{
"object": "embedding",
"embedding": [0.0023064255, -0.009327292, -0.0028842222],
"index": 0
}
],
"model": "text-embedding-ada-002",
"usage": {
"prompt_tokens": 8,
"total_tokens": 8
}
}))]
pub struct OpenAIEmbedResponse {
pub object: String,
pub data: Vec<EmbeddingObject>,
pub model: String,
pub usage: Usage,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct EmbeddingObject {
pub object: String,
pub embedding: Vec<f32>,
pub index: usize,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct Usage {
pub prompt_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct OpenAIError {
pub error: OpenAIErrorDetail,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct OpenAIErrorDetail {
pub message: String,
#[serde(rename = "type")]
pub error_type: String,
#[serde(default)]
pub param: Option<String>,
#[serde(default)]
pub code: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncodingFormat {
Float,
Base64,
}
impl EncodingFormat {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"float" => Some(EncodingFormat::Float),
"base64" => Some(EncodingFormat::Base64),
_ => None,
}
}
pub fn is_base64(&self) -> bool {
matches!(self, EncodingFormat::Base64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openai_input_single() {
let input = OpenAIInput::Single("hello".to_string());
assert!(input.is_single());
assert!(!input.is_multiple());
assert_eq!(input.as_single(), Some("hello"));
assert!(input.as_multiple().is_none());
assert_eq!(input.len(), 1);
assert!(!input.is_empty());
}
#[test]
fn test_openai_input_multiple() {
let input = OpenAIInput::Multiple(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
assert!(!input.is_single());
assert!(input.is_multiple());
assert!(input.as_single().is_none());
assert_eq!(input.as_multiple().map(|v| v.len()), Some(3));
assert_eq!(input.len(), 3);
assert!(!input.is_empty());
}
#[test]
fn test_openai_input_empty_multiple() {
let input = OpenAIInput::Multiple(vec![]);
assert!(input.is_multiple());
assert_eq!(input.len(), 0);
assert!(input.is_empty());
}
#[test]
fn test_openai_input_to_vec_single() {
let input = OpenAIInput::Single("world".to_string());
let v = input.to_vec();
assert_eq!(v, vec!["world".to_string()]);
}
#[test]
fn test_openai_input_to_vec_multiple() {
let input = OpenAIInput::Multiple(vec!["x".to_string(), "y".to_string()]);
let v = input.to_vec();
assert_eq!(v, vec!["x".to_string(), "y".to_string()]);
}
#[test]
fn test_openai_input_deserialize_single() {
let json = "\"single text\"";
let input: OpenAIInput = serde_json::from_str(json).unwrap();
assert!(input.is_single());
assert_eq!(input.as_single(), Some("single text"));
}
#[test]
fn test_openai_input_deserialize_multiple() {
let json = "[\"text1\", \"text2\"]";
let input: OpenAIInput = serde_json::from_str(json).unwrap();
assert!(input.is_multiple());
assert_eq!(input.len(), 2);
}
#[test]
fn test_encoding_format_parse_float() {
assert_eq!(EncodingFormat::parse("float"), Some(EncodingFormat::Float));
assert_eq!(EncodingFormat::parse("FLOAT"), Some(EncodingFormat::Float));
assert_eq!(EncodingFormat::parse("Float"), Some(EncodingFormat::Float));
}
#[test]
fn test_encoding_format_parse_base64() {
assert_eq!(
EncodingFormat::parse("base64"),
Some(EncodingFormat::Base64)
);
assert_eq!(
EncodingFormat::parse("BASE64"),
Some(EncodingFormat::Base64)
);
}
#[test]
fn test_encoding_format_parse_invalid() {
assert_eq!(EncodingFormat::parse("binary"), None);
assert_eq!(EncodingFormat::parse(""), None);
}
#[test]
fn test_encoding_format_is_base64() {
assert!(EncodingFormat::Base64.is_base64());
assert!(!EncodingFormat::Float.is_base64());
}
#[test]
fn test_openai_embed_request_deserialize() {
let json = r#"{
"input": "hello world",
"model": "text-embedding-ada-002"
}"#;
let req: OpenAIEmbedRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.model, "text-embedding-ada-002");
assert!(req.input.is_single());
assert_eq!(req.encoding_format, None);
assert_eq!(req.dimensions, None);
assert_eq!(req.user, None);
}
#[test]
fn test_openai_embed_request_deserialize_with_optional() {
let json = r#"{
"input": ["text1", "text2"],
"model": "text-embedding-3-small",
"encoding_format": "base64",
"dimensions": 512,
"user": "user-123"
}"#;
let req: OpenAIEmbedRequest = serde_json::from_str(json).unwrap();
assert!(req.input.is_multiple());
assert_eq!(req.encoding_format, Some("base64".to_string()));
assert_eq!(req.dimensions, Some(512));
assert_eq!(req.user, Some("user-123".to_string()));
}
}