1use crate::{
2 completion::{self, CompletionError},
3 http_client::HttpClientExt,
4 json_utils,
5 message::{self, Reasoning, ToolChoice},
6 providers::internal::{completion_send::send_completion, envelope::DirectPayload},
7 telemetry::{CompletionOperation, CompletionSpanBuilder, SpanCombinator},
8};
9use std::collections::HashMap;
10
11use super::client::Client;
12use crate::completion::CompletionRequest;
13use serde::{Deserialize, Serialize};
14use tracing::Instrument;
15
16pub(crate) const PROVIDER_NAME: &str = "cohere";
19
20#[derive(Debug, Deserialize, Serialize)]
21pub struct CompletionResponse {
22 pub id: String,
23 pub finish_reason: FinishReason,
24 message: Message,
25 #[serde(default)]
26 pub usage: Option<Usage>,
27}
28
29type AssistantMessageParts = (Vec<AssistantContent>, Vec<Citation>, Vec<ToolCall>);
30
31impl CompletionResponse {
32 pub fn message(&self) -> Result<AssistantMessageParts, CompletionError> {
34 let Message::Assistant {
35 content,
36 citations,
37 tool_calls,
38 ..
39 } = self.message.clone()
40 else {
41 return Err(CompletionError::ResponseError(
42 "completion response did not contain an assistant message".into(),
43 ));
44 };
45
46 Ok((content, citations, tool_calls))
47 }
48}
49
50impl crate::telemetry::ProviderResponseExt for CompletionResponse {
51 type Usage = Usage;
52
53 fn get_response_id(&self) -> Option<String> {
54 Some(self.id.clone())
55 }
56
57 fn get_response_model_name(&self) -> Option<String> {
58 None
59 }
60
61 fn get_text_response(&self) -> Option<String> {
62 let Message::Assistant { ref content, .. } = self.message else {
63 return None;
64 };
65
66 let res = content
67 .iter()
68 .filter_map(|x| {
69 if let AssistantContent::Text { text } = x {
70 Some(text.to_string())
71 } else {
72 None
73 }
74 })
75 .collect::<Vec<String>>()
76 .join("\n");
77
78 if res.is_empty() { None } else { Some(res) }
79 }
80
81 fn get_usage(&self) -> Option<Self::Usage> {
82 self.usage.clone()
83 }
84}
85
86#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Serialize)]
87#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
88pub enum FinishReason {
89 MaxTokens,
90 StopSequence,
91 Complete,
92 Error,
93 ToolCall,
94 #[serde(untagged)]
97 Other(String),
98}
99
100pub(crate) fn map_finish_reason(reason: &FinishReason) -> completion::FinishReason {
106 match reason {
107 FinishReason::Complete | FinishReason::StopSequence => completion::FinishReason::Stop,
108 FinishReason::MaxTokens => completion::FinishReason::Length,
109 FinishReason::ToolCall => completion::FinishReason::ToolCalls,
110 FinishReason::Error => completion::FinishReason::Other("ERROR".to_owned()),
111 FinishReason::Other(other) => completion::FinishReason::Other(other.clone()),
112 }
113}
114
115#[derive(Debug, Deserialize, Clone, Serialize)]
116pub struct Usage {
117 #[serde(default)]
118 pub billed_units: Option<BilledUnits>,
119 #[serde(default)]
120 pub tokens: Option<Tokens>,
121 #[serde(default)]
123 pub cached_tokens: Option<f64>,
124}
125
126impl From<&Usage> for crate::completion::Usage {
129 fn from(usage: &Usage) -> crate::completion::Usage {
130 let mut normalized = crate::completion::Usage::new();
131
132 if let Some(ref tokens) = usage.tokens {
133 normalized.input_tokens = tokens.input_tokens.unwrap_or_default() as u64;
134 normalized.output_tokens = tokens.output_tokens.unwrap_or_default() as u64;
135 normalized.total_tokens = normalized.input_tokens + normalized.output_tokens;
136 normalized.cached_input_tokens = usage.cached_tokens.unwrap_or_default() as u64;
139 }
140
141 normalized
142 }
143}
144
145impl From<Usage> for crate::completion::Usage {
146 fn from(usage: Usage) -> crate::completion::Usage {
147 crate::completion::Usage::from(&usage)
148 }
149}
150
151#[derive(Debug, Deserialize, Clone, Serialize)]
152pub struct BilledUnits {
153 #[serde(default)]
154 pub output_tokens: Option<f64>,
155 #[serde(default)]
156 pub classifications: Option<f64>,
157 #[serde(default)]
158 pub search_units: Option<f64>,
159 #[serde(default)]
160 pub input_tokens: Option<f64>,
161}
162
163#[derive(Debug, Deserialize, Clone, Serialize)]
164pub struct Tokens {
165 #[serde(default)]
166 pub input_tokens: Option<f64>,
167 #[serde(default)]
168 pub output_tokens: Option<f64>,
169}
170
171impl TryFrom<CompletionResponse> for completion::CompletionResponse {
172 type Error = CompletionError;
173
174 fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
175 let (content, _, tool_calls) = response.message()?;
176
177 let model_response = if !tool_calls.is_empty() {
178 crate::message::require_non_empty(
179 tool_calls
180 .into_iter()
181 .filter_map(|tool_call| {
182 let ToolCallFunction { name, arguments } = tool_call.function?;
183 let id = tool_call.id.unwrap_or_default();
188
189 Some(completion::AssistantContent::tool_call(id, name, arguments))
190 })
191 .collect::<Vec<_>>(),
192 || {
193 CompletionError::ResponseError(
194 "response contained tool call metadata without any callable tool content"
195 .to_owned(),
196 )
197 },
198 )?
199 } else {
200 crate::message::require_non_empty_response(
201 content
202 .into_iter()
203 .map(|content| match content {
204 AssistantContent::Text { text } => completion::AssistantContent::text(text),
205 AssistantContent::Thinking { thinking } => {
206 completion::AssistantContent::Reasoning(Reasoning::new(&thinking))
207 }
208 })
209 .collect::<Vec<_>>(),
210 )?
211 };
212
213 let usage = response
214 .usage
215 .as_ref()
216 .map(completion::Usage::from)
217 .unwrap_or_default();
218
219 Ok(
220 completion::CompletionResponse::new(model_response, usage, PROVIDER_NAME)
223 .with_optional_response_id(Some(response.id.as_str()).filter(|id| !id.is_empty()))
224 .with_finish_reason(map_finish_reason(&response.finish_reason)),
225 )
226 }
227}
228
229#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
230pub struct Document {
231 pub id: String,
232 pub data: HashMap<String, serde_json::Value>,
233}
234
235impl From<completion::Document> for Document {
236 fn from(document: completion::Document) -> Self {
237 let mut data: HashMap<String, serde_json::Value> = HashMap::new();
238
239 document
242 .additional_props
243 .into_iter()
244 .for_each(|(key, value)| {
245 data.insert(key, value.into());
246 });
247
248 data.insert("text".to_string(), document.text.into());
249
250 Self {
251 id: document.id,
252 data,
253 }
254 }
255}
256
257#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
258pub struct ToolCall {
259 #[serde(default)]
260 pub id: Option<String>,
261 #[serde(default)]
262 pub r#type: Option<ToolType>,
263 #[serde(default)]
264 pub function: Option<ToolCallFunction>,
265}
266
267#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
268pub struct ToolCallFunction {
269 pub name: String,
270 #[serde(with = "json_utils::stringified_json")]
271 pub arguments: serde_json::Value,
272}
273
274#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
275#[serde(rename_all = "lowercase")]
276pub enum ToolType {
277 #[default]
278 Function,
279}
280
281#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
282pub struct Tool {
283 pub r#type: ToolType,
284 pub function: Function,
285}
286
287#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
288pub struct Function {
289 pub name: String,
290 #[serde(default)]
291 pub description: Option<String>,
292 pub parameters: serde_json::Value,
293}
294
295impl From<completion::ToolDefinition> for Tool {
296 fn from(tool: completion::ToolDefinition) -> Self {
297 Self {
298 r#type: ToolType::default(),
299 function: Function {
300 name: tool.name,
301 description: Some(tool.description),
302 parameters: tool.parameters,
303 },
304 }
305 }
306}
307
308#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
309#[serde(tag = "role", rename_all = "lowercase")]
310pub enum Message {
311 User {
312 content: Vec<UserContent>,
313 },
314
315 Assistant {
316 #[serde(default)]
317 content: Vec<AssistantContent>,
318 #[serde(default)]
319 citations: Vec<Citation>,
320 #[serde(default)]
321 tool_calls: Vec<ToolCall>,
322 #[serde(default)]
323 tool_plan: Option<String>,
324 },
325
326 Tool {
327 content: Vec<ToolResultContent>,
328 tool_call_id: String,
329 },
330
331 System {
332 content: String,
333 },
334}
335
336#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
337#[serde(tag = "type", rename_all = "lowercase")]
338pub enum UserContent {
339 Text { text: String },
340 ImageUrl { image_url: ImageUrl },
341}
342
343#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
344#[serde(tag = "type", rename_all = "lowercase")]
345pub enum AssistantContent {
346 Text { text: String },
347 Thinking { thinking: String },
348}
349
350#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
351pub struct ImageUrl {
352 pub url: String,
353}
354
355#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
356#[serde(tag = "type", rename_all = "lowercase")]
357pub enum ToolResultContent {
358 Text { text: String },
359 Document { document: Document },
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
363pub struct Citation {
364 #[serde(default)]
365 pub start: Option<u32>,
366 #[serde(default)]
367 pub end: Option<u32>,
368 #[serde(default)]
369 pub text: Option<String>,
370 #[serde(rename = "type")]
371 pub citation_type: Option<CitationType>,
372 #[serde(default)]
373 pub sources: Vec<Source>,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
377#[serde(tag = "type", rename_all = "lowercase")]
378pub enum Source {
379 Document {
380 id: Option<String>,
381 document: Option<serde_json::Map<String, serde_json::Value>>,
382 },
383 Tool {
384 id: Option<String>,
385 tool_output: Option<serde_json::Map<String, serde_json::Value>>,
386 },
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
390#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
391pub enum CitationType {
392 TextContent,
393 Plan,
394}
395
396impl TryFrom<message::Message> for Vec<Message> {
397 type Error = message::MessageError;
398
399 fn try_from(message: message::Message) -> Result<Self, Self::Error> {
400 Ok(match message {
401 message::Message::User { content } => content
402 .into_iter()
403 .map(|content| match content {
404 message::UserContent::Text(message::Text { text, .. }) => Ok(Message::User {
405 content: vec![UserContent::Text { text }],
406 }),
407 message::UserContent::ToolResult(tool_result) => Ok(Message::Tool {
408 tool_call_id: tool_result.wire_call_id().to_owned(),
409 content: tool_result
410 .content
411 .into_iter()
412 .map(|content| match content {
413 message::ToolResultContent::Text(text) => {
414 Ok(ToolResultContent::Text { text: text.text })
415 }
416 message::ToolResultContent::Json { value } => {
417 Ok(ToolResultContent::Text {
418 text: value.to_string(),
419 })
420 }
421 message::ToolResultContent::Image(_) => {
422 Err(message::MessageError::ConversionError(
423 "Only text tool result content is supported by Cohere"
424 .to_owned(),
425 ))
426 }
427 })
428 .collect::<Result<Vec<_>, _>>()?,
429 }),
430 _ => Err(message::MessageError::ConversionError(
431 "Only text content is supported by Cohere".to_owned(),
432 )),
433 })
434 .collect::<Result<Vec<_>, _>>()?,
435 message::Message::System { content } => {
436 vec![Message::System { content }]
437 }
438 message::Message::Assistant { content, .. } => {
439 let mut text_content = vec![];
440 let mut tool_calls = vec![];
441
442 for content in content.into_iter() {
443 match content {
444 message::AssistantContent::Text(message::Text { text, .. }) => {
445 text_content.push(AssistantContent::Text { text });
446 }
447 message::AssistantContent::ToolCall(message::ToolCall {
448 id,
449 provider,
450 function:
451 message::ToolFunction {
452 name, arguments, ..
453 },
454 ..
455 }) => {
456 tool_calls.push(ToolCall {
457 id: Some(match provider {
458 Some(provider) => provider.call_id,
459 None => id.into_string(),
460 }),
461 r#type: Some(ToolType::Function),
462 function: Some(ToolCallFunction {
463 name,
464 arguments: serde_json::to_value(arguments).unwrap_or_default(),
465 }),
466 });
467 }
468 message::AssistantContent::Reasoning(reasoning) => {
469 let thinking = reasoning.display_text();
470 text_content.push(AssistantContent::Thinking { thinking });
471 }
472 message::AssistantContent::Image(_) => {
473 return Err(message::MessageError::ConversionError(
474 "Cohere currently doesn't support images.".to_owned(),
475 ));
476 }
477 }
478 }
479
480 vec![Message::Assistant {
481 content: text_content,
482 citations: vec![],
483 tool_calls,
484 tool_plan: None,
485 }]
486 }
487 })
488 }
489}
490
491impl TryFrom<Message> for message::Message {
492 type Error = message::MessageError;
493
494 fn try_from(message: Message) -> Result<Self, Self::Error> {
495 match message {
496 Message::User { content } => Ok(message::Message::User {
497 content: content
498 .into_iter()
499 .map(|content| match content {
500 UserContent::Text { text } => {
501 message::UserContent::Text(message::Text::new(text))
502 }
503 UserContent::ImageUrl { image_url } => {
504 message::UserContent::image_url(image_url.url, None, None)
505 }
506 })
507 .collect(),
508 }),
509 Message::Assistant {
510 content,
511 tool_calls,
512 ..
513 } => {
514 let mut content = content
515 .into_iter()
516 .map(|content| match content {
517 AssistantContent::Text { text } => message::AssistantContent::text(text),
518 AssistantContent::Thinking { thinking } => {
519 message::AssistantContent::Reasoning(Reasoning::new(&thinking))
520 }
521 })
522 .collect::<Vec<_>>();
523
524 content.extend(tool_calls.into_iter().filter_map(|tool_call| {
525 let ToolCallFunction { name, arguments } = tool_call.function?;
526
527 Some(message::AssistantContent::tool_call(
531 tool_call.id.unwrap_or_default(),
532 name,
533 arguments,
534 ))
535 }));
536
537 let content = crate::message::require_non_empty(content, || {
538 message::MessageError::ConversionError(
539 "Expected either text content or tool calls".to_string(),
540 )
541 })?;
542
543 Ok(message::Message::Assistant { id: None, content })
544 }
545 Message::Tool {
546 content,
547 tool_call_id,
548 } => {
549 let content = content.into_iter().map(|content| {
550 Ok(match content {
551 ToolResultContent::Text { text } => message::ToolResultContent::text(text),
552 ToolResultContent::Document { document } => {
553 message::ToolResultContent::json(
554 serde_json::to_value(document.data).map_err(|e| {
555 message::MessageError::ConversionError(
556 format!("Failed to convert tool result document content into JSON: {e}"),
557 )
558 })?,
559 )
560 }
561 })
562 }).collect::<Result<Vec<_>, _>>()?;
563
564 Ok(message::Message::User {
565 content: vec![message::UserContent::tool_result_from_wire(
568 tool_call_id,
569 "",
570 content,
571 )],
572 })
573 }
574 Message::System { content } => Ok(message::Message::user(content)),
575 }
576 }
577}
578
579#[derive(Clone)]
580pub struct CompletionModel<T = reqwest::Client> {
581 pub(crate) client: Client<T>,
582 pub model: String,
583}
584
585#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
589#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
590pub enum CohereToolChoice {
591 Required,
592 None,
593}
594
595impl TryFrom<ToolChoice> for CohereToolChoice {
596 type Error = CompletionError;
597
598 fn try_from(tool_choice: ToolChoice) -> Result<Self, Self::Error> {
599 match tool_choice {
600 ToolChoice::Required => Ok(Self::Required),
601 ToolChoice::None => Ok(Self::None),
602 ToolChoice::Auto => Err(CompletionError::RequestError(
603 "\"auto\" is not an allowed tool_choice value in the Cohere API; \
604 omit tool_choice to let the model decide"
605 .into(),
606 )),
607 ToolChoice::Specific { .. } => Err(CompletionError::RequestError(
608 "the Cohere API cannot be forced to call specific tools by name; \
609 use ToolChoice::Required and restrict the tools you pass instead"
610 .into(),
611 )),
612 }
613 }
614}
615
616#[derive(Debug, Serialize, Deserialize)]
617pub(super) struct CohereCompletionRequest {
618 pub(super) model: String,
619 pub messages: Vec<Message>,
620 documents: Vec<Document>,
621 #[serde(skip_serializing_if = "Option::is_none")]
622 temperature: Option<f64>,
623 #[serde(skip_serializing_if = "Option::is_none")]
624 max_tokens: Option<u64>,
625 #[serde(skip_serializing_if = "Vec::is_empty")]
626 tools: Vec<Tool>,
627 #[serde(skip_serializing_if = "Option::is_none")]
628 tool_choice: Option<CohereToolChoice>,
629 #[serde(flatten, skip_serializing_if = "Option::is_none")]
630 pub additional_params: Option<serde_json::Value>,
631}
632
633impl TryFrom<(&str, CompletionRequest)> for CohereCompletionRequest {
634 type Error = CompletionError;
635
636 fn try_from((model, req): (&str, CompletionRequest)) -> Result<Self, Self::Error> {
637 let documents = req
638 .documents
639 .iter()
640 .cloned()
641 .map(Document::from)
642 .collect::<Vec<_>>();
643 if req.output_schema.is_some() {
644 tracing::warn!("Structured outputs currently not supported for Cohere");
645 }
646
647 let model = req.model.clone().unwrap_or_else(|| model.to_string());
648 let mut partial_history = vec![];
649 partial_history.extend(req.chat_history);
650
651 let mut full_history: Vec<Message> = req.preamble.map_or_else(Vec::new, |preamble| {
652 vec![Message::System { content: preamble }]
653 });
654
655 full_history.extend(
656 partial_history
657 .into_iter()
658 .map(message::Message::try_into)
659 .collect::<Result<Vec<Vec<Message>>, _>>()?
660 .into_iter()
661 .flatten()
662 .collect::<Vec<_>>(),
663 );
664
665 let tool_choice = req
666 .tool_choice
667 .map(CohereToolChoice::try_from)
668 .transpose()?;
669
670 let has_tools = !req.tools.is_empty()
673 || req
674 .additional_params
675 .as_ref()
676 .and_then(|params| params.get("tools"))
677 .and_then(serde_json::Value::as_array)
678 .is_some_and(|tools| !tools.is_empty());
679 if matches!(tool_choice, Some(CohereToolChoice::Required)) && !has_tools {
680 return Err(CompletionError::RequestError(
681 "Cohere requires at least one tool when tool_choice is REQUIRED".into(),
682 ));
683 }
684
685 Ok(Self {
686 model: model.to_string(),
687 messages: full_history,
688 documents,
689 temperature: req.temperature,
690 max_tokens: req.max_tokens,
691 tools: req.tools.into_iter().map(Tool::from).collect::<Vec<_>>(),
692 tool_choice,
693 additional_params: req.additional_params,
694 })
695 }
696}
697
698impl<T> CompletionModel<T>
699where
700 T: HttpClientExt,
701{
702 pub fn new(client: Client<T>, model: impl Into<String>) -> Self {
703 Self {
704 client,
705 model: model.into(),
706 }
707 }
708}
709
710impl<T> crate::client::ConstructCompletionModel<Client<T>> for CompletionModel<T>
711where
712 T: HttpClientExt,
713 Client<T>: Clone,
714{
715 fn construct(client: &Client<T>, model: String) -> Self {
716 Self::new(client.clone(), model)
717 }
718}
719
720impl<T> CompletionModel<T>
721where
722 T: HttpClientExt + Clone + 'static,
723{
724 pub async fn raw_completion(
733 &self,
734 completion_request: completion::CompletionRequest,
735 ) -> Result<CompletionResponse, CompletionError> {
736 let system_instructions = completion_request.preamble.clone();
737 let record_telemetry_content = completion_request.record_telemetry_content;
738 let request = CohereCompletionRequest::try_from((self.model.as_ref(), completion_request))?;
739
740 let llm_span =
741 CompletionSpanBuilder::new(PROVIDER_NAME, &request.model, CompletionOperation::Chat)
742 .system_instructions(system_instructions.as_deref(), record_telemetry_content)
743 .build();
744
745 crate::providers::internal::trace_json(
746 crate::providers::internal::LogTarget::Completions,
747 "Cohere completion request",
748 &request,
749 );
750
751 let req_body = serde_json::to_vec(&request)?;
752
753 let req = self
754 .client
755 .post("/v2/chat")?
756 .body(req_body)
757 .map_err(|e| CompletionError::HttpError(e.into()))?;
758
759 send_completion::<_, DirectPayload<CompletionResponse>, _>(
762 &self.client,
763 req,
764 "Cohere completion",
765 None,
769 |json_response| {
770 let span = tracing::Span::current();
771 let usage = json_response
772 .usage
773 .as_ref()
774 .map(completion::Usage::from)
775 .unwrap_or_default();
776 span.record_token_usage(&usage);
777 span.record_response_metadata(json_response);
778 },
779 )
780 .instrument(llm_span)
781 .await
782 .map(|(payload, _)| payload)
783 }
784}
785
786impl<T> completion::CompletionModel for CompletionModel<T>
787where
788 T: HttpClientExt + Clone + 'static,
789{
790 async fn completion(
791 &self,
792 completion_request: completion::CompletionRequest,
793 ) -> Result<completion::CompletionResponse, CompletionError> {
794 let raw = self.raw_completion(completion_request).await?;
796 let captured = serde_json::to_value(&raw)?;
797 let response: completion::CompletionResponse = raw.try_into()?;
798 Ok(response.with_raw(captured))
799 }
800
801 async fn stream(
802 &self,
803 request: CompletionRequest,
804 ) -> Result<crate::streaming::StreamingCompletionResponse, CompletionError> {
805 CompletionModel::stream(self, request).await
806 }
807}
808#[cfg(test)]
809mod tests {
810 use super::*;
811 use serde_path_to_error::deserialize;
812
813 #[test]
814 fn test_deserialize_completion_response() {
815 let json_data = r#"
816 {
817 "id": "abc123",
818 "message": {
819 "role": "assistant",
820 "tool_plan": "I will use the subtract tool to find the difference between 2 and 5.",
821 "tool_calls": [
822 {
823 "id": "subtract_sm6ps6fb6y9f",
824 "type": "function",
825 "function": {
826 "name": "subtract",
827 "arguments": "{\"x\":5,\"y\":2}"
828 }
829 }
830 ]
831 },
832 "finish_reason": "TOOL_CALL",
833 "usage": {
834 "billed_units": {
835 "input_tokens": 78,
836 "output_tokens": 27
837 },
838 "tokens": {
839 "input_tokens": 1028,
840 "output_tokens": 63
841 }
842 }
843 }
844 "#;
845
846 let mut deserializer = serde_json::Deserializer::from_str(json_data);
847 let result: Result<CompletionResponse, _> = deserialize(&mut deserializer);
848
849 let response = result.unwrap();
850 let (_, citations, tool_calls) = response.message().expect("assistant message");
851 let CompletionResponse {
852 id,
853 finish_reason,
854 usage,
855 ..
856 } = response;
857
858 assert_eq!(id, "abc123");
859 assert_eq!(finish_reason, FinishReason::ToolCall);
860
861 let Usage {
862 billed_units,
863 tokens,
864 ..
865 } = usage.unwrap();
866 let BilledUnits {
867 input_tokens: billed_input_tokens,
868 output_tokens: billed_output_tokens,
869 ..
870 } = billed_units.unwrap();
871 let Tokens {
872 input_tokens,
873 output_tokens,
874 } = tokens.unwrap();
875
876 assert_eq!(billed_input_tokens.unwrap(), 78.0);
877 assert_eq!(billed_output_tokens.unwrap(), 27.0);
878 assert_eq!(input_tokens.unwrap(), 1028.0);
879 assert_eq!(output_tokens.unwrap(), 63.0);
880
881 assert!(citations.is_empty());
882 assert_eq!(tool_calls.len(), 1);
883
884 let ToolCallFunction { name, arguments } = tool_calls[0].function.clone().unwrap();
885
886 assert_eq!(name, "subtract");
887 assert_eq!(arguments, serde_json::json!({"x": 5, "y": 2}));
888 }
889
890 #[test]
891 fn finish_reason_maps_every_documented_wire_value() {
892 assert_eq!(
893 map_finish_reason(&FinishReason::Complete),
894 completion::FinishReason::Stop
895 );
896 assert_eq!(
897 map_finish_reason(&FinishReason::StopSequence),
898 completion::FinishReason::Stop
899 );
900 assert_eq!(
901 map_finish_reason(&FinishReason::MaxTokens),
902 completion::FinishReason::Length
903 );
904 assert_eq!(
905 map_finish_reason(&FinishReason::ToolCall),
906 completion::FinishReason::ToolCalls
907 );
908 assert_eq!(
909 map_finish_reason(&FinishReason::Error),
910 completion::FinishReason::Other("ERROR".to_owned())
911 );
912 }
913
914 #[test]
915 fn unknown_finish_reason_survives_verbatim() {
916 let reason: FinishReason = serde_json::from_str("\"ERROR_TOXIC\"")
917 .expect("unknown reasons must still deserialize");
918 assert_eq!(reason, FinishReason::Other("ERROR_TOXIC".to_owned()));
919 assert_eq!(
920 map_finish_reason(&reason),
921 completion::FinishReason::Other("ERROR_TOXIC".to_owned())
922 );
923 }
924
925 #[test]
926 fn tool_call_response_normalizes_to_tool_calls_finish_reason() {
927 let response: CompletionResponse = serde_json::from_str(
928 r#"{
929 "id": "abc123",
930 "message": {
931 "role": "assistant",
932 "tool_calls": [{
933 "id": "subtract_1",
934 "type": "function",
935 "function": {"name": "subtract", "arguments": "{\"x\":5,\"y\":2}"}
936 }]
937 },
938 "finish_reason": "TOOL_CALL",
939 "usage": {"tokens": {"input_tokens": 10, "output_tokens": 4}}
940 }"#,
941 )
942 .expect("fixture should deserialize");
943
944 let normalized: completion::CompletionResponse =
945 response.try_into().expect("normalization should succeed");
946
947 assert_eq!(normalized.provider, PROVIDER_NAME);
948 assert_eq!(normalized.response_id.as_deref(), Some("abc123"));
949 assert_eq!(normalized.message_id, None);
950 assert_eq!(normalized.model, None);
951 assert_eq!(
952 normalized.finish_reason(),
953 Some(completion::FinishReason::ToolCalls)
954 );
955 assert_eq!(normalized.usage.input_tokens, 10);
956 assert_eq!(normalized.usage.output_tokens, 4);
957 assert_eq!(normalized.usage.total_tokens, 14);
958 }
959
960 #[test]
961 fn test_convert_completion_message_to_message_and_back() {
962 let completion_message = completion::Message::User {
963 content: vec![completion::message::UserContent::Text(
964 completion::message::Text::new("Hello, world!".to_string()),
965 )],
966 };
967
968 let messages: Vec<Message> = completion_message.clone().try_into().unwrap();
969 let _converted_back: Vec<completion::Message> = messages
970 .into_iter()
971 .map(|msg| msg.try_into().unwrap())
972 .collect::<Vec<_>>();
973 }
974
975 #[test]
976 fn test_convert_message_to_completion_message_and_back() {
977 let message = Message::User {
978 content: vec![UserContent::Text {
979 text: "Hello, world!".to_string(),
980 }],
981 };
982
983 let completion_message: completion::Message = message.clone().try_into().unwrap();
984 let _converted_back: Vec<Message> = completion_message.try_into().unwrap();
985 }
986
987 #[test]
988 fn usage_is_mapped_from_tokens_and_carries_cached_input() {
989 let usage: Usage = serde_json::from_str(
990 r#"{
991 "billed_units": {"input_tokens": 135, "output_tokens": 24},
992 "cached_tokens": 112,
993 "tokens": {"input_tokens": 1610, "output_tokens": 56}
994 }"#,
995 )
996 .expect("usage should deserialize");
997
998 let mapped = crate::completion::Usage::from(&usage);
999 assert_eq!(mapped.input_tokens, 1610);
1000 assert_eq!(mapped.output_tokens, 56);
1001 assert_eq!(mapped.total_tokens, 1666);
1002 assert_eq!(mapped.cached_input_tokens, 112);
1003 }
1004
1005 #[test]
1006 fn response_usage_matches_the_canonical_mapping() {
1007 let response: CompletionResponse = serde_json::from_str(
1008 r#"{
1009 "id": "abc123",
1010 "finish_reason": "COMPLETE",
1011 "message": {"role": "assistant", "content": [{"type": "text", "text": "hi"}]},
1012 "usage": {
1013 "billed_units": {"input_tokens": 135, "output_tokens": 24},
1014 "cached_tokens": 112,
1015 "tokens": {"input_tokens": 1610, "output_tokens": 56}
1016 }
1017 }"#,
1018 )
1019 .expect("response should deserialize");
1020
1021 let expected = crate::completion::Usage::from(
1022 response.usage.as_ref().expect("usage should be present"),
1023 );
1024 let converted: completion::CompletionResponse =
1025 response.try_into().expect("response should convert");
1026
1027 assert_eq!(converted.usage, expected);
1028 assert_eq!(converted.usage.input_tokens, 1610);
1029 assert_eq!(converted.usage.cached_input_tokens, 112);
1030 }
1031
1032 #[test]
1033 fn usage_without_token_counts_maps_to_zero() {
1034 let usage: Usage = serde_json::from_str("{}").expect("usage should deserialize");
1035 assert_eq!(
1036 crate::completion::Usage::from(&usage),
1037 crate::completion::Usage::new()
1038 );
1039
1040 let cached_only: Usage =
1041 serde_json::from_str(r#"{"cached_tokens": 512}"#).expect("usage should deserialize");
1042 assert_eq!(
1043 crate::completion::Usage::from(&cached_only),
1044 crate::completion::Usage::new()
1045 );
1046 }
1047
1048 #[test]
1049 fn tool_result_content_is_type_tagged() {
1050 let text = serde_json::to_value(ToolResultContent::Text {
1051 text: "-3".to_owned(),
1052 })
1053 .expect("tool result text content should serialize");
1054 assert_eq!(text, serde_json::json!({"type": "text", "text": "-3"}));
1055
1056 let document = serde_json::to_value(ToolResultContent::Document {
1057 document: Document {
1058 id: "doc_1".to_owned(),
1059 data: HashMap::from([("text".to_owned(), "-3".into())]),
1060 },
1061 })
1062 .expect("tool result document content should serialize");
1063 assert_eq!(
1064 document,
1065 serde_json::json!({
1066 "type": "document",
1067 "document": {"id": "doc_1", "data": {"text": "-3"}}
1068 })
1069 );
1070
1071 let roundtrip: ToolResultContent =
1072 serde_json::from_value(text).expect("tool result content should deserialize");
1073 assert_eq!(
1074 roundtrip,
1075 ToolResultContent::Text {
1076 text: "-3".to_owned()
1077 }
1078 );
1079 }
1080
1081 #[test]
1082 fn cohere_builder_request_serializes_documents_in_cohere_shape() {
1083 let request = crate::completion::CompletionRequestBuilder::new(
1084 crate::test_utils::MockCompletionModel::default(),
1085 "What is glarb-glarb?",
1086 )
1087 .document(crate::completion::request::Document {
1088 id: "doc_1".to_string(),
1089 text: "Definition of glarb-glarb: an ancient tool.".to_string(),
1090 additional_props: HashMap::from([("source".to_string(), "field-notes".to_string())]),
1091 })
1092 .build();
1093
1094 let request = CohereCompletionRequest::try_from(("command-a-03-2025", request))
1095 .expect("request conversion should succeed");
1096
1097 assert_eq!(request.documents.len(), 1);
1098 assert_eq!(request.documents[0].id, "doc_1");
1099
1100 let documents = serde_json::to_value(&request.documents)
1101 .expect("documents should serialize")
1102 .as_array()
1103 .cloned()
1104 .expect("documents should serialize as an array");
1105 assert_eq!(
1106 documents[0],
1107 serde_json::json!({
1108 "id": "doc_1",
1109 "data": {
1110 "text": "Definition of glarb-glarb: an ancient tool.",
1111 "source": "field-notes"
1112 }
1113 })
1114 );
1115 }
1116
1117 #[test]
1118 fn tool_choice_serializes_as_a_bare_cohere_string() {
1119 assert_eq!(
1120 serde_json::to_value(CohereToolChoice::Required).expect("serialize"),
1121 serde_json::json!("REQUIRED")
1122 );
1123 assert_eq!(
1124 serde_json::to_value(CohereToolChoice::None).expect("serialize"),
1125 serde_json::json!("NONE")
1126 );
1127
1128 assert_eq!(
1129 CohereToolChoice::try_from(ToolChoice::Required).expect("required is supported"),
1130 CohereToolChoice::Required
1131 );
1132 assert_eq!(
1133 CohereToolChoice::try_from(ToolChoice::None).expect("none is supported"),
1134 CohereToolChoice::None
1135 );
1136 }
1137
1138 #[test]
1139 fn unsupported_tool_choices_are_rejected_before_the_request_is_sent() {
1140 for unsupported in [
1141 ToolChoice::Auto,
1142 ToolChoice::Specific {
1143 function_names: vec!["subtract".to_string()],
1144 },
1145 ] {
1146 let error = CohereToolChoice::try_from(unsupported.clone())
1147 .expect_err("Cohere has no encoding for this tool choice");
1148 assert!(
1149 matches!(error, CompletionError::RequestError(_)),
1150 "expected a request error for {unsupported:?}, got {error:?}"
1151 );
1152 }
1153 }
1154
1155 #[tokio::test]
1158 async fn required_tool_choice_without_tools_is_rejected_before_the_request_is_sent() {
1159 use crate::client::CompletionClient;
1160 use crate::completion::CompletionModel as _;
1161 use crate::test_utils::RecordingHttpClient;
1162
1163 let http_client = RecordingHttpClient::new("{}");
1164 let client = crate::providers::cohere::Client::builder()
1165 .api_key("test-key")
1166 .http_client(http_client.clone())
1167 .build()
1168 .expect("build client");
1169 let model = client.completion_model(crate::providers::cohere::COMMAND_A_03_2025);
1170 let request = model
1171 .completion_request("hello")
1172 .tool_choice(ToolChoice::Required)
1173 .build();
1174
1175 let error = model
1176 .completion(request)
1177 .await
1178 .expect_err("REQUIRED without tools should fail locally");
1179
1180 assert!(matches!(error, CompletionError::RequestError(_)));
1181 let message = error.to_string();
1182 assert!(
1183 message.contains("at least one tool") && message.contains("REQUIRED"),
1184 "unexpected error: {error:?}"
1185 );
1186 assert!(
1187 http_client.requests().is_empty(),
1188 "invalid requests must fail before reaching the HTTP client"
1189 );
1190 }
1191
1192 #[test]
1195 fn required_tool_choice_accepts_raw_tools_from_additional_params() {
1196 let request = crate::completion::CompletionRequestBuilder::new(
1197 crate::test_utils::MockCompletionModel::default(),
1198 "hello",
1199 )
1200 .tool_choice(ToolChoice::Required)
1201 .additional_params(serde_json::json!({
1202 "tools": [{
1203 "type": "function",
1204 "function": {
1205 "name": "ping",
1206 "description": "Return pong",
1207 "parameters": {"type": "object", "properties": {}}
1208 }
1209 }]
1210 }))
1211 .build();
1212
1213 let request = CohereCompletionRequest::try_from(("command-a-03-2025", request))
1214 .expect("raw Cohere tools should satisfy REQUIRED");
1215 let body = serde_json::to_value(request).expect("request should serialize");
1216
1217 assert_eq!(body["tool_choice"], serde_json::json!("REQUIRED"));
1218 assert_eq!(body["tools"].as_array().map(Vec::len), Some(1));
1219 }
1220
1221 #[test]
1222 fn max_tokens_is_forwarded_and_omitted_when_unset() {
1223 let capped = crate::completion::CompletionRequestBuilder::new(
1224 crate::test_utils::MockCompletionModel::default(),
1225 "hello",
1226 )
1227 .max_tokens(64)
1228 .build();
1229 let capped = CohereCompletionRequest::try_from(("command-a-03-2025", capped))
1230 .expect("request conversion should succeed");
1231 let body = serde_json::to_value(&capped).expect("request should serialize");
1232 assert_eq!(body["max_tokens"], serde_json::json!(64));
1233
1234 let uncapped = crate::completion::CompletionRequestBuilder::new(
1235 crate::test_utils::MockCompletionModel::default(),
1236 "hello",
1237 )
1238 .build();
1239 let uncapped = CohereCompletionRequest::try_from(("command-a-03-2025", uncapped))
1240 .expect("request conversion should succeed");
1241 let body = serde_json::to_value(&uncapped).expect("request should serialize");
1242 assert!(body.get("max_tokens").is_none());
1243 }
1244
1245 #[test]
1246 fn tool_choice_is_omitted_when_unset() {
1247 let request = crate::completion::CompletionRequestBuilder::new(
1248 crate::test_utils::MockCompletionModel::default(),
1249 "hello",
1250 )
1251 .build();
1252
1253 let request = CohereCompletionRequest::try_from(("command-a-03-2025", request))
1254 .expect("request conversion should succeed");
1255 let body = serde_json::to_value(&request).expect("request should serialize");
1256
1257 assert!(body.get("tool_choice").is_none());
1258 }
1259
1260 #[tokio::test]
1261 async fn completion_non_success_preserves_status_and_body() {
1262 use crate::client::CompletionClient;
1263 use crate::completion::CompletionModel as _;
1264 use crate::test_utils::RecordingHttpClient;
1265
1266 let body = r#"{"error":{"message":"boom"}}"#;
1267 let http_client =
1268 RecordingHttpClient::with_error_response(http::StatusCode::SERVICE_UNAVAILABLE, body);
1269 let client = crate::providers::cohere::Client::builder()
1270 .api_key("test-key")
1271 .http_client(http_client)
1272 .build()
1273 .expect("build client");
1274 let model = client.completion_model(crate::providers::cohere::COMMAND_A_03_2025);
1275 let request = model.completion_request("hello").build();
1276
1277 let error = model
1278 .completion(request)
1279 .await
1280 .expect_err("should fail with non-success status");
1281
1282 assert!(matches!(error, CompletionError::HttpError(_)));
1283 assert_eq!(
1284 error.provider_response_status(),
1285 Some(http::StatusCode::SERVICE_UNAVAILABLE)
1286 );
1287 assert_eq!(error.provider_response_body(), Some(body));
1288 }
1289}