Skip to main content

rig_core/providers/cohere/
completion.rs

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
16/// Stable descriptor name recorded on normalized responses, streams, and
17/// telemetry spans for this provider.
18pub(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    /// Return that parts of the response for assistant messages w/o dealing with the other variants
33    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    /// A reason outside the set Cohere documents today, kept verbatim in
95    /// Cohere's own spelling rather than failing deserialization.
96    #[serde(untagged)]
97    Other(String),
98}
99
100/// Map Cohere's `finish_reason` onto rig's normalized vocabulary.
101///
102/// `ERROR` — and anything Cohere adds later — is carried through as
103/// [`completion::FinishReason::Other`] in Cohere's own wire spelling instead of
104/// being flattened into a natural stop.
105pub(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    /// Subset of `tokens.input_tokens`; excluded from `billed_units.input_tokens`.
122    #[serde(default)]
123    pub cached_tokens: Option<f64>,
124}
125
126/// `tokens` is the total-usage counter; `billed_units` excludes cached input
127/// and system overhead, silently undercounting.
128impl 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            // `cached_input_tokens` is a subset of `input_tokens`, so it's only
137            // reported when Cohere also reports `input_tokens`.
138            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                        // The wire's id when present, or empty so the
184                        // conversion mints — never the tool name: a name-as-id
185                        // is fake provenance and collides two same-tool calls
186                        // in one turn.
187                        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            // Cohere's `/v2/chat` payload reports no model identifier, so the
221            // normalized `model` stays unset.
222            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        // We use `.into()` here explicitly since the `document.additional_props` type will likely
240        //  evolve into `serde_json::Value` in the future.
241        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                    // Empty when the wire issued no id, so the conversion
528                    // mints — never the tool name (fake provenance; collides
529                    // two same-tool calls in one turn).
530                    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                    // Cohere tool messages carry no tool name; this
566                    // conversion is lossy for name-keyed wires.
567                    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/// Cohere's `tool_choice` is a bare string; only `REQUIRED`/`NONE` are valid.
586/// `Auto` errors below rather than silently mapping to the omitted-field
587/// behavior that would actually let the model decide.
588#[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        // Count tools supplied through the provider escape hatch as well as
671        // typed tools so REQUIRED remains usable with Cohere-specific schemas.
672        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    /// Execute a completion and return Cohere's own wire response.
725    ///
726    /// This is the escape hatch for Cohere-specific fields rig does not
727    /// normalize (citations, tool plans). It shares the request builder,
728    /// transport, telemetry, and error handling with
729    /// [`CompletionModel::completion`](completion::CompletionModel::completion),
730    /// which calls it and then applies the provider-local mapping — one network
731    /// request either way.
732    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        // Left unboxed so `provider_response_status`/`_body` can read the
760        // status and body straight off the transport error.
761        send_completion::<_, DirectPayload<CompletionResponse>, _>(
762            &self.client,
763            req,
764            "Cohere completion",
765            // Cohere reports no request-id response header (its `x-debug-trace-id`
766            // is a debug trace handle, not a documented request id); the
767            // normalized id is None by design.
768            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        // Capture before `try_into` consumes the raw value.
795        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    /// Invalid REQUIRED requests cannot produce a cassette because validation
1156    /// must stop them before the HTTP boundary.
1157    #[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    /// This internal unit test protects the raw provider-parameter escape hatch;
1193    /// cassette coverage exercises the public typed-tool path instead.
1194    #[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}