1use crate::api::{ModelAliasSpec, ModelTask};
2use crate::error::{Result, RuntimeError};
3use crate::provider::remote_common::{RemoteProviderBase, check_http_status, resolve_api_key};
4use crate::traits::{
5 EmbedResult, EmbeddingModel, GenerationOptions, GenerationResult, GeneratorModel, ImageInput,
6 LoadedModelHandle, Message, MessageRole, Modality, ModelProvider, MultimodalBlock,
7 MultimodalEmbeddingModel, MultimodalInput, ProviderCapabilities, ProviderHealth, RerankerModel,
8 ScoredDoc, TokenUsage,
9};
10use async_trait::async_trait;
11use base64::Engine;
12use reqwest::Client;
13use serde_json::json;
14use std::sync::Arc;
15
16pub struct RemoteCohereProvider {
22 base: RemoteProviderBase,
23}
24
25impl Default for RemoteCohereProvider {
26 fn default() -> Self {
27 Self {
28 base: RemoteProviderBase::new(),
29 }
30 }
31}
32
33impl RemoteCohereProvider {
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 #[cfg(test)]
39 fn insert_test_breaker(&self, key: crate::api::ModelRuntimeKey, age: std::time::Duration) {
40 self.base.insert_test_breaker(key, age);
41 }
42
43 #[cfg(test)]
44 fn breaker_count(&self) -> usize {
45 self.base.breaker_count()
46 }
47
48 #[cfg(test)]
49 fn force_cleanup_now_for_test(&self) {
50 self.base.force_cleanup_now_for_test();
51 }
52}
53
54#[async_trait]
55impl ModelProvider for RemoteCohereProvider {
56 fn provider_id(&self) -> &'static str {
57 "remote/cohere"
58 }
59
60 fn capabilities(&self) -> ProviderCapabilities {
61 ProviderCapabilities {
62 supported_tasks: vec![
63 ModelTask::Embed,
64 ModelTask::Generate,
65 ModelTask::Rerank,
66 ModelTask::EmbedMultimodal,
67 ],
68 }
69 }
70
71 async fn load(&self, spec: &ModelAliasSpec) -> Result<LoadedModelHandle> {
72 let cb = self.base.circuit_breaker_for(spec);
73 let api_key = resolve_api_key(&spec.options, "api_key_env", "CO_API_KEY")?;
74
75 let input_type = spec
76 .options
77 .get("input_type")
78 .and_then(|v| v.as_str())
79 .unwrap_or("search_document")
80 .to_string();
81 let embedding_dimensions = spec
82 .options
83 .get("embedding_dimensions")
84 .and_then(|v| v.as_u64())
85 .map(|v| v as u32);
86
87 match spec.task {
88 ModelTask::Embed => {
89 let default_dims = match spec.model_id.as_str() {
90 "embed-english-light-v3.0" | "embed-multilingual-light-v3.0" => 384,
91 "embed-v4.0" => 1536,
92 _ => 1024,
93 };
94 let model = CohereEmbeddingModel {
95 client: self.base.client.clone(),
96 cb: cb.clone(),
97 model_id: spec.model_id.clone(),
98 api_key,
99 input_type,
100 dimensions: embedding_dimensions.unwrap_or(default_dims),
101 };
102 let handle: Arc<dyn EmbeddingModel> = Arc::new(model);
103 Ok(Arc::new(handle) as LoadedModelHandle)
104 }
105 ModelTask::Generate => {
106 let model = CohereGeneratorModel {
107 client: self.base.client.clone(),
108 cb,
109 model_id: spec.model_id.clone(),
110 api_key,
111 };
112 let handle: Arc<dyn GeneratorModel> = Arc::new(model);
113 Ok(Arc::new(handle) as LoadedModelHandle)
114 }
115 ModelTask::Rerank => {
116 let model = CohereRerankerModel {
117 client: self.base.client.clone(),
118 cb,
119 model_id: spec.model_id.clone(),
120 api_key,
121 };
122 let handle: Arc<dyn RerankerModel> = Arc::new(model);
123 Ok(Arc::new(handle) as LoadedModelHandle)
124 }
125 ModelTask::EmbedMultimodal => {
126 let default_dims = match spec.model_id.as_str() {
131 "embed-v4.0" => 1536,
132 _ => 1024,
133 };
134 let model = CohereMultimodalEmbeddingModel {
135 client: self.base.client.clone(),
136 cb,
137 model_id: spec.model_id.clone(),
138 api_key,
139 input_type,
140 dimensions: embedding_dimensions.unwrap_or(default_dims),
141 };
142 let handle: Arc<dyn MultimodalEmbeddingModel> = Arc::new(model);
143 Ok(Arc::new(handle) as LoadedModelHandle)
144 }
145 task => Err(RuntimeError::CapabilityMismatch(format!(
146 "Cohere provider does not support task {:?}",
147 task
148 ))),
149 }
150 }
151
152 async fn health(&self) -> ProviderHealth {
153 ProviderHealth::Healthy
154 }
155}
156
157struct CohereEmbeddingModel {
158 client: Client,
159 cb: crate::reliability::CircuitBreakerWrapper,
160 model_id: String,
161 api_key: String,
162 input_type: String,
163 dimensions: u32,
164}
165
166#[async_trait]
167impl EmbeddingModel for CohereEmbeddingModel {
168 async fn embed(&self, texts: &[&str]) -> Result<EmbedResult> {
169 let texts: Vec<String> = texts.iter().map(|s| s.to_string()).collect();
170
171 self.cb
172 .call(move || async move {
173 let response = self
174 .client
175 .post("https://api.cohere.com/v2/embed")
176 .header("Authorization", format!("Bearer {}", self.api_key))
177 .json(&json!({
178 "texts": texts,
179 "model": self.model_id,
180 "input_type": self.input_type,
181 "embedding_types": ["float"]
182 }))
183 .send()
184 .await
185 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
186
187 let body: serde_json::Value = check_http_status("Cohere", response)?
188 .json()
189 .await
190 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
191
192 let float_embeddings = body
193 .get("embeddings")
194 .and_then(|e| e.get("float"))
195 .and_then(|f| f.as_array())
196 .ok_or_else(|| {
197 RuntimeError::ApiError(
198 "Invalid Cohere embedding response format".to_string(),
199 )
200 })?;
201
202 let mut vectors = Vec::new();
203 for embedding in float_embeddings {
204 if let Some(values) = embedding.as_array() {
205 let vec: Vec<f32> = values
206 .iter()
207 .filter_map(|v| v.as_f64().map(|f| f as f32))
208 .collect();
209 vectors.push(vec);
210 }
211 }
212
213 let usage = body
216 .get("meta")
217 .and_then(|m| m.get("billed_units"))
218 .and_then(|b| b.get("input_tokens"))
219 .and_then(|t| t.as_u64())
220 .map(|tokens| TokenUsage {
221 prompt_tokens: tokens as usize,
222 completion_tokens: 0,
223 total_tokens: tokens as usize,
224 });
225
226 Ok(EmbedResult { vectors, usage })
227 })
228 .await
229 }
230
231 fn dimensions(&self) -> u32 {
232 self.dimensions
233 }
234}
235
236impl crate::traits::ModelInfo for CohereEmbeddingModel {
237 fn model_id(&self) -> &str {
238 &self.model_id
239 }
240}
241
242struct CohereGeneratorModel {
243 client: Client,
244 cb: crate::reliability::CircuitBreakerWrapper,
245 model_id: String,
246 api_key: String,
247}
248
249impl crate::traits::ModelInfo for CohereGeneratorModel {
250 fn model_id(&self) -> &str {
251 &self.model_id
252 }
253}
254
255#[async_trait]
256impl GeneratorModel for CohereGeneratorModel {
257 async fn generate(
258 &self,
259 messages: &[Message],
260 options: GenerationOptions,
261 ) -> Result<GenerationResult> {
262 let messages: Vec<serde_json::Value> = messages
263 .iter()
264 .map(|msg| {
265 let role = match msg.role {
266 MessageRole::System => "system",
267 MessageRole::User => "user",
268 MessageRole::Assistant => "assistant",
269 };
270 json!({ "role": role, "content": msg.text() })
271 })
272 .collect();
273
274 self.cb
275 .call(move || async move {
276 let mut body = json!({
277 "model": self.model_id,
278 "messages": messages,
279 });
280
281 if let Some(max_tokens) = options.max_tokens {
282 body["max_tokens"] = json!(max_tokens);
283 }
284 if let Some(temperature) = options.temperature {
285 body["temperature"] = json!(temperature);
286 }
287 if let Some(top_p) = options.top_p {
288 body["p"] = json!(top_p);
289 }
290
291 let response = self
292 .client
293 .post("https://api.cohere.com/v2/chat")
294 .header("Authorization", format!("Bearer {}", self.api_key))
295 .json(&body)
296 .send()
297 .await
298 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
299
300 let body: serde_json::Value = check_http_status("Cohere", response)?
301 .json()
302 .await
303 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
304
305 let text = body
306 .get("message")
307 .and_then(|m| m.get("content"))
308 .and_then(|c| c.as_array())
309 .and_then(|arr| arr.first())
310 .and_then(|item| item.get("text"))
311 .and_then(|t| t.as_str())
312 .unwrap_or("")
313 .to_string();
314
315 let usage = body.get("usage").map(|u| {
316 let input = u
317 .get("tokens")
318 .and_then(|t| t.get("input_tokens"))
319 .and_then(|v| v.as_u64())
320 .unwrap_or(0);
321 let output = u
322 .get("tokens")
323 .and_then(|t| t.get("output_tokens"))
324 .and_then(|v| v.as_u64())
325 .unwrap_or(0);
326 TokenUsage {
327 prompt_tokens: input as usize,
328 completion_tokens: output as usize,
329 total_tokens: (input + output) as usize,
330 }
331 });
332
333 Ok(GenerationResult {
334 text,
335 usage,
336 images: vec![],
337 audio: None,
338 })
339 })
340 .await
341 }
342}
343
344struct CohereRerankerModel {
345 client: Client,
346 cb: crate::reliability::CircuitBreakerWrapper,
347 model_id: String,
348 api_key: String,
349}
350
351impl crate::traits::ModelInfo for CohereRerankerModel {
352 fn model_id(&self) -> &str {
353 &self.model_id
354 }
355}
356
357#[async_trait]
358impl RerankerModel for CohereRerankerModel {
359 async fn rerank(&self, query: &str, docs: &[&str]) -> Result<Vec<ScoredDoc>> {
360 let query = query.to_string();
361 let docs: Vec<String> = docs.iter().map(|s| s.to_string()).collect();
362
363 self.cb
364 .call(move || async move {
365 let response = self
366 .client
367 .post("https://api.cohere.com/v2/rerank")
368 .header("Authorization", format!("Bearer {}", self.api_key))
369 .json(&json!({
370 "query": query,
371 "documents": docs,
372 "model": self.model_id,
373 }))
374 .send()
375 .await
376 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
377
378 let body: serde_json::Value = check_http_status("Cohere", response)?
379 .json()
380 .await
381 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
382
383 let results_json =
384 body.get("results")
385 .and_then(|r| r.as_array())
386 .ok_or_else(|| {
387 RuntimeError::ApiError("Invalid rerank response format".to_string())
388 })?;
389
390 let mut results = Vec::new();
391 for item in results_json {
392 let index = item.get("index").and_then(|i| i.as_u64()).unwrap_or(0) as usize;
393 let score = item
394 .get("relevance_score")
395 .and_then(|s| s.as_f64())
396 .unwrap_or(0.0) as f32;
397 results.push(ScoredDoc {
398 index,
399 score,
400 text: None,
401 });
402 }
403 Ok(results)
404 })
405 .await
406 }
407}
408
409struct CohereMultimodalEmbeddingModel {
410 client: Client,
411 cb: crate::reliability::CircuitBreakerWrapper,
412 model_id: String,
413 api_key: String,
414 input_type: String,
415 dimensions: u32,
416}
417
418impl CohereMultimodalEmbeddingModel {
419 fn input_to_json(input: &MultimodalInput) -> Result<serde_json::Value> {
423 let mut parts = Vec::with_capacity(input.blocks.len());
424 for block in &input.blocks {
425 match block {
426 MultimodalBlock::Text(text) => {
427 parts.push(json!({ "type": "text", "text": text }));
428 }
429 MultimodalBlock::Image(ImageInput::Url(url)) => {
430 parts.push(json!({
431 "type": "image_url",
432 "image_url": { "url": url }
433 }));
434 }
435 MultimodalBlock::Image(ImageInput::Bytes { data, media_type }) => {
436 let b64 = base64::engine::general_purpose::STANDARD.encode(data);
437 let data_url = format!("data:{media_type};base64,{b64}");
438 parts.push(json!({
439 "type": "image_url",
440 "image_url": { "url": data_url }
441 }));
442 }
443 MultimodalBlock::Audio(_) => {
444 return Err(RuntimeError::ApiError(
445 "Cohere Embed v4 does not support audio inputs".to_string(),
446 ));
447 }
448 }
449 }
450 Ok(json!({ "content": parts }))
451 }
452}
453
454#[async_trait]
455impl MultimodalEmbeddingModel for CohereMultimodalEmbeddingModel {
456 async fn embed(&self, inputs: Vec<MultimodalInput>) -> Result<EmbedResult> {
457 let body_inputs: Result<Vec<serde_json::Value>> =
458 inputs.iter().map(Self::input_to_json).collect();
459 let body_inputs = body_inputs?;
460 let model = self.model_id.clone();
461 let input_type = self.input_type.clone();
462 let api_key = self.api_key.clone();
463 let client = self.client.clone();
464
465 self.cb
466 .call(move || async move {
467 let response = client
468 .post("https://api.cohere.com/v2/embed")
469 .header("Authorization", format!("Bearer {api_key}"))
470 .json(&json!({
471 "model": model,
472 "input_type": input_type,
473 "embedding_types": ["float"],
474 "inputs": body_inputs,
475 }))
476 .send()
477 .await
478 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
479
480 let body: serde_json::Value = check_http_status("Cohere", response)?
481 .json()
482 .await
483 .map_err(|e| RuntimeError::ApiError(e.to_string()))?;
484
485 let float_embeddings = body
486 .get("embeddings")
487 .and_then(|e| e.get("float"))
488 .and_then(|f| f.as_array())
489 .ok_or_else(|| {
490 RuntimeError::ApiError(
491 "Invalid Cohere multimodal embedding response format".to_string(),
492 )
493 })?;
494
495 let mut vectors = Vec::with_capacity(float_embeddings.len());
496 for embedding in float_embeddings {
497 if let Some(values) = embedding.as_array() {
498 let vec: Vec<f32> = values
499 .iter()
500 .filter_map(|v| v.as_f64().map(|f| f as f32))
501 .collect();
502 vectors.push(vec);
503 }
504 }
505
506 let usage = body
508 .get("meta")
509 .and_then(|m| m.get("billed_units"))
510 .and_then(|b| b.get("input_tokens"))
511 .and_then(|t| t.as_u64())
512 .map(|tokens| TokenUsage {
513 prompt_tokens: tokens as usize,
514 completion_tokens: 0,
515 total_tokens: tokens as usize,
516 });
517
518 Ok(EmbedResult { vectors, usage })
519 })
520 .await
521 }
522
523 fn dimensions(&self) -> u32 {
524 self.dimensions
525 }
526
527 fn supported_modalities(&self) -> &[Modality] {
528 const COHERE_V4_MODALITIES: &[Modality] = &[Modality::Text, Modality::Image];
530 COHERE_V4_MODALITIES
531 }
532}
533
534impl crate::traits::ModelInfo for CohereMultimodalEmbeddingModel {
535 fn model_id(&self) -> &str {
536 &self.model_id
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::api::ModelRuntimeKey;
544 use crate::provider::remote_common::RemoteProviderBase;
545 use crate::traits::ModelProvider;
546 use std::time::Duration;
547
548 static ENV_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
549
550 fn spec(alias: &str, task: ModelTask, model_id: &str) -> ModelAliasSpec {
551 ModelAliasSpec {
552 alias: alias.to_string(),
553 task,
554 provider_id: "remote/cohere".to_string(),
555 model_id: model_id.to_string(),
556 revision: None,
557 warmup: crate::api::WarmupPolicy::Lazy,
558 required: false,
559 timeout: None,
560 load_timeout: None,
561 retry: None,
562 options: serde_json::Value::Null,
563 }
564 }
565
566 #[tokio::test]
567 async fn breaker_reused_for_same_runtime_key() {
568 let _lock = ENV_LOCK.lock().await;
569 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
570
571 let provider = RemoteCohereProvider::new();
572 let s1 = spec("embed/a", ModelTask::Embed, "embed-english-v3.0");
573 let s2 = spec("embed/b", ModelTask::Embed, "embed-english-v3.0");
574
575 let _ = provider.load(&s1).await.unwrap();
576 let _ = provider.load(&s2).await.unwrap();
577
578 assert_eq!(provider.breaker_count(), 1);
579
580 unsafe { std::env::remove_var("CO_API_KEY") };
581 }
582
583 #[tokio::test]
584 async fn breaker_isolated_by_task_and_model() {
585 let _lock = ENV_LOCK.lock().await;
586 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
587
588 let provider = RemoteCohereProvider::new();
589 let embed = spec("embed/a", ModelTask::Embed, "embed-english-v3.0");
590 let gen_spec = spec("chat/a", ModelTask::Generate, "command-r-plus");
591 let rerank = spec("rerank/a", ModelTask::Rerank, "rerank-english-v3.0");
592
593 let _ = provider.load(&embed).await.unwrap();
594 let _ = provider.load(&gen_spec).await.unwrap();
595 let _ = provider.load(&rerank).await.unwrap();
596
597 assert_eq!(provider.breaker_count(), 3);
598
599 unsafe { std::env::remove_var("CO_API_KEY") };
600 }
601
602 #[tokio::test]
603 async fn breaker_cleanup_evicts_stale_entries() {
604 let _lock = ENV_LOCK.lock().await;
605 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
606
607 let provider = RemoteCohereProvider::new();
608 let stale = spec("embed/stale", ModelTask::Embed, "embed-english-v3.0");
609 let fresh = spec("chat/fresh", ModelTask::Generate, "command-r-plus");
610 provider.insert_test_breaker(
611 ModelRuntimeKey::new(&stale),
612 RemoteProviderBase::BREAKER_TTL + Duration::from_secs(5),
613 );
614 provider.insert_test_breaker(ModelRuntimeKey::new(&fresh), Duration::from_secs(1));
615 assert_eq!(provider.breaker_count(), 2);
616
617 provider.force_cleanup_now_for_test();
618 let _ = provider.load(&fresh).await.unwrap();
619
620 assert_eq!(provider.breaker_count(), 1);
621
622 unsafe { std::env::remove_var("CO_API_KEY") };
623 }
624
625 #[tokio::test]
626 async fn supports_all_three_tasks() {
627 let _lock = ENV_LOCK.lock().await;
628 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
629
630 let provider = RemoteCohereProvider::new();
631
632 let embed = spec("embed/a", ModelTask::Embed, "embed-english-v3.0");
633 assert!(provider.load(&embed).await.is_ok());
634
635 let gen_spec = spec("gen/a", ModelTask::Generate, "command-r-plus");
636 assert!(provider.load(&gen_spec).await.is_ok());
637
638 let rerank = spec("rerank/a", ModelTask::Rerank, "rerank-english-v3.0");
639 assert!(provider.load(&rerank).await.is_ok());
640
641 unsafe { std::env::remove_var("CO_API_KEY") };
642 }
643
644 #[tokio::test]
645 async fn rejects_raw_task() {
646 let _lock = ENV_LOCK.lock().await;
647 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
648
649 let provider = RemoteCohereProvider::new();
650 let raw = spec("raw/a", ModelTask::Raw, "embed-english-v3.0");
651
652 let err = provider.load(&raw).await.unwrap_err();
653 assert!(matches!(err, RuntimeError::CapabilityMismatch(_)));
654 assert!(err.to_string().contains("does not support task Raw"));
655
656 unsafe { std::env::remove_var("CO_API_KEY") };
657 }
658
659 #[tokio::test]
660 async fn default_embedding_dimensions() {
661 let _lock = ENV_LOCK.lock().await;
662 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
663
664 let provider = RemoteCohereProvider::new();
665 let s = spec("embed/dim", ModelTask::Embed, "embed-english-v3.0");
666
667 let handle = provider.load(&s).await.unwrap();
668 let model = handle.downcast_ref::<Arc<dyn EmbeddingModel>>().unwrap();
669 assert_eq!(model.dimensions(), 1024);
670
671 unsafe { std::env::remove_var("CO_API_KEY") };
672 }
673
674 #[tokio::test]
675 async fn custom_embedding_dimensions() {
676 let _lock = ENV_LOCK.lock().await;
677 unsafe { std::env::set_var("CO_API_KEY", "test-key") };
678
679 let provider = RemoteCohereProvider::new();
680 let mut s = spec("embed/dim-custom", ModelTask::Embed, "embed-english-v3.0");
681 s.options = serde_json::json!({"embedding_dimensions": 256});
682
683 let handle = provider.load(&s).await.unwrap();
684 let model = handle.downcast_ref::<Arc<dyn EmbeddingModel>>().unwrap();
685 assert_eq!(model.dimensions(), 256);
686
687 unsafe { std::env::remove_var("CO_API_KEY") };
688 }
689}