1use std::collections::BTreeMap;
13use std::sync::Arc;
14
15use serde::{Deserialize, Serialize};
16use serde_json::{Map, Value, json};
17
18use super::CohereConfig;
19use super::streaming::ChatDecoder;
20use crate::catalog::ModelFacts;
21use crate::completion::options::{BaseInput, FinalBody, RawAt, request_params};
22use crate::completion::{CompletionRequest, Document, ProviderCapabilities, Replay};
23use crate::error::EncodeError;
24use crate::json_utils::Lenient;
25use crate::message::{
26 AssistantContent, AssistantMessage, DocumentData, DocumentSourceKind as Source, Message,
27 MimeType, ToolChoice, ToolResult, ToolResultContent, UserContent,
28};
29use crate::operation::Completion;
30use crate::providers::internal::object;
31use crate::providers::internal::wire_ids::WireIds;
32use crate::wire::{Capabilities, Descriptor, Encoded, Framing, Mode, Wire};
33
34const CHAT_PATH: &str = "/v2/chat";
36
37pub(crate) const API: &str = "cohere.chat";
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
43pub struct NativeChat {
44 pub provider: CohereConfig,
46 pub model: String,
48 pub strict_tools: bool,
51 #[serde(skip)]
53 pub facts: ModelFacts,
54}
55
56impl NativeChat {
57 pub fn new(provider: CohereConfig, model: impl Into<String>) -> Self {
59 Self {
60 provider,
61 model: model.into(),
62 strict_tools: false,
63 facts: ModelFacts::default(),
64 }
65 }
66
67 pub fn with_facts(mut self, facts: ModelFacts) -> Self {
70 self.facts = facts;
71 self
72 }
73
74 pub fn with_strict_tools(mut self) -> Self {
76 self.strict_tools = true;
77 self
78 }
79
80 fn body(&self, request: &CompletionRequest, mode: Mode) -> Result<FinalBody, EncodeError> {
83 request_params(
84 self,
85 request,
86 |input| self.base(request, mode, input),
87 RawAt::Top,
88 &[],
89 )
90 }
91
92 fn base(
94 &self,
95 request: &CompletionRequest,
96 mode: Mode,
97 input: &mut BaseInput<'_>,
98 ) -> Result<Map<String, Value>, EncodeError> {
99 let model = request.model.clone().unwrap_or_else(|| self.model.clone());
100 let messages = self.messages(&request.chat_history, &model)?;
101 let mut tools: Vec<Value> = request
102 .tools
103 .iter()
104 .filter(|tool| match &request.tool_choice {
105 Some(ToolChoice::Specific { function_names }) => {
108 function_names.contains(&tool.name)
109 }
110 _ => true,
111 })
112 .map(|tool| {
113 json!({"type": "function", "function": {
114 "name": tool.name,
115 "description": tool.description,
116 "parameters": tool.parameters,
117 }})
118 })
119 .collect();
120 tools.extend(input.raw_tools()?);
121 let tool_choice = match &request.tool_choice {
122 None | Some(ToolChoice::Auto) => None,
123 Some(ToolChoice::None) => Some("NONE"),
124 Some(ToolChoice::Required | ToolChoice::Specific { .. }) => Some("REQUIRED"),
125 }
126 .filter(|_| !tools.is_empty());
127 let documents: Vec<Value> = request
128 .documents
129 .iter()
130 .enumerate()
131 .map(|(position, document)| document_value(position, document))
132 .collect();
133 let response_format = request
134 .output_schema
135 .clone()
136 .map(|schema| json!({"type": "json_object", "schema": schema.to_value()}));
137 Ok(crate::providers::internal::set_fields([
138 ("model", Some(Value::String(model))),
139 ("messages", Some(Value::Array(messages))),
140 (
141 "documents",
142 (!documents.is_empty()).then_some(Value::Array(documents)),
143 ),
144 ("tools", (!tools.is_empty()).then_some(Value::Array(tools))),
145 ("tool_choice", tool_choice.map(Value::from)),
146 (
147 "strict_tools",
148 self.strict_tools.then_some(Value::Bool(true)),
149 ),
150 ("response_format", response_format),
151 ("temperature", request.temperature.map(Value::from)),
152 ("max_tokens", request.max_tokens.map(Value::from)),
153 (
154 "stream",
155 (mode == Mode::Streaming).then_some(Value::Bool(true)),
156 ),
157 ]))
158 }
159
160 fn messages(&self, history: &[Arc<Message>], model: &str) -> Result<Vec<Value>, EncodeError> {
163 let ids = WireIds::for_target(history, self, model);
164 let mut messages = Vec::new();
165 for message in history {
166 match &**message {
167 Message::System { content } => {
168 messages.push(json!({"role": "system", "content": content}));
169 }
170 Message::User { content } => {
171 let mut parts = Vec::new();
172 for part in content {
173 if let UserContent::ToolResult(result) = part {
174 user_message(&mut messages, &mut parts);
175 messages.push(tool_message(result, &ids));
176 } else {
177 parts.push(user_part(part)?);
178 }
179 }
180 user_message(&mut messages, &mut parts);
181 }
182 Message::Assistant(turn) => messages.extend(self.assistant(turn, &ids)),
183 }
184 }
185 if messages.is_empty() {
186 return Err(EncodeError::request(
187 "Cohere chat request has no messages after conversion",
188 ));
189 }
190 Ok(messages)
191 }
192
193 fn assistant(&self, turn: &AssistantMessage, ids: &WireIds) -> Option<Value> {
199 let (mut content, mut plan, mut calls, mut citations) =
200 (Vec::new(), String::new(), Vec::new(), Vec::new());
201 for block in &turn.content {
202 let replay = block.replay(self, ids);
203 let kind = replay_kind(&replay);
204 let mut item = match replay {
205 Replay::Item(item) => match item.into_owned() {
206 Value::Object(item) => item,
207 _ => Map::new(),
208 },
209 Replay::Identity(_) | Replay::Rebuild => Map::new(),
210 };
211 let cited = match item.shift_remove("citations") {
212 Some(Value::Array(cited)) => cited,
213 _ => Vec::new(),
214 };
215 let part = match block {
216 AssistantContent::Text(text) if !text.text.is_empty() => {
217 json!({"type": "text", "text": text.text})
218 }
219 AssistantContent::Reasoning(reasoning) if kind.as_deref() == Some(PLAN) => {
220 citations.extend(pointed(cited, None));
221 plan.push_str(&reasoning.text);
222 continue;
223 }
224 AssistantContent::Reasoning(reasoning) if !reasoning.text.is_empty() => {
225 json!({"type": "thinking", "thinking": reasoning.text})
226 }
227 AssistantContent::Opaque(opaque)
228 if opaque.replay && opaque.item.get("type").is_some() =>
229 {
230 content.push(opaque.item.clone());
231 continue;
232 }
233 AssistantContent::ToolCall(call) => {
234 item.entry("type")
235 .or_insert_with(|| Value::from("function"));
236 item.insert("id".to_owned(), Value::String(ids.spell(&call.id)));
237 let function = item
238 .entry("function")
239 .or_insert_with(|| Value::Object(Map::new()));
240 if !function.is_object() {
241 *function = Value::Object(Map::new());
242 }
243 if let Value::Object(function) = function {
244 function
245 .insert("name".to_owned(), Value::from(call.function.name.as_str()));
246 function.insert(
247 "arguments".to_owned(),
248 Value::String(
249 serde_json::to_string(&call.function.arguments).unwrap_or_default(),
250 ),
251 );
252 }
253 calls.push(Value::Object(item));
254 continue;
255 }
256 AssistantContent::Text(_)
257 | AssistantContent::Reasoning(_)
258 | AssistantContent::Image(_)
259 | AssistantContent::Opaque(_) => continue,
260 };
261 citations.extend(pointed(cited, Some(content.len())));
262 content.push(if item.is_empty() {
263 part
264 } else {
265 Value::Object(item)
266 });
267 }
268 if content.is_empty() && calls.is_empty() {
269 return None;
270 }
271 Some(Value::Object(crate::providers::internal::set_fields([
272 ("role", Some(Value::from("assistant"))),
273 (
274 "content",
275 (!content.is_empty()).then_some(Value::Array(content)),
276 ),
277 (
278 "tool_plan",
279 (!plan.is_empty()).then_some(Value::String(plan)),
280 ),
281 (
282 "tool_calls",
283 (!calls.is_empty()).then_some(Value::Array(calls)),
284 ),
285 (
286 "citations",
287 (!citations.is_empty()).then_some(Value::Array(citations)),
288 ),
289 ])))
290 }
291}
292
293pub(crate) const PLAN: &str = "tool_plan";
295
296fn replay_kind(replay: &Replay<'_>) -> Option<String> {
299 match replay {
300 Replay::Item(item) => item.str("type").map(str::to_owned),
301 Replay::Identity(identity) => identity
302 .get("type")
303 .and_then(Value::as_str)
304 .map(str::to_owned),
305 Replay::Rebuild => None,
306 }
307}
308
309fn kind(block: &AssistantContent, target: &NativeChat) -> Option<String> {
311 replay_kind(&block.replay(target, &WireIds::default()))
312}
313
314fn pointed(citations: Vec<Value>, content_index: Option<usize>) -> Vec<Value> {
317 citations
318 .into_iter()
319 .map(|mut citation| {
320 if let (Some(citation), Some(index)) = (citation.as_object_mut(), content_index) {
321 citation.insert("content_index".to_owned(), Value::from(index));
322 }
323 citation
324 })
325 .collect()
326}
327
328fn document_value(position: usize, document: &Document) -> Value {
332 let mut data: BTreeMap<&str, &str> = document
333 .additional_props
334 .iter()
335 .map(|(key, value)| (key.as_str(), value.as_str()))
336 .collect();
337 data.insert("text", &document.text);
338 let id = if document.id.is_empty() {
339 format!("doc_{position}")
340 } else {
341 document.id.clone()
342 };
343 json!({"id": id, "data": data})
344}
345
346fn user_part(part: &UserContent) -> Result<Value, EncodeError> {
349 Ok(match part {
350 UserContent::Text(text) => json!({"type": "text", "text": text.text}),
351 UserContent::Image(image) => {
352 let mime = image.media_type.as_ref().map(MimeType::to_mime_type);
353 let url = match (&image.data, mime) {
354 (Source::Url(url), _) => url.clone(),
355 (Source::Base64(data), Some(mime)) => format!("data:{mime};base64,{data}"),
356 _ => return Err(unsendable("an image")),
357 };
358 object! {"type": "image_url", "image_url": object! {"url": url}}
359 }
360 UserContent::Document(document) => match &document.data {
361 DocumentData::Text(text) => json!({"type": "text", "text": text}),
362 DocumentData::File(_) => return Err(unsendable("a document")),
363 },
364 UserContent::Audio(_) => return Err(unsendable("audio")),
365 UserContent::Video(_) => return Err(unsendable("a video")),
366 UserContent::ToolResult(_) => return Err(unsendable("a tool result as a content part")),
367 })
368}
369
370fn unsendable(what: &str) -> EncodeError {
373 EncodeError::request(format!("Cohere chat cannot carry {what} in this form"))
374}
375
376fn user_message(messages: &mut Vec<Value>, parts: &mut Vec<Value>) {
378 if parts.is_empty() {
379 return;
380 }
381 messages.push(object! {"role": "user", "content": std::mem::take(parts)});
382}
383
384fn tool_message(result: &ToolResult, ids: &WireIds) -> Value {
386 let content: Vec<Value> = result
387 .content
388 .iter()
389 .filter_map(|part| match part {
390 ToolResultContent::Text(text) => Some(json!({"type": "text", "text": text.text})),
391 ToolResultContent::Json { value } => {
392 Some(json!({"type": "text", "text": value.to_string()}))
393 }
394 ToolResultContent::Image(_) => None,
395 })
396 .collect();
397 object! {"role": "tool", "tool_call_id": ids.spell(&result.call), "content": content}
398}
399
400impl Wire for NativeChat {
401 type Op = Completion;
402 type Payload = Encoded;
403 type Frame = crate::wire::WireFrame;
404 type Event = super::streaming::ChatEvent;
405 type Decoder<'id> = ChatDecoder;
406 type Reassembler = super::streaming::document::ChatResponse;
407
408 fn describe(&self) -> Descriptor<'_> {
409 Descriptor::new(super::PROVIDER_NAME)
410 .model(self.model.as_str())
411 .facts(&self.facts)
412 .capabilities(Capabilities::completion(
413 ProviderCapabilities::default().with_native_output_tool_composition(true),
414 ))
415 .replay(self)
416 }
417
418 fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
419 let body = self.body(&request, mode)?;
420 crate::providers::internal::trace_json(
421 crate::providers::internal::LogTarget::Completions,
422 "Cohere chat request",
423 &body,
424 );
425 let request = self.provider.post(CHAT_PATH).body(body.into_body())?;
426 let framing = match mode {
427 Mode::Streaming => Framing::Sse,
428 Mode::Unary => Framing::Whole,
429 };
430 Ok(Encoded::new(request, framing)
431 .with_request_id_header(Some(REQUEST_ID_HEADER))
432 .with_projection(ChatDecoder::project)
433 .with_route(Some(CHAT_PATH)))
434 }
435
436 fn decoder<'id>(&self) -> Self::Decoder<'id> {
437 ChatDecoder::default()
438 }
439}
440
441const REQUEST_ID_HEADER: &str = "x-debug-trace-id";
443
444impl crate::completion::ReplayTarget for NativeChat {
445 fn map_options(
447 &self,
448 request: &CompletionRequest,
449 fields: crate::completion::options::OptionFields<'_>,
450 ) -> crate::completion::options::OptionMap {
451 use crate::completion::options::{Mapping, OptionMap};
452 use crate::completion::{CacheRetention, Effort, Reasoning};
453 let model = request.model.as_deref().unwrap_or(&self.model);
454 let thinks = super::thinks(&self.facts, model);
457 let reasons = thinks.unwrap_or_else(|| model.contains("reasoning"));
458 const NO_FIELD: &str = "Cohere's chat API has no such field";
459 OptionMap {
460 reasoning: Mapping::of(fields.reasoning, |reasoning| match reasoning {
461 Reasoning::Off if reasons => {
462 Mapping::Send(json!({"thinking": {"type": "disabled"}}))
463 }
464 Reasoning::Off => Mapping::Omit("the model does not think"),
465 Reasoning::Effort(_) | Reasoning::Budget { .. } if thinks == Some(false) => {
466 Mapping::unsupported("the model does not think")
467 }
468 Reasoning::Effort(Effort::High) => {
469 Mapping::Send(json!({"thinking": {"type": "enabled"}}))
470 }
471 Reasoning::Effort(effort) => Mapping::unsupported(format!(
472 "Cohere takes thinking on or a token budget, not `{}`",
473 effort.as_str()
474 )),
475 Reasoning::Budget { tokens } => Mapping::Send(json!({
476 "thinking": {"type": "enabled", "token_budget": tokens},
477 })),
478 }),
479 cache: Mapping::of(fields.cache, |cache| match cache {
480 CacheRetention::None => Mapping::Omit("Cohere does not cache prompts"),
481 CacheRetention::Short | CacheRetention::Long => {
482 Mapping::unsupported("Cohere has no prompt cache")
483 }
484 }),
485 service_tier: Mapping::of(fields.service_tier, |_| {
486 Mapping::unsupported("Cohere's `priority` is a queue position, not a tier")
487 }),
488 verbosity: Mapping::of(fields.verbosity, |_| Mapping::unsupported(NO_FIELD)),
489 parallel_tool_calls: Mapping::of(fields.parallel_tool_calls, |_| {
490 Mapping::unsupported(NO_FIELD)
491 }),
492 top_p: Mapping::of(fields.top_p, |top_p| {
493 if (0.01..=0.99).contains(&top_p) {
494 Mapping::Send(json!({ "p": top_p }))
495 } else {
496 Mapping::unsupported("Cohere takes `p` from 0.01 to 0.99")
497 }
498 }),
499 seed: Mapping::of(fields.seed, |seed| Mapping::Send(json!({ "seed": seed }))),
500 stop: Mapping::of_stop(fields.stop, |stop| match stop.len() {
501 0..=5 => Mapping::Send(json!({ "stop_sequences": stop })),
502 _ => Mapping::unsupported("Cohere takes at most 5 stop sequences"),
503 }),
504 cache_key: Mapping::unrouted(fields.cache_key),
505 }
506 }
507
508 fn api(&self) -> crate::message::Api {
509 crate::message::Api::from_static(API)
510 }
511
512 fn facts(&self) -> Option<&ModelFacts> {
513 Some(&self.facts)
514 }
515
516 fn provider(&self) -> &str {
517 super::PROVIDER_NAME
518 }
519
520 fn model(&self) -> &str {
521 &self.model
522 }
523
524 fn accepts(&self, model: &str) -> crate::completion::Accepts {
527 crate::completion::Accepts {
528 user_images: self.facts.reads_images_or(
529 super::PROVIDER_NAME,
530 model,
531 super::reads_images,
532 ),
533 assistant_images: false,
534 tool_result_images: false,
535 tools: true,
536 foreign_reasoning: false,
537 }
538 }
539
540 fn encodes(&self, _model: &str, media: crate::completion::Media<'_>) -> bool {
543 use crate::completion::{Media, Place};
544 match media {
545 Media::Image(image, Place::User) => match &image.data {
546 Source::Url(_) => true,
547 Source::Base64(_) => image.media_type.is_some(),
548 _ => false,
549 },
550 Media::Document(document) => matches!(document.data, DocumentData::Text(_)),
551 Media::Image(..) | Media::Audio(_) | Media::Video(_) => false,
552 }
553 }
554
555 fn identity(&self, item: &Value) -> Map<String, Value> {
557 item.str("type")
558 .filter(|kind| *kind == PLAN)
559 .map(|kind| Map::from_iter([("type".to_owned(), Value::from(kind))]))
560 .unwrap_or_default()
561 }
562
563 fn call_id_slot(&self) -> Option<&'static str> {
564 Some("/id")
565 }
566
567 fn takes_documents(&self) -> bool {
568 true
569 }
570
571 fn sends_alone(&self, block: &AssistantContent) -> bool {
575 match block {
576 AssistantContent::Text(text) => !text.text.is_empty(),
577 AssistantContent::Reasoning(reasoning) => {
578 !reasoning.text.is_empty() && kind(block, self).as_deref() != Some(PLAN)
579 }
580 AssistantContent::Opaque(opaque) => opaque.item.get("type").is_some(),
581 AssistantContent::ToolCall(_) => true,
582 AssistantContent::Image(_) => false,
583 }
584 }
585}
586
587#[cfg(test)]
588mod tests;