rig_core/providers/cohere/
embeddings.rs1use super::{client::ApiResponse, client::Client};
2use crate::{
3 embeddings::{self, EmbeddingError},
4 http_client::HttpClientExt,
5 wasm_compat::*,
6};
7use serde::Deserialize;
8use serde_json::json;
9
10#[derive(Deserialize)]
11pub struct EmbeddingResponse {
12 #[serde(default)]
13 pub response_type: Option<String>,
14 pub id: String,
15 pub embeddings: Vec<Vec<serde_json::Number>>,
16 pub texts: Vec<String>,
17 #[serde(default)]
18 pub meta: Option<Meta>,
19}
20
21#[derive(Deserialize)]
22pub struct Meta {
23 pub api_version: ApiVersion,
24 pub billed_units: BilledUnits,
25 #[serde(default)]
26 pub warnings: Vec<String>,
27}
28
29#[derive(Deserialize)]
30pub struct ApiVersion {
31 pub version: String,
32 #[serde(default)]
33 pub is_deprecated: Option<bool>,
34 #[serde(default)]
35 pub is_experimental: Option<bool>,
36}
37
38#[derive(Deserialize, Debug)]
39pub struct BilledUnits {
40 #[serde(default)]
41 pub input_tokens: u32,
42 #[serde(default)]
43 pub output_tokens: u32,
44 #[serde(default)]
45 pub search_units: u32,
46 #[serde(default)]
47 pub classifications: u32,
48}
49
50impl std::fmt::Display for BilledUnits {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(
53 f,
54 "Input tokens: {}\nOutput tokens: {}\nSearch units: {}\nClassifications: {}",
55 self.input_tokens, self.output_tokens, self.search_units, self.classifications
56 )
57 }
58}
59
60#[derive(Clone)]
61pub struct EmbeddingModel<T = reqwest::Client> {
62 client: Client<T>,
63 pub model: String,
64 pub input_type: String,
65 ndims: usize,
66}
67
68impl<T> embeddings::EmbeddingModel for EmbeddingModel<T>
69where
70 T: HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
71{
72 const MAX_DOCUMENTS: usize = 96;
73 type Client = Client<T>;
74
75 fn make(client: &Self::Client, model: impl Into<String>, dims: Option<usize>) -> Self {
76 let model = model.into();
77 let dims = dims
78 .or(super::model_dimensions_from_identifier(&model))
79 .unwrap_or_default();
80
81 Self::new(client.clone(), model, "search_document", dims)
82 }
83
84 fn ndims(&self) -> usize {
85 self.ndims
86 }
87
88 async fn embed_texts(
89 &self,
90 documents: impl IntoIterator<Item = String>,
91 ) -> Result<Vec<embeddings::Embedding>, EmbeddingError> {
92 let documents = documents.into_iter().collect::<Vec<_>>();
93
94 let body = json!({
95 "model": self.model.to_string(),
96 "texts": documents,
97 "input_type": self.input_type
98 });
99
100 let body = serde_json::to_vec(&body)?;
101
102 let req = self
103 .client
104 .post("/v1/embed")?
105 .body(body)
106 .map_err(|e| EmbeddingError::HttpError(e.into()))?;
107
108 let response = self
109 .client
110 .send::<_, Vec<u8>>(req)
111 .await
112 .map_err(EmbeddingError::HttpError)?;
113
114 let status = response.status();
115 let raw_body = response.into_body().await?;
116
117 if status.is_success() {
118 let body: ApiResponse<EmbeddingResponse> = serde_json::from_slice(raw_body.as_slice())?;
119
120 match body {
121 ApiResponse::Ok(response) => {
122 match response.meta {
123 Some(meta) => tracing::info!(target: "rig",
124 "Cohere embeddings billed units: {}",
125 meta.billed_units,
126 ),
127 None => tracing::info!(target: "rig",
128 "Cohere embeddings billed units: n/a",
129 ),
130 };
131
132 if response.embeddings.len() != documents.len() {
133 return Err(EmbeddingError::DocumentError(
134 format!(
135 "Expected {} embeddings, got {}",
136 documents.len(),
137 response.embeddings.len()
138 )
139 .into(),
140 ));
141 }
142
143 Ok(response
144 .embeddings
145 .into_iter()
146 .zip(documents.into_iter())
147 .map(|(embedding, document)| embeddings::Embedding {
148 document,
149 vec: embedding.into_iter().filter_map(|n| n.as_f64()).collect(),
150 })
151 .collect())
152 }
153 ApiResponse::Err(error) => {
154 tracing::warn!(
155 message = %error.message,
156 "Cohere returned an error response"
157 );
158 Err(EmbeddingError::from_http_response(
159 status,
160 String::from_utf8_lossy(&raw_body),
161 ))
162 }
163 }
164 } else {
165 Err(EmbeddingError::from_http_response(
166 status,
167 String::from_utf8_lossy(&raw_body),
168 ))
169 }
170 }
171}
172
173impl<T> EmbeddingModel<T> {
174 pub fn new(
175 client: Client<T>,
176 model: impl Into<String>,
177 input_type: &str,
178 ndims: usize,
179 ) -> Self {
180 Self {
181 client,
182 model: model.into(),
183 input_type: input_type.to_string(),
184 ndims,
185 }
186 }
187
188 pub fn with_model(client: Client<T>, model: &str, input_type: &str, ndims: usize) -> Self {
189 Self {
190 client,
191 model: model.into(),
192 input_type: input_type.into(),
193 ndims,
194 }
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[tokio::test]
203 async fn embeddings_non_success_preserves_status_and_body() {
204 use crate::embeddings::EmbeddingModel as _;
205 use crate::test_utils::RecordingHttpClient;
206
207 let body = r#"{"error":{"message":"boom"}}"#;
208 let http_client =
209 RecordingHttpClient::with_error_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
210 let client = crate::providers::cohere::Client::builder()
211 .api_key("test-key")
212 .http_client(http_client)
213 .build()
214 .expect("build client");
215 let model = client.embedding_model(
216 crate::providers::cohere::EMBED_ENGLISH_V3,
217 "search_document",
218 );
219
220 let error = model
221 .embed_texts(["hello".to_string()])
222 .await
223 .expect_err("should fail with non-success status");
224
225 assert!(matches!(error, EmbeddingError::HttpError(_)));
226 assert_eq!(
227 error.provider_response_status(),
228 Some(http::StatusCode::SERVICE_UNAVAILABLE)
229 );
230 assert_eq!(error.provider_response_body(), Some(body));
231 }
232
233 #[tokio::test]
234 async fn embeddings_2xx_error_envelope_preserves_status_and_body() {
235 use crate::embeddings::EmbeddingModel as _;
236 use crate::test_utils::RecordingHttpClient;
237
238 let body = r#"{"message":"boom"}"#;
240 let http_client = RecordingHttpClient::new(body);
241 let client = crate::providers::cohere::Client::builder()
242 .api_key("test-key")
243 .http_client(http_client)
244 .build()
245 .expect("build client");
246 let model = client.embedding_model(
247 crate::providers::cohere::EMBED_ENGLISH_V3,
248 "search_document",
249 );
250
251 let error = model
252 .embed_texts(["hello".to_string()])
253 .await
254 .expect_err("should fail with provider error envelope");
255
256 match &error {
257 EmbeddingError::ProviderResponse(stored) => {
258 assert_eq!(stored.body, body);
259 assert_eq!(stored.status, Some(http::StatusCode::OK));
260 }
261 other => panic!("expected ProviderResponse, got {other:?}"),
262 }
263 }
264}