1use crate::error::HostError;
11
12pub trait Embedder: Send {
15 fn dim(&self) -> usize;
18
19 fn embed(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>, HostError>;
26}
27
28#[derive(Clone, Copy, Debug, Default)]
31pub struct NullEmbedder;
32
33impl Embedder for NullEmbedder {
34 fn dim(&self) -> usize {
35 0
36 }
37
38 fn embed(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>, HostError> {
39 Ok(vec![Vec::new(); texts.len()])
40 }
41}
42
43#[derive(Clone)]
56pub struct SharedEmbedder(std::sync::Arc<std::sync::Mutex<Box<dyn Embedder>>>);
57
58impl SharedEmbedder {
59 pub fn new(inner: Box<dyn Embedder>) -> Self {
61 Self(std::sync::Arc::new(std::sync::Mutex::new(inner)))
62 }
63
64 fn inner(&self) -> std::sync::MutexGuard<'_, Box<dyn Embedder>> {
69 self.0.lock().unwrap_or_else(|e| e.into_inner())
70 }
71}
72
73impl std::fmt::Debug for SharedEmbedder {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("SharedEmbedder")
76 .field("dim", &self.dim())
77 .finish()
78 }
79}
80
81impl Embedder for SharedEmbedder {
82 fn dim(&self) -> usize {
83 self.inner().dim()
84 }
85
86 fn embed(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>, HostError> {
87 self.inner().embed(texts)
88 }
89}
90
91#[derive(Debug)]
93pub struct OpenAiCompatEmbedder {
94 url: String,
95 model: String,
96 api_key: Option<String>,
97 dim: usize,
98 agent: ureq::Agent,
99}
100
101impl OpenAiCompatEmbedder {
102 pub fn new(base_url: &str, model: &str, dim: usize) -> Self {
108 Self {
109 url: format!("{}/embeddings", base_url.trim_end_matches('/')),
110 model: model.to_string(),
111 api_key: None,
112 dim,
113 agent: ureq::Agent::new_with_defaults(),
114 }
115 }
116
117 pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
120 self.api_key = Some(key.into());
121 self
122 }
123}
124
125impl Embedder for OpenAiCompatEmbedder {
126 fn dim(&self) -> usize {
127 self.dim
128 }
129
130 fn embed(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>, HostError> {
131 if texts.is_empty() {
132 return Ok(Vec::new());
133 }
134 let body = serde_json::json!({ "model": self.model, "input": texts });
135 let mut request = self.agent.post(&self.url);
136 if let Some(key) = &self.api_key {
137 request = request.header("Authorization", &format!("Bearer {key}"));
138 }
139 let mut response = request
140 .send_json(&body)
141 .map_err(|e| HostError::Embed(format!("request to {}: {e}", self.url)))?;
142 let value: serde_json::Value = response
143 .body_mut()
144 .read_json()
145 .map_err(|e| HostError::Embed(format!("response body: {e}")))?;
146
147 let data = value
151 .get("data")
152 .and_then(|d| d.as_array())
153 .ok_or_else(|| HostError::Embed("response has no data array".into()))?;
154 if data.len() != texts.len() {
155 return Err(HostError::Embed(format!(
156 "expected {} embeddings, got {}",
157 texts.len(),
158 data.len()
159 )));
160 }
161 let mut out = vec![Vec::new(); texts.len()];
162 for item in data {
163 let index = item
164 .get("index")
165 .and_then(|i| i.as_u64())
166 .ok_or_else(|| HostError::Embed("embedding without an index".into()))?
167 as usize;
168 let raw = item
169 .get("embedding")
170 .and_then(|e| e.as_array())
171 .ok_or_else(|| HostError::Embed("embedding is not an array".into()))?;
172 if index >= out.len() || !out[index].is_empty() {
173 return Err(HostError::Embed(format!("bad embedding index {index}")));
174 }
175 if raw.len() != self.dim {
176 return Err(HostError::Embed(format!(
177 "dimension mismatch: server sent {}, configured {}",
178 raw.len(),
179 self.dim
180 )));
181 }
182 let mut v = Vec::with_capacity(raw.len());
183 for x in raw {
184 v.push(
185 x.as_f64().ok_or_else(|| {
186 HostError::Embed("embedding component is not a number".into())
187 })? as f32,
188 );
189 }
190 out[index] = v;
191 }
192 Ok(out)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 struct Counting(usize);
203 impl Embedder for Counting {
204 fn dim(&self) -> usize {
205 3
206 }
207 fn embed(&mut self, texts: &[&str]) -> Result<Vec<Vec<f32>>, HostError> {
208 self.0 += texts.len();
209 Ok(vec![vec![self.0 as f32; 3]; texts.len()])
210 }
211 }
212
213 #[test]
214 fn clones_of_a_shared_embedder_reach_the_same_provider() {
215 let shared = SharedEmbedder::new(Box::new(Counting(0)));
216 let mut a = shared.clone();
217 let mut b = shared.clone();
218
219 assert_eq!(a.dim(), 3);
220 assert_eq!(format!("{shared:?}"), "SharedEmbedder { dim: 3 }");
221
222 assert_eq!(a.embed(&["x"]).unwrap(), vec![vec![1.0; 3]]);
225 assert_eq!(b.embed(&["y", "z"]).unwrap(), vec![vec![3.0; 3]; 2]);
226 }
227
228 #[test]
229 fn a_poisoned_provider_is_recovered_rather_than_propagated() {
230 let shared = SharedEmbedder::new(Box::new(Counting(0)));
231 let poisoner = shared.clone();
232 let _ = std::thread::spawn(move || {
234 let _guard = poisoner.inner();
235 panic!("provider blew up");
236 })
237 .join();
238
239 let mut after = shared.clone();
240 assert_eq!(after.embed(&["still works"]).unwrap().len(), 1);
241 }
242
243 #[test]
244 fn the_null_embedder_produces_one_empty_vector_per_text() {
245 let mut null = NullEmbedder;
246 assert_eq!(null.dim(), 0);
247 assert_eq!(null.embed(&["a", "b"]).unwrap(), vec![Vec::<f32>::new(); 2]);
248 }
249}