1use async_trait::async_trait;
2use serde_json::{json, Value};
3
4use crate::http::{secret_header, Http};
5use crate::openai::{
6 float_vec, parse_arguments, parse_tool_calls, usage, validate_embed_args, OpenAiProvider,
7};
8use crate::stream::{self, parse_data, Accumulator, ChatStream, StreamEvent};
9use crate::{
10 ChatRequest, ChatResponse, EmbeddingProvider, Embeddings, LlmError, LlmProvider, Message,
11 ModelInfo, Part, Result, Role, StopReason,
12};
13
14#[derive(Clone)]
18pub struct CohereProvider {
19 http: Http,
20 api_key: String,
21 base_url: String,
22}
23
24impl std::fmt::Debug for CohereProvider {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("CohereProvider")
27 .field("api_key", &"<redacted>")
28 .field("base_url", &self.base_url)
29 .field("max_retries", &self.http.max_retries)
30 .finish()
31 }
32}
33
34impl CohereProvider {
35 pub fn new(api_key: impl Into<String>) -> Self {
36 Self {
37 http: Http::default(),
38 api_key: api_key.into(),
39 base_url: "https://api.cohere.com/v2".to_string(),
40 }
41 }
42
43 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
44 self.base_url = base_url.into().trim_end_matches('/').to_string();
45 self
46 }
47
48 pub fn with_client(mut self, client: reqwest::Client) -> Self {
50 self.http.client = client;
51 self
52 }
53
54 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
56 self.http.max_retries = max_retries;
57 self
58 }
59
60 fn build_body(req: &ChatRequest) -> Value {
61 let mut body = OpenAiProvider::build_body(req, true);
62 if let Some(obj) = body.as_object_mut() {
63 if let Some(max_tokens) = obj.remove("max_completion_tokens") {
65 obj.insert("max_tokens".into(), max_tokens);
66 }
67 obj.remove("response_format");
68 obj.remove("reasoning_effort");
69 }
70 if let Some(format) = &req.response_format {
71 body["response_format"] =
72 json!({ "type": "json_object", "json_schema": format.schema });
73 }
74 if req.reasoning.is_some() {
75 body["thinking"] = json!({ "type": "enabled" });
76 }
77 body
78 }
79
80 fn post(&self, path: &str) -> Result<reqwest::RequestBuilder> {
81 Ok(self
82 .http
83 .client
84 .post(format!("{}{path}", self.base_url))
85 .header(
86 reqwest::header::AUTHORIZATION,
87 secret_header("CohereProvider", &format!("Bearer {}", self.api_key))?,
88 ))
89 }
90
91 fn models_url(&self) -> String {
93 match self.base_url.strip_suffix("/v2") {
94 Some(root) => format!("{root}/v1/models?page_size=1000"),
95 None => format!("{}/models?page_size=1000", self.base_url),
96 }
97 }
98}
99
100fn parse_response(v: &Value, requested_model: &str) -> Result<ChatResponse> {
101 let message = &v["message"];
102 if !message.is_object() {
103 return Err(LlmError::InvalidResponse("missing field `message`".into()));
104 }
105
106 let mut parts = Vec::new();
107 for block in message["content"].as_array().into_iter().flatten() {
108 match block["type"].as_str() {
109 Some("text") => {
110 if let Some(text) = block["text"].as_str().filter(|t| !t.is_empty()) {
111 parts.push(Part::text(text));
112 }
113 }
114 Some("thinking") => parts.push(Part::Reasoning {
115 text: block["thinking"].as_str().unwrap_or_default().to_string(),
116 raw: Value::Null,
117 }),
118 _ => {}
119 }
120 }
121 parts.extend(parse_tool_calls(&message["tool_calls"]));
122
123 let stop_reason = match v["finish_reason"].as_str() {
124 Some("COMPLETE" | "STOP_SEQUENCE") => StopReason::EndTurn,
125 Some("TOOL_CALL") => StopReason::ToolUse,
126 Some("MAX_TOKENS") => StopReason::MaxTokens,
127 other => StopReason::Other(other.unwrap_or("unknown").to_string()),
128 };
129
130 Ok(ChatResponse {
131 model: requested_model.to_string(),
133 message: Message::new(Role::Assistant, parts),
134 stop_reason,
135 usage: usage(&v["usage"]["tokens"], "input_tokens", "output_tokens"),
136 })
137}
138
139struct CohereStream {
141 model: String,
142 text: String,
143 thinking: String,
144 calls: Vec<(String, String, String)>,
146 end: Value,
147 done: bool,
148}
149
150impl Accumulator for CohereStream {
151 fn on_event(&mut self, _event: &str, data: &str) -> Result<Vec<StreamEvent>> {
152 let v = parse_data(data)?;
153 let message = &v["delta"]["message"];
154 let mut out = Vec::new();
155 match v["type"].as_str() {
156 Some("content-delta") => {
157 if let Some(text) = message["content"]["text"]
158 .as_str()
159 .filter(|t| !t.is_empty())
160 {
161 self.text.push_str(text);
162 out.push(StreamEvent::TextDelta(text.to_string()));
163 }
164 if let Some(text) = message["content"]["thinking"]
165 .as_str()
166 .filter(|t| !t.is_empty())
167 {
168 self.thinking.push_str(text);
169 out.push(StreamEvent::ReasoningDelta(text.to_string()));
170 }
171 }
172 Some("tool-call-start") => {
173 let call = &message["tool_calls"];
174 let id = call["id"].as_str().unwrap_or_default().to_string();
175 let name = call["function"]["name"]
176 .as_str()
177 .unwrap_or_default()
178 .to_string();
179 let args = call["function"]["arguments"]
180 .as_str()
181 .unwrap_or_default()
182 .to_string();
183 out.push(StreamEvent::ToolCallStart {
184 id: id.clone(),
185 name: name.clone(),
186 });
187 self.calls.push((id, name, args));
188 }
189 Some("tool-call-delta") => {
190 let args = message["tool_calls"]["function"]["arguments"]
191 .as_str()
192 .unwrap_or_default();
193 if let Some(call) = self.calls.last_mut() {
194 call.2.push_str(args);
195 out.push(StreamEvent::ToolCallDelta {
196 id: call.0.clone(),
197 arguments: args.to_string(),
198 });
199 }
200 }
201 Some("message-end") => {
202 self.end = v["delta"].clone();
203 self.done = true;
204 }
205 _ => {}
206 }
207 Ok(out)
208 }
209
210 fn finish(&mut self) -> Result<ChatResponse> {
211 if !self.done {
212 return Err(crate::stream::ended_early());
213 }
214 let mut parts = Vec::new();
215 if !self.thinking.is_empty() {
216 parts.push(Part::Reasoning {
217 text: std::mem::take(&mut self.thinking),
218 raw: Value::Null,
219 });
220 }
221 if !self.text.is_empty() {
222 parts.push(Part::text(std::mem::take(&mut self.text)));
223 }
224 for (id, name, args) in self.calls.drain(..) {
225 parts.push(Part::ToolCall {
226 id,
227 name,
228 arguments: parse_arguments(&args),
229 signature: None,
230 });
231 }
232 let response = parse_response(
233 &json!({ "message": {}, "finish_reason": self.end["finish_reason"], "usage": self.end["usage"] }),
234 &self.model,
235 )?;
236 Ok(ChatResponse {
237 message: Message::new(Role::Assistant, parts),
238 ..response
239 })
240 }
241}
242
243#[async_trait]
244impl LlmProvider for CohereProvider {
245 async fn chat(&self, req: &ChatRequest) -> Result<ChatResponse> {
246 crate::validate(
247 "CohereProvider",
248 &self.api_key,
249 true,
250 &self.base_url,
251 64,
252 req,
253 )?;
254 let builder = self.post("/chat")?.json(&Self::build_body(req));
255 parse_response(&self.http.json(builder).await?, &req.model)
256 }
257
258 async fn chat_stream(&self, req: &ChatRequest) -> Result<ChatStream> {
259 crate::validate(
260 "CohereProvider",
261 &self.api_key,
262 true,
263 &self.base_url,
264 64,
265 req,
266 )?;
267 let mut body = Self::build_body(req);
268 body["stream"] = json!(true);
269 let response = self.http.send(self.post("/chat")?.json(&body)).await?;
270 Ok(stream::drive(
271 response,
272 Box::new(CohereStream {
273 model: req.model.clone(),
274 text: String::new(),
275 thinking: String::new(),
276 calls: Vec::new(),
277 end: Value::Null,
278 done: false,
279 }),
280 ))
281 }
282
283 async fn list_models(&self) -> Result<Vec<ModelInfo>> {
284 crate::validate_endpoint("CohereProvider", &self.api_key, true, &self.base_url)?;
285 let builder = self.http.client.get(self.models_url()).header(
286 reqwest::header::AUTHORIZATION,
287 secret_header("CohereProvider", &format!("Bearer {}", self.api_key))?,
288 );
289 let v = self.http.json(builder).await?;
290 Ok(v["models"]
291 .as_array()
292 .into_iter()
293 .flatten()
294 .filter_map(|m| m["name"].as_str())
295 .map(|name| ModelInfo {
296 id: name.to_string(),
297 display_name: None,
298 })
299 .collect())
300 }
301}
302
303#[async_trait]
304impl EmbeddingProvider for CohereProvider {
305 async fn embed(&self, model: &str, inputs: &[&str]) -> Result<Embeddings> {
306 crate::validate_endpoint("CohereProvider", &self.api_key, true, &self.base_url)?;
307 validate_embed_args(model, inputs)?;
308 let builder = self.post("/embed")?.json(&json!({
309 "model": model,
310 "texts": inputs,
311 "input_type": "search_document",
312 "embedding_types": ["float"],
313 }));
314 let v = self.http.json(builder).await?;
315 Ok(Embeddings {
316 vectors: v["embeddings"]["float"]
317 .as_array()
318 .into_iter()
319 .flatten()
320 .map(float_vec)
321 .collect(),
322 usage: usage(&v["meta"]["billed_units"], "input_tokens", "output_tokens"),
323 })
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::ReasoningEffort;
331
332 #[test]
333 fn builds_cohere_specific_fields() {
334 let req = ChatRequest::new("command-a")
335 .max_tokens(50)
336 .json_schema("answer", json!({"type": "object"}))
337 .reasoning(ReasoningEffort::High)
338 .message(Message::user("hi"));
339 let body = CohereProvider::build_body(&req);
340 assert_eq!(body["max_tokens"], 50);
341 assert!(body.get("max_completion_tokens").is_none());
342 assert!(body.get("reasoning_effort").is_none());
343 assert_eq!(body["response_format"]["type"], "json_object");
344 assert_eq!(body["thinking"]["type"], "enabled");
345 assert_eq!(body["messages"][0]["content"], "hi");
346 }
347
348 #[test]
349 fn parses_tool_call_response() {
350 let v = json!({
351 "finish_reason": "TOOL_CALL",
352 "message": {
353 "role": "assistant",
354 "tool_plan": "I will look up the weather.",
355 "tool_calls": [{
356 "id": "get_weather_abc",
357 "type": "function",
358 "function": { "name": "get_weather", "arguments": "{\"city\":\"Berlin\"}" }
359 }]
360 },
361 "usage": { "tokens": { "input_tokens": 12, "output_tokens": 8 } }
362 });
363 let resp = parse_response(&v, "command-a").unwrap();
364 assert_eq!(resp.stop_reason, StopReason::ToolUse);
365 assert_eq!(resp.tool_calls()[0].arguments["city"], "Berlin");
366 assert_eq!(resp.usage.input_tokens, 12);
367 assert_eq!(resp.model, "command-a");
368 }
369
370 #[test]
371 fn parses_text_response() {
372 let v = json!({
373 "finish_reason": "COMPLETE",
374 "message": { "role": "assistant", "content": [{ "type": "text", "text": "Hello" }] }
375 });
376 let resp = parse_response(&v, "command-a").unwrap();
377 assert_eq!(resp.text(), "Hello");
378 assert_eq!(resp.stop_reason, StopReason::EndTurn);
379 }
380
381 #[test]
382 fn stream_rebuilds_text_and_tool_calls() {
383 let mut acc = CohereStream {
384 model: "command-a".into(),
385 text: String::new(),
386 thinking: String::new(),
387 calls: Vec::new(),
388 end: Value::Null,
389 done: false,
390 };
391 let events = [
392 r#"{"type":"message-start","id":"x"}"#,
393 r#"{"type":"content-delta","index":0,"delta":{"message":{"content":{"text":"Hi"}}}}"#,
394 r#"{"type":"tool-call-start","index":0,"delta":{"message":{"tool_calls":{"id":"c1","type":"function","function":{"name":"f","arguments":""}}}}}"#,
395 r#"{"type":"tool-call-delta","index":0,"delta":{"message":{"tool_calls":{"function":{"arguments":"{\"x\":1}"}}}}}"#,
396 r#"{"type":"message-end","delta":{"finish_reason":"TOOL_CALL","usage":{"tokens":{"input_tokens":3,"output_tokens":5}}}}"#,
397 ];
398 for e in events {
399 acc.on_event("", e).unwrap();
400 }
401 let resp = acc.finish().unwrap();
402 assert_eq!(resp.text(), "Hi");
403 assert_eq!(resp.tool_calls()[0].arguments["x"], 1);
404 assert_eq!(resp.stop_reason, StopReason::ToolUse);
405 assert_eq!(resp.usage.output_tokens, 5);
406 }
407
408 #[test]
409 fn models_use_the_v1_api() {
410 assert_eq!(
411 CohereProvider::new("k").models_url(),
412 "https://api.cohere.com/v1/models?page_size=1000"
413 );
414 }
415}