lc_embeddings/
openai_compat.rs1use crate::{EmbeddingError, Embeddings};
9use async_trait::async_trait;
10use serde::Deserialize;
11
12pub trait CompatConfigAccess {
14 fn api_key(&self) -> &str;
15 fn base_url(&self) -> &str;
16 fn model(&self) -> &str;
17}
18
19pub trait CompatSpec: CompatConfigAccess + Sized + Default {
24 fn api_key_env() -> &'static str;
26 fn batch_size() -> usize;
28 fn dimension_for(model: &str) -> Result<usize, EmbeddingError>;
30 fn from_env_result() -> Result<Self, String>;
32}
33
34pub struct OpenAICompatEmbeddings<C: CompatConfigAccess + CompatSpec> {
39 config: C,
40 client: reqwest::Client,
41 dimension: usize,
42}
43
44impl<C: CompatConfigAccess + CompatSpec> std::fmt::Debug for OpenAICompatEmbeddings<C> {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("OpenAICompatEmbeddings")
47 .field("model", &self.config.model())
48 .field("dimension", &self.dimension)
49 .finish()
50 }
51}
52
53impl<C: CompatConfigAccess + CompatSpec> OpenAICompatEmbeddings<C> {
54 pub fn new(config: C) -> Result<Self, EmbeddingError> {
57 if config.api_key().trim().is_empty() {
58 return Err(EmbeddingError::Config(format!(
59 "{} is empty",
60 C::api_key_env()
61 )));
62 }
63 let dimension = C::dimension_for(config.model())?;
64 Ok(Self {
65 config,
66 client: reqwest::Client::new(),
67 dimension,
68 })
69 }
70
71 pub fn from_env_result() -> Result<Self, String> {
73 let config = C::from_env_result()?;
74 Self::new(config).map_err(|e| e.to_string())
75 }
76}
77
78#[async_trait]
79impl<C: CompatConfigAccess + CompatSpec + Send + Sync> Embeddings for OpenAICompatEmbeddings<C> {
80 async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
81 if text.trim().is_empty() {
82 return Err(EmbeddingError::EmptyInput);
83 }
84
85 let url = format!("{}/embeddings", self.config.base_url());
86
87 let body = serde_json::json!({
88 "model": self.config.model(),
89 "input": text,
90 });
91
92 let response = crate::retry::post_json_with_retry(
94 &self.client,
95 &url,
96 self.config.api_key(),
97 &body,
98 &crate::retry::DEFAULT_RETRY,
99 )
100 .await
101 .map_err(|e| EmbeddingError::HttpError(e.to_string()))?;
102
103 let status = response.status();
104 if !status.is_success() {
105 let error_text = response.text().await.map_err(|e| {
107 EmbeddingError::HttpError(format!("failed to read error response body: {e}"))
108 })?;
109 return Err(EmbeddingError::ApiError(format!(
110 "HTTP {}: {}",
111 status, error_text
112 )));
113 }
114
115 let embedding_response: EmbeddingResponse = response
116 .json()
117 .await
118 .map_err(|e| EmbeddingError::ParseError(e.to_string()))?;
119
120 let mut embedding = embedding_response
121 .data
122 .first()
123 .ok_or_else(|| EmbeddingError::ApiError("No embedding data in response".to_string()))?
124 .embedding
125 .clone();
126 crate::l2_normalize(&mut embedding);
128 Ok(embedding)
129 }
130
131 async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
132 if texts.is_empty() {
134 return Ok(Vec::new());
135 }
136 if texts.iter().any(|t| t.trim().is_empty()) {
137 return Err(EmbeddingError::EmptyInput);
138 }
139
140 let url = format!("{}/embeddings", self.config.base_url());
141 let batch_size = C::batch_size().max(1);
142 let mut all_results: Vec<Option<Vec<f32>>> = vec![None; texts.len()];
145 let mut offset = 0;
146
147 for chunk in texts.chunks(batch_size) {
148 let body = serde_json::json!({
149 "model": self.config.model(),
150 "input": chunk,
151 });
152
153 let response = crate::retry::post_json_with_retry(
155 &self.client,
156 &url,
157 self.config.api_key(),
158 &body,
159 &crate::retry::DEFAULT_RETRY,
160 )
161 .await
162 .map_err(|e| EmbeddingError::HttpError(e.to_string()))?;
163
164 let status = response.status();
165 if !status.is_success() {
166 let error_text = response.text().await.map_err(|e| {
168 EmbeddingError::HttpError(format!("failed to read error response body: {e}"))
169 })?;
170 return Err(EmbeddingError::ApiError(format!(
171 "HTTP {}: {}",
172 status, error_text
173 )));
174 }
175
176 let embedding_response: EmbeddingResponse = response
177 .json()
178 .await
179 .map_err(|e| EmbeddingError::ParseError(e.to_string()))?;
180
181 for item in embedding_response.data {
182 let global_index = offset + item.index as usize;
183 if global_index >= all_results.len() {
184 return Err(EmbeddingError::BatchMismatch {
186 expected: all_results.len(),
187 actual: global_index + 1,
188 });
189 }
190 all_results[global_index] = Some(item.embedding);
191 }
192 offset += chunk.len();
193 }
194
195 all_results
197 .into_iter()
198 .map(|opt| {
199 let mut v = opt.ok_or(EmbeddingError::EmptyVectorInBatch)?;
200 crate::l2_normalize(&mut v);
201 Ok(v)
202 })
203 .collect()
204 }
205
206 fn dimension(&self) -> usize {
207 self.dimension
208 }
209
210 fn model_name(&self) -> &str {
211 self.config.model()
212 }
213}
214
215#[derive(Debug, Deserialize)]
217struct EmbeddingResponse {
218 data: Vec<EmbeddingData>,
219}
220
221#[derive(Debug, Deserialize)]
222struct EmbeddingData {
223 embedding: Vec<f32>,
224 index: i32,
225}