use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::EmbeddingProvider;
#[derive(Serialize)]
struct EmbedRequest<'a> {
input: &'a str,
model: &'a str,
}
#[derive(Deserialize)]
struct EmbedResponse {
data: Vec<EmbedDatum>,
}
#[derive(Deserialize)]
struct EmbedDatum {
embedding: Vec<f32>,
}
pub struct OpenAiEmbedding {
client: reqwest::Client,
api_base: String,
api_key: String,
model: String,
}
impl OpenAiEmbedding {
pub fn from_env() -> Self {
let api_base = std::env::var("RECURSIVE_API_BASE")
.unwrap_or_else(|_| "https://api.openai.com/v1".into());
let api_key = std::env::var("RECURSIVE_API_KEY").unwrap_or_default();
let model = std::env::var("RECURSIVE_EMBEDDING_MODEL")
.unwrap_or_else(|_| "text-embedding-3-small".into());
Self::new(api_base, api_key, model)
}
pub fn new(
api_base: impl Into<String>,
api_key: impl Into<String>,
model: impl Into<String>,
) -> Self {
Self {
client: reqwest::Client::new(),
api_base: api_base.into(),
api_key: api_key.into(),
model: model.into(),
}
}
}
#[async_trait]
impl EmbeddingProvider for OpenAiEmbedding {
async fn embed(&self, text: &str) -> Vec<f32> {
let url = format!("{}/embeddings", self.api_base.trim_end_matches('/'));
let req = EmbedRequest {
input: text,
model: &self.model,
};
match self
.client
.post(&url)
.bearer_auth(&self.api_key)
.json(&req)
.send()
.await
{
Ok(resp) => match resp.json::<EmbedResponse>().await {
Ok(body) => body
.data
.into_iter()
.next()
.map(|d| d.embedding)
.unwrap_or_default(),
Err(e) => {
tracing::warn!(error = %e, "embedding: failed to parse response");
vec![]
}
},
Err(e) => {
tracing::warn!(error = %e, "embedding: HTTP request failed");
vec![]
}
}
}
}