1use async_trait::async_trait;
7use serde::Deserialize;
8use serde_json::json;
9
10use crate::{EmbeddingError, Embeddings};
11
12pub const COHERE_EMBED_MODEL: &str = "embed-english-v3.0";
14
15pub const COHERE_EMBED_BASE_URL: &str = "https://api.cohere.com/v2";
17
18#[derive(Debug, Clone, Copy)]
20pub enum CohereEmbedInputType {
21 SearchQuery,
23 SearchDocument,
25 Classification,
27 Clustering,
29}
30
31impl CohereEmbedInputType {
32 fn as_str(&self) -> &'static str {
33 match self {
34 CohereEmbedInputType::SearchQuery => "search_query",
35 CohereEmbedInputType::SearchDocument => "search_document",
36 CohereEmbedInputType::Classification => "classification",
37 CohereEmbedInputType::Clustering => "clustering",
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct CohereEmbeddingsConfig {
45 pub api_key: String,
46 pub base_url: String,
47 pub model: String,
48 pub input_type: CohereEmbedInputType,
49}
50
51impl Default for CohereEmbeddingsConfig {
52 fn default() -> Self {
53 Self {
54 api_key: String::new(),
55 base_url: COHERE_EMBED_BASE_URL.to_string(),
56 model: COHERE_EMBED_MODEL.to_string(),
57 input_type: CohereEmbedInputType::SearchQuery,
58 }
59 }
60}
61
62impl CohereEmbeddingsConfig {
63 pub fn new(api_key: impl Into<String>) -> Self {
65 Self {
66 api_key: api_key.into(),
67 ..Default::default()
68 }
69 }
70
71 pub fn from_env_result() -> Result<Self, String> {
73 let api_key = std::env::var("COHERE_API_KEY")
74 .map_err(|_| "COHERE_API_KEY environment variable not set".to_string())?;
75 let base_url =
76 std::env::var("COHERE_BASE_URL").unwrap_or_else(|_| COHERE_EMBED_BASE_URL.to_string());
77 let model =
78 std::env::var("COHERE_EMBED_MODEL").unwrap_or_else(|_| COHERE_EMBED_MODEL.to_string());
79 Ok(Self {
80 api_key,
81 base_url,
82 model,
83 ..Default::default()
84 })
85 }
86
87 pub fn with_model(mut self, model: impl Into<String>) -> Self {
89 self.model = model.into();
90 self
91 }
92
93 pub fn with_input_type(mut self, input_type: CohereEmbedInputType) -> Self {
95 self.input_type = input_type;
96 self
97 }
98}
99
100#[derive(Debug, Deserialize)]
102struct CohereEmbedResponse {
103 data: Vec<CohereEmbedData>,
104}
105
106#[derive(Debug, Deserialize)]
107struct CohereEmbedData {
108 embedding: Vec<f32>,
109}
110
111pub struct CohereEmbeddings {
113 config: CohereEmbeddingsConfig,
114 client: reqwest::Client,
115 dimension: usize,
116}
117
118impl std::fmt::Debug for CohereEmbeddings {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct("CohereEmbeddings")
121 .field("model", &self.config.model)
122 .finish()
123 }
124}
125
126impl CohereEmbeddings {
127 pub fn new(config: CohereEmbeddingsConfig) -> Result<Self, EmbeddingError> {
133 if config.api_key.trim().is_empty() {
134 return Err(EmbeddingError::Config(
135 "COHERE_API_KEY is empty".to_string(),
136 ));
137 }
138 let dimension = Self::dimension_for(&config.model)?;
139 Ok(Self {
140 config,
141 client: reqwest::Client::new(),
142 dimension,
143 })
144 }
145
146 fn dimension_for(model: &str) -> Result<usize, EmbeddingError> {
148 match model {
149 "embed-english-v3.0" | "embed-multilingual-v3.0" => Ok(1024),
150 other => Err(EmbeddingError::Config(format!(
151 "unknown embedding dimension for Cohere model '{other}' \
152 (supported: 'embed-english-v3.0', 'embed-multilingual-v3.0')"
153 ))),
154 }
155 }
156
157 pub fn from_env_result() -> Result<Self, String> {
159 let config = CohereEmbeddingsConfig::from_env_result()?;
160 Self::new(config).map_err(|e| e.to_string())
161 }
162}
163
164#[async_trait]
165impl Embeddings for CohereEmbeddings {
166 async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
167 if text.trim().is_empty() {
169 return Err(EmbeddingError::EmptyInput);
170 }
171
172 let url = format!("{}/embed", self.config.base_url);
173 let body = json!({
174 "model": self.config.model,
175 "input_type": self.config.input_type.as_str(),
176 "texts": [text],
177 "embedding_types": ["float"],
178 });
179
180 let response = crate::retry::post_json_with_retry(
182 &self.client,
183 &url,
184 &self.config.api_key,
185 &body,
186 &crate::retry::DEFAULT_RETRY,
187 )
188 .await
189 .map_err(|e| EmbeddingError::HttpError(e.to_string()))?;
190
191 let status = response.status();
192 if !status.is_success() {
193 let error_text = response.text().await.map_err(|e| {
195 EmbeddingError::HttpError(format!("failed to read error response body: {e}"))
196 })?;
197 return Err(EmbeddingError::ApiError(format!(
198 "HTTP {}: {}",
199 status, error_text
200 )));
201 }
202
203 let embed_response: CohereEmbedResponse = response
204 .json()
205 .await
206 .map_err(|e| EmbeddingError::ParseError(e.to_string()))?;
207
208 let mut embedding = embed_response
209 .data
210 .first()
211 .map(|d| d.embedding.clone())
212 .ok_or_else(|| EmbeddingError::ApiError("No embedding in response".to_string()))?;
213 crate::l2_normalize(&mut embedding);
215 Ok(embedding)
216 }
217
218 async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
219 if texts.is_empty() {
221 return Ok(Vec::new());
222 }
223 if texts.iter().any(|t| t.trim().is_empty()) {
224 return Err(EmbeddingError::EmptyInput);
225 }
226
227 let url = format!("{}/embed", self.config.base_url);
228 let body = json!({
229 "model": self.config.model,
230 "input_type": CohereEmbedInputType::SearchDocument.as_str(),
231 "texts": texts,
232 "embedding_types": ["float"],
233 });
234
235 let response = crate::retry::post_json_with_retry(
237 &self.client,
238 &url,
239 &self.config.api_key,
240 &body,
241 &crate::retry::DEFAULT_RETRY,
242 )
243 .await
244 .map_err(|e| EmbeddingError::HttpError(e.to_string()))?;
245
246 let status = response.status();
247 if !status.is_success() {
248 let error_text = response.text().await.map_err(|e| {
250 EmbeddingError::HttpError(format!("failed to read error response body: {e}"))
251 })?;
252 return Err(EmbeddingError::ApiError(format!(
253 "HTTP {}: {}",
254 status, error_text
255 )));
256 }
257
258 let embed_response: CohereEmbedResponse = response
259 .json()
260 .await
261 .map_err(|e| EmbeddingError::ParseError(e.to_string()))?;
262
263 let mut embeddings: Vec<Vec<f32>> = embed_response
264 .data
265 .into_iter()
266 .map(|d| d.embedding)
267 .collect();
268
269 if embeddings.len() != texts.len() {
272 return Err(EmbeddingError::BatchMismatch {
273 expected: texts.len(),
274 actual: embeddings.len(),
275 });
276 }
277
278 for v in embeddings.iter_mut() {
280 crate::l2_normalize(v);
281 }
282
283 Ok(embeddings)
284 }
285
286 fn dimension(&self) -> usize {
287 self.dimension
288 }
289
290 fn model_name(&self) -> &str {
291 &self.config.model
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use crate::test_support::{spawn_embeddings_stub, spawn_status_stub};
299 use std::sync::atomic::Ordering;
300 use std::sync::Arc;
301
302 #[tokio::test]
304 async fn test_embed_query_retries_on_429() {
305 let success_body = r#"{"data":[{"embedding":[0.6,0.8]}]}"#;
306 let (base_url, requests) = spawn_status_stub(429, 2, 200, success_body).await;
307 let config = CohereEmbeddingsConfig {
308 api_key: "test-key".into(),
309 base_url,
310 model: COHERE_EMBED_MODEL.into(),
311 input_type: CohereEmbedInputType::SearchQuery,
312 };
313 let embeddings = CohereEmbeddings::new(config).unwrap();
314
315 let v = embeddings
316 .embed_query("hello")
317 .await
318 .expect("429 两次后应重试成功");
319 assert_eq!(v.len(), 2);
320 let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
322 assert!((norm - 1.0).abs() < 1e-5, "norm = {}", norm);
323 assert_eq!(requests.load(Ordering::SeqCst), 3, "1 次初始 + 2 次重试");
324 }
325
326 #[tokio::test]
329 async fn test_embed_documents_truncated_errors() {
330 let base_url = spawn_embeddings_stub(Arc::new(|n| n.saturating_sub(1))).await;
331 let config = CohereEmbeddingsConfig {
332 api_key: "test-key".into(),
333 base_url,
334 model: COHERE_EMBED_MODEL.into(),
335 input_type: CohereEmbedInputType::SearchDocument,
336 };
337 let embeddings = CohereEmbeddings::new(config).unwrap();
338
339 let result = embeddings.embed_documents(&["a", "b"]).await;
340 assert!(
341 matches!(
342 result,
343 Err(EmbeddingError::BatchMismatch {
344 expected: 2,
345 actual: 1
346 })
347 ),
348 "少返回应报 BatchMismatch,实际: {:?}",
349 result
350 );
351 }
352
353 #[test]
354 fn test_config_new() {
355 let config = CohereEmbeddingsConfig::new("test-key");
356 assert_eq!(config.api_key, "test-key");
357 assert_eq!(config.model, COHERE_EMBED_MODEL);
358 }
359
360 #[test]
361 fn test_config_builder() {
362 let config = CohereEmbeddingsConfig::new("key")
363 .with_model("embed-multilingual-v3.0")
364 .with_input_type(CohereEmbedInputType::SearchDocument);
365 assert_eq!(config.model, "embed-multilingual-v3.0");
366 assert!(matches!(
367 config.input_type,
368 CohereEmbedInputType::SearchDocument
369 ));
370 }
371
372 #[test]
373 fn test_input_type_str() {
374 assert_eq!(CohereEmbedInputType::SearchQuery.as_str(), "search_query");
375 assert_eq!(
376 CohereEmbedInputType::SearchDocument.as_str(),
377 "search_document"
378 );
379 assert_eq!(
380 CohereEmbedInputType::Classification.as_str(),
381 "classification"
382 );
383 assert_eq!(CohereEmbedInputType::Clustering.as_str(), "clustering");
384 }
385
386 #[test]
387 fn test_embeddings_new() {
388 let config = CohereEmbeddingsConfig::new("key");
389 let embeddings = CohereEmbeddings::new(config).unwrap();
390 assert_eq!(embeddings.model_name(), COHERE_EMBED_MODEL);
391 assert_eq!(embeddings.dimension(), 1024);
392 }
393
394 #[test]
396 fn test_new_rejects_empty_api_key() {
397 let config = CohereEmbeddingsConfig {
398 api_key: String::new(),
399 base_url: COHERE_EMBED_BASE_URL.into(),
400 model: COHERE_EMBED_MODEL.into(),
401 input_type: CohereEmbedInputType::SearchDocument,
402 };
403 let err = CohereEmbeddings::new(config).unwrap_err();
404 assert!(matches!(err, EmbeddingError::Config(_)));
405 }
406
407 #[test]
409 fn test_new_rejects_unknown_model() {
410 let config = CohereEmbeddingsConfig::new("key").with_model("some-unknown-model");
411 let err = CohereEmbeddings::new(config).unwrap_err();
412 assert!(matches!(err, EmbeddingError::Config(_)));
413 }
414
415 #[tokio::test]
417 async fn test_empty_input_contract() {
418 let embeddings = CohereEmbeddings::new(CohereEmbeddingsConfig::new("key")).unwrap();
419 assert!(matches!(
420 embeddings.embed_query("").await,
421 Err(EmbeddingError::EmptyInput)
422 ));
423 assert!(matches!(
424 embeddings.embed_query(" ").await,
425 Err(EmbeddingError::EmptyInput)
426 ));
427 assert_eq!(
428 embeddings.embed_documents(&[]).await.unwrap(),
429 Vec::<Vec<f32>>::new()
430 );
431 assert!(matches!(
432 embeddings.embed_documents(&["ok", " "]).await,
433 Err(EmbeddingError::EmptyInput)
434 ));
435 }
436}