use crate::{EmbeddingError, Embeddings};
use async_trait::async_trait;
use serde::Deserialize;
pub trait CompatConfigAccess {
fn api_key(&self) -> &str;
fn base_url(&self) -> &str;
fn model(&self) -> &str;
}
pub trait CompatSpec: CompatConfigAccess + Sized + Default {
fn api_key_env() -> &'static str;
fn batch_size() -> usize;
fn dimension_for(model: &str) -> Result<usize, EmbeddingError>;
fn from_env_result() -> Result<Self, String>;
}
pub struct OpenAICompatEmbeddings<C: CompatConfigAccess + CompatSpec> {
config: C,
client: reqwest::Client,
dimension: usize,
}
impl<C: CompatConfigAccess + CompatSpec> std::fmt::Debug for OpenAICompatEmbeddings<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenAICompatEmbeddings")
.field("model", &self.config.model())
.field("dimension", &self.dimension)
.finish()
}
}
impl<C: CompatConfigAccess + CompatSpec> OpenAICompatEmbeddings<C> {
pub fn new(config: C) -> Result<Self, EmbeddingError> {
if config.api_key().trim().is_empty() {
return Err(EmbeddingError::Config(format!(
"{} is empty",
C::api_key_env()
)));
}
let dimension = C::dimension_for(config.model())?;
Ok(Self {
config,
client: reqwest::Client::new(),
dimension,
})
}
pub fn from_env_result() -> Result<Self, String> {
let config = C::from_env_result()?;
Self::new(config).map_err(|e| e.to_string())
}
#[deprecated(
since = "0.7.0",
note = "Use from_env_result() which returns Result<Self, String>"
)]
#[allow(deprecated)]
pub fn from_env() -> Self {
Self::from_env_result()
.unwrap_or_else(|_| Self::new(C::default()).expect("from_env(): missing API key"))
}
}
#[async_trait]
impl<C: CompatConfigAccess + CompatSpec + Send + Sync> Embeddings for OpenAICompatEmbeddings<C> {
async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
if text.trim().is_empty() {
return Err(EmbeddingError::EmptyInput);
}
let url = format!("{}/embeddings", self.config.base_url());
let body = serde_json::json!({
"model": self.config.model(),
"input": text,
});
let response = crate::retry::post_json_with_retry(
&self.client,
&url,
self.config.api_key(),
&body,
&crate::retry::DEFAULT_RETRY,
)
.await
.map_err(|e| EmbeddingError::HttpError(e.to_string()))?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.map_err(|e| {
EmbeddingError::HttpError(format!("failed to read error response body: {e}"))
})?;
return Err(EmbeddingError::ApiError(format!(
"HTTP {}: {}",
status, error_text
)));
}
let embedding_response: EmbeddingResponse = response
.json()
.await
.map_err(|e| EmbeddingError::ParseError(e.to_string()))?;
let mut embedding = embedding_response
.data
.first()
.ok_or_else(|| EmbeddingError::ApiError("No embedding data in response".to_string()))?
.embedding
.clone();
crate::l2_normalize(&mut embedding);
Ok(embedding)
}
async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
if texts.is_empty() {
return Ok(Vec::new());
}
if texts.iter().any(|t| t.trim().is_empty()) {
return Err(EmbeddingError::EmptyInput);
}
let url = format!("{}/embeddings", self.config.base_url());
let batch_size = C::batch_size().max(1);
let mut all_results: Vec<Option<Vec<f32>>> = vec![None; texts.len()];
let mut offset = 0;
for chunk in texts.chunks(batch_size) {
let body = serde_json::json!({
"model": self.config.model(),
"input": chunk,
});
let response = crate::retry::post_json_with_retry(
&self.client,
&url,
self.config.api_key(),
&body,
&crate::retry::DEFAULT_RETRY,
)
.await
.map_err(|e| EmbeddingError::HttpError(e.to_string()))?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await.map_err(|e| {
EmbeddingError::HttpError(format!("failed to read error response body: {e}"))
})?;
return Err(EmbeddingError::ApiError(format!(
"HTTP {}: {}",
status, error_text
)));
}
let embedding_response: EmbeddingResponse = response
.json()
.await
.map_err(|e| EmbeddingError::ParseError(e.to_string()))?;
for item in embedding_response.data {
let global_index = offset + item.index as usize;
if global_index >= all_results.len() {
return Err(EmbeddingError::BatchMismatch {
expected: all_results.len(),
actual: global_index + 1,
});
}
all_results[global_index] = Some(item.embedding);
}
offset += chunk.len();
}
all_results
.into_iter()
.map(|opt| {
let mut v = opt.ok_or(EmbeddingError::EmptyVectorInBatch)?;
crate::l2_normalize(&mut v);
Ok(v)
})
.collect()
}
fn dimension(&self) -> usize {
self.dimension
}
fn model_name(&self) -> &str {
self.config.model()
}
}
#[derive(Debug, Deserialize)]
struct EmbeddingResponse {
data: Vec<EmbeddingData>,
}
#[derive(Debug, Deserialize)]
struct EmbeddingData {
embedding: Vec<f32>,
index: i32,
}