Skip to main content

rig_core/providers/cohere/
streaming.rs

1//! The native chat reply decoder, for a whole reply and for the event
2//! stream alike. Each content part, the tool plan and each tool call
3//! becomes one block in the order it arrives. A block's provider item is
4//! the part as Cohere stated it, with the citations that point at it, so
5//! citations and the tool plan round-trip through history.
6//!
7//! ```
8//! use rig_core::providers::cohere::streaming::ChatDecoder;
9//!
10//! let decoder = ChatDecoder::default();
11//! # let _ = decoder;
12//! ```
13
14use std::collections::BTreeMap;
15
16use serde_json::{Map, Value, json};
17
18use super::chat::PLAN;
19use crate::completion::{FinishReason, Usage};
20use crate::error::ProviderError;
21use crate::json_utils::Lenient;
22use crate::message::{CallId, Source, SourceLocation, ToolName};
23use crate::operation::{Block, CallFragment, Completion, Finish};
24use crate::providers::internal::wire;
25use crate::wire::{
26    AdapterVerdict, Decoder, Flow, ObservationSink, Out, SpanUnit, WireCitation, WireEvent,
27    WireFrame, WireSpan,
28};
29
30/// The stream's event tags; any other tag classifies as unknown.
31const KNOWN_EVENT_TYPES: &[&str] = &[
32    "message-start",
33    "content-start",
34    "content-delta",
35    "content-end",
36    "tool-plan-delta",
37    "tool-call-start",
38    "tool-call-delta",
39    "tool-call-end",
40    "citation-start",
41    "citation-end",
42    "message-end",
43    "debug",
44];
45
46/// The wire index of the tool plan. Content parts keep their own indices,
47/// and calls theirs from [`CALLS`] on.
48const PLAN_INDEX: usize = 1 << 21;
49
50/// The wire index of the first tool call. A content or call index Cohere
51/// states must stay below it.
52const CALLS: usize = 1 << 20;
53
54/// `index` as Cohere stated it for a content part or call, failing when it
55/// would reach the indices the decoder keeps for calls and the plan.
56fn checked(index: usize) -> Result<usize, ProviderError> {
57    if index < CALLS {
58        Ok(index)
59    } else {
60        Err(ProviderError::Response(format!(
61            "Cohere stated index {index}, past the {CALLS} parts or calls a reply may hold"
62        )))
63    }
64}
65
66/// One stream event, or the whole reply a unary call answers (`kind` is
67/// then empty), as Cohere sent it.
68#[derive(Debug, Clone, PartialEq)]
69pub struct ChatEvent {
70    /// The event as sent.
71    pub fields: Value,
72}
73
74impl ChatEvent {
75    fn kind(&self) -> &str {
76        self.fields.str("type").unwrap_or_default()
77    }
78
79    /// The part or call the event addresses.
80    fn index(&self) -> Result<usize, ProviderError> {
81        self.fields
82            .u64("index")
83            .and_then(|index| usize::try_from(index).ok())
84            .ok_or_else(|| {
85                ProviderError::Response(format!("Cohere `{}` names no index", self.kind()))
86            })
87            .and_then(checked)
88    }
89
90    /// The object at `pointer` under the event's `delta.message`.
91    fn message(&self, key: &str) -> Option<&Map<String, Value>> {
92        self.fields
93            .get("delta")?
94            .get("message")?
95            .get(key)?
96            .as_object()
97    }
98}
99
100/// What an open block is.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102enum Kind {
103    Text,
104    Thinking,
105    Plan,
106    Call,
107    Opaque,
108}
109
110/// Decodes native chat replies, a whole reply or a stream of events. An
111/// `*-end` event states its block complete, and `message-end` states every
112/// block still open complete, but a call whose arguments are not JSON.
113#[derive(Debug, Default)]
114pub struct ChatDecoder {
115    /// Each open block's kind and, for a call, the argument text streamed.
116    open: BTreeMap<usize, (Kind, String)>,
117    /// Every index a block opened at and its kind, so a citation finds its
118    /// block.
119    started: BTreeMap<usize, Kind>,
120    message_id: Option<String>,
121}
122
123impl ChatDecoder {
124    /// Open the content part at `index` as Cohere states it.
125    fn content(
126        &mut self,
127        index: usize,
128        part: Map<String, Value>,
129        out: &mut Out<'_, Completion>,
130    ) -> Result<(), ProviderError> {
131        let index = checked(index)?;
132        let part = Value::Object(part);
133        let (kind, block, key) = match part.str("type") {
134            Some("text") | None => (Kind::Text, Block::Text, "text"),
135            Some("thinking") => (
136                Kind::Thinking,
137                Block::Reasoning { redacted: false },
138                "thinking",
139            ),
140            // A part rig does not know goes back as it came.
141            Some(_) => (Kind::Opaque, Block::Opaque { replay: true }, ""),
142        };
143        let text = part.str(key).unwrap_or_default().to_owned();
144        self.open.insert(index, (kind, String::new()));
145        self.started.insert(index, kind);
146        out.open(index, block, part)?;
147        out.push(index, &text)
148    }
149
150    /// Grow the open part at `index`, and its item, by a delta's text.
151    fn grow(
152        &mut self,
153        index: usize,
154        delta: &Map<String, Value>,
155        out: &mut Out<'_, Completion>,
156    ) -> Result<(), ProviderError> {
157        let Some((kind, _)) = self.open.get(&index) else {
158            return Err(ProviderError::Response(format!(
159                "Cohere streamed content to part {index}, which is not open"
160            )));
161        };
162        let key = match kind {
163            Kind::Thinking => "thinking",
164            Kind::Plan => PLAN,
165            Kind::Text | Kind::Call => "text",
166            // An unknown part's deltas merge into its item.
167            Kind::Opaque => {
168                return out.edit(index, |item| {
169                    crate::operation::completion::merge(item, delta)
170                });
171            }
172        };
173        let Some(text) = delta.get(key).and_then(Value::as_str) else {
174            return Ok(());
175        };
176        out.push(index, text)?;
177        let delta = Map::from_iter([(key.to_owned(), Value::from(text))]);
178        out.edit(index, |item| {
179            crate::operation::completion::merge(item, &delta)
180        })
181    }
182
183    /// Append a fragment of the tool plan, opening it at its first.
184    fn plan(&mut self, text: &str, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
185        if !self.started.contains_key(&PLAN_INDEX) {
186            self.open.insert(PLAN_INDEX, (Kind::Plan, String::new()));
187            self.started.insert(PLAN_INDEX, Kind::Plan);
188            let item = json!({"type": PLAN, PLAN: ""});
189            out.open(PLAN_INDEX, Block::Reasoning { redacted: false }, item)?;
190        }
191        let delta = Map::from_iter([(PLAN.to_owned(), Value::from(text))]);
192        self.grow(PLAN_INDEX, &delta, out)
193    }
194
195    /// Open the tool call at `index` from its stated id, name and the
196    /// arguments it opens with.
197    fn call(
198        &mut self,
199        index: usize,
200        call: Map<String, Value>,
201        out: &mut Out<'_, Completion>,
202    ) -> Result<(), ProviderError> {
203        // The plan is whole once the calls it plans begin.
204        if self.open.contains_key(&PLAN_INDEX) {
205            self.stop(PLAN_INDEX, out)?;
206        }
207        let index = CALLS + checked(index)?;
208        let item = Value::Object(call);
209        let id = item.str("id").unwrap_or_default().to_owned();
210        let arguments = item
211            .at("/function/arguments")
212            .and_then(Value::as_str)
213            .unwrap_or_default()
214            .to_owned();
215        self.open.insert(index, (Kind::Call, String::new()));
216        self.started.insert(index, Kind::Call);
217        match ToolName::new(
218            item.at("/function/name")
219                .and_then(Value::as_str)
220                .unwrap_or_default(),
221        ) {
222            Ok(name) => {
223                let id = CallId::from_wire(&id);
224                out.open(index, Block::Call { id, name }, item)?;
225            }
226            // A nameless call: the writer drops it with a warning.
227            Err(_) => out.fragment(
228                Some(index),
229                CallFragment {
230                    id: Some(&id),
231                    ..CallFragment::default()
232                },
233            )?,
234        }
235        self.arguments(index, &arguments, out)
236    }
237
238    /// Append argument text to the open call at `index`.
239    fn arguments(
240        &mut self,
241        index: usize,
242        fragment: &str,
243        out: &mut Out<'_, Completion>,
244    ) -> Result<(), ProviderError> {
245        let Some((Kind::Call, json)) = self.open.get_mut(&index) else {
246            return Err(ProviderError::Response(format!(
247                "Cohere streamed arguments to call {}, which is not open",
248                index.saturating_sub(CALLS)
249            )));
250        };
251        json.push_str(fragment);
252        out.push(index, fragment)
253    }
254
255    /// Attach `citation` to the item of the block it cites: the tool plan,
256    /// or the content part at its `content_index` (the first by default).
257    /// A citation of a text part is also cited on its block. A citation of
258    /// a block that never opened has nowhere to go, and one whose
259    /// `content_index` reaches [`CALLS`] fails the reply.
260    fn cite(&self, citation: Value, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
261        let index = if citation.str("type") == Some("PLAN") {
262            PLAN_INDEX
263        } else {
264            checked(
265                citation
266                    .u64("content_index")
267                    .map_or(Ok(0), usize::try_from)
268                    .unwrap_or(usize::MAX),
269            )?
270        };
271        let Some(kind) = self.started.get(&index) else {
272            tracing::warn!(
273                index,
274                "Cohere cited a block the reply never opened; dropping it"
275            );
276            return Ok(());
277        };
278        if *kind == Kind::Text
279            && let Some(cited) = citation_of(&citation)
280        {
281            out.cite(index, cited);
282        }
283        out.edit(index, |item| {
284            if let Some(item) = item.as_object_mut() {
285                match item.get_mut("citations") {
286                    Some(Value::Array(citations)) => citations.push(citation),
287                    _ => {
288                        item.insert("citations".to_owned(), Value::Array(vec![citation]));
289                    }
290                }
291            }
292        })
293    }
294
295    /// End the block at `index` as stated complete. Its item becomes the
296    /// block's native unless it would replay nothing: blank text, or a call
297    /// whose arguments are not a JSON object.
298    fn stop(&mut self, index: usize, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
299        let Some((kind, json)) = self.open.remove(&index) else {
300            return Err(ProviderError::Response(format!(
301                "Cohere ended block {index}, which is not open"
302            )));
303        };
304        out.edit(index, |item| {
305            let kept = match kind {
306                Kind::Text => !item.str("text").unwrap_or_default().trim().is_empty(),
307                Kind::Thinking | Kind::Plan | Kind::Opaque => true,
308                Kind::Call => {
309                    let arguments = if json.trim().is_empty() {
310                        "{}"
311                    } else {
312                        json.as_str()
313                    };
314                    let object = crate::json_utils::parse_tool_arguments(arguments)
315                        .is_ok_and(|parsed| parsed.is_object());
316                    if let Some(function) = item.get_mut("function").and_then(Value::as_object_mut)
317                    {
318                        function.insert("arguments".to_owned(), Value::from(arguments));
319                    }
320                    object
321                }
322            };
323            if !kept {
324                *item = Value::Null;
325            }
326        })?;
327        out.finish(index)
328    }
329
330    /// End the reply: every block still open is complete, but a call whose
331    /// arguments are not JSON yet, which the end closes unfinished. Its
332    /// cost is the `billed_units` priced at the catalog, since the `tokens`
333    /// it reports as usage include tokens Cohere does not bill.
334    fn end(
335        &mut self,
336        usage_value: Option<&Value>,
337        reason: Option<&str>,
338        error: Option<&str>,
339        mut out: Out<'_, Completion>,
340    ) -> Result<Flow, ProviderError> {
341        let open: Vec<usize> = self
342            .open
343            .iter()
344            .filter(|(_, (kind, json))| {
345                *kind != Kind::Call
346                    || json.trim().is_empty()
347                    || crate::json_utils::parse_tool_arguments(json).is_ok_and(|v| v.is_object())
348            })
349            .map(|(index, _)| *index)
350            .collect();
351        for index in open {
352            self.stop(index, &mut out)?;
353        }
354        let error = error
355            .filter(|error| !error.is_empty())
356            .map(str::to_owned)
357            .or_else(|| {
358                (reason == Some("ERROR")).then(|| "Cohere ended the reply with an error".to_owned())
359            });
360        let mut usage = usage_of(usage_value);
361        if let Some(billed) = billed_of(usage_value) {
362            usage.cost = out
363                .catalog_pricing(None)
364                .and_then(|pricing| pricing.cost(&billed));
365        }
366        Ok(out.end(Finish {
367            usage,
368            reason: reason.map(finish_of),
369            response_id: self.message_id.clone(),
370            model: None,
371            error,
372        }))
373    }
374
375    /// A whole reply, written block by block through the calls a stream
376    /// makes, then ended.
377    fn whole(
378        &mut self,
379        reply: &Value,
380        mut out: Out<'_, Completion>,
381    ) -> Result<Flow, ProviderError> {
382        self.message_id = reply.str("id").map(str::to_owned);
383        // A `message` string is Cohere's error body, sent with a success
384        // status.
385        let message = match reply.get("message") {
386            Some(Value::String(_)) => {
387                return Err(ProviderError::from_provider_body(reply.to_string()));
388            }
389            message => message.unwrap_or(&Value::Null),
390        };
391        // The plan leads, as it streams first.
392        if let Some(plan) = message.str(PLAN).filter(|plan| !plan.is_empty()) {
393            self.plan(plan, &mut out)?;
394            self.stop(PLAN_INDEX, &mut out)?;
395        }
396        // A part or call that is not an object states nothing to keep.
397        for (index, part) in message.arr("content").iter().enumerate() {
398            if let Some(part) = part.as_object() {
399                self.content(index, part.clone(), &mut out)?;
400                self.stop(index, &mut out)?;
401            }
402        }
403        for (index, call) in message.arr("tool_calls").iter().enumerate() {
404            if let Some(call) = call.as_object() {
405                self.call(index, call.clone(), &mut out)?;
406                self.stop(CALLS + index, &mut out)?;
407            }
408        }
409        for citation in message.arr("citations") {
410            self.cite(citation.clone(), &mut out)?;
411        }
412        self.end(reply.get("usage"), reply.str("finish_reason"), None, out)
413    }
414}
415
416/// How a native `finish_reason` ends the turn. `ERROR`, `TIMEOUT` and any
417/// reason Cohere does not document are [`FinishReason::Other`], which fails
418/// the turn.
419fn finish_of(reason: &str) -> FinishReason {
420    match reason {
421        "COMPLETE" | "STOP_SEQUENCE" => FinishReason::Stop,
422        "MAX_TOKENS" => FinishReason::Length,
423        "TOOL_CALL" => FinishReason::ToolCalls,
424        other => FinishReason::Other(other.to_owned()),
425    }
426}
427
428/// One native citation as a [`WireCitation`]: a span of its content part
429/// in characters, checked against the `text` Cohere quotes, cited by each
430/// document or tool output it names. A citation without both offsets is
431/// `None` and stays only in the item.
432fn citation_of(citation: &Value) -> Option<WireCitation> {
433    let mut span = WireSpan::new(
434        citation.u64("start")?,
435        citation.u64("end")?,
436        SpanUnit::Chars,
437    );
438    if let Some(text) = citation.str("text") {
439        span = span.quoted(text);
440    }
441    let sources = citation
442        .arr("sources")
443        .iter()
444        .filter_map(|source| {
445            let id = source.str("id")?.to_owned();
446            match source.str("type") {
447                Some("document") => {
448                    let cited = Source::new(SourceLocation::Document {
449                        index: None,
450                        id: Some(id),
451                        within: None,
452                    });
453                    Some(match source.at("/document/title").and_then(Value::as_str) {
454                        Some(title) => cited.title(title),
455                        None => cited,
456                    })
457                }
458                Some("tool") => Some(Source::new(SourceLocation::ToolOutput { id })),
459                _ => None,
460            }
461        })
462        .collect();
463    Some(WireCitation::new(Some(span), sources))
464}
465
466/// The `billed_units` Cohere charges for, as usage to price: input and
467/// output, which leave out the tokens Cohere adds and does not bill.
468/// `None` unless both are reported.
469fn billed_of(usage: Option<&Value>) -> Option<Usage> {
470    let count = |pointer: &str| usage?.at(pointer)?.as_u64_lenient();
471    let input = count("/billed_units/input_tokens")?;
472    let output = count("/billed_units/output_tokens")?;
473    Some(Usage::new().input_tokens(input).output_tokens(output))
474}
475
476/// Rig's usage from Cohere's: the `tokens` the model read and wrote,
477/// else the `billed_units`, with `cached_tokens` among the input.
478fn usage_of(usage: Option<&Value>) -> Usage {
479    let count = |pointer: &str| usage?.at(pointer)?.as_u64_lenient();
480    let input = count("/tokens/input_tokens").or_else(|| count("/billed_units/input_tokens"));
481    let output = count("/tokens/output_tokens").or_else(|| count("/billed_units/output_tokens"));
482    Usage {
483        input_tokens: input,
484        output_tokens: output,
485        cached_input_tokens: count("/cached_tokens"),
486        cache_creation_input_tokens: None,
487        reasoning_tokens: count("/tokens/reasoning_tokens"),
488        total_tokens: input.zip(output).map(|(input, output)| input + output),
489        tool_use_prompt_tokens: None,
490        cost: None,
491    }
492}
493
494impl<'id> Decoder<'id, Completion> for ChatDecoder {
495    type Event = ChatEvent;
496
497    /// A stream event by its `type`; a frame without one is the whole
498    /// reply, recognized by its `message`.
499    fn classify(&self, frame: WireFrame) -> WireEvent<ChatEvent> {
500        let data = frame.as_str();
501        wire::classify_or_untagged(
502            &data,
503            "type",
504            |tag| KNOWN_EVENT_TYPES.contains(&tag),
505            |fields| ChatEvent { fields },
506            |data| {
507                wire::classify_marker_keyed_frame(data, &["message"])
508                    .map(|fields| ChatEvent { fields })
509            },
510        )
511    }
512
513    fn decode(
514        &mut self,
515        event: ChatEvent,
516        mut out: Out<'id, Completion>,
517    ) -> Result<Flow, ProviderError> {
518        match event.kind() {
519            "" => return self.whole(&event.fields, out),
520            "message-start" => self.message_id = event.fields.str("id").map(str::to_owned),
521            "content-start" => {
522                let part = event.message("content").cloned().unwrap_or_default();
523                self.content(event.index()?, part, &mut out)?;
524            }
525            "content-delta" => {
526                let empty = Map::new();
527                let delta = event.message("content").unwrap_or(&empty);
528                self.grow(event.index()?, delta, &mut out)?;
529            }
530            "content-end" => self.stop(event.index()?, &mut out)?,
531            "tool-plan-delta" => {
532                if let Some(text) = event
533                    .fields
534                    .at("/delta/message/tool_plan")
535                    .and_then(Value::as_str)
536                {
537                    self.plan(text, &mut out)?;
538                }
539            }
540            "tool-call-start" => {
541                let call = event.message("tool_calls").cloned().unwrap_or_default();
542                self.call(event.index()?, call, &mut out)?;
543            }
544            "tool-call-delta" => {
545                let fragment = event
546                    .fields
547                    .at("/delta/message/tool_calls/function/arguments")
548                    .and_then(Value::as_str)
549                    .unwrap_or_default();
550                self.arguments(CALLS + event.index()?, fragment, &mut out)?;
551            }
552            "tool-call-end" => self.stop(CALLS + event.index()?, &mut out)?,
553            "citation-start" => {
554                if let Some(citation) = event.message("citations") {
555                    self.cite(Value::Object(citation.clone()), &mut out)?;
556                }
557            }
558            "message-end" => {
559                let delta = event.fields.get("delta");
560                return self.end(
561                    delta.and_then(|delta| delta.get("usage")),
562                    delta.and_then(|delta| delta.str("finish_reason")),
563                    delta.and_then(|delta| delta.str("error")),
564                    out,
565                );
566            }
567            // `citation-end` and `debug`: the classifier passes only the
568            // listed tags.
569            _ => {}
570        }
571        Ok(Flow::More)
572    }
573}
574
575impl ChatDecoder {
576    /// Native chat metadata projected before normalization can discard it:
577    /// the finish reason and the reply id, on the unary reply
578    /// and on the stream's `message-start` and `message-end` events.
579    pub(crate) fn project(payload: &[u8], sink: &mut ObservationSink<'_>) {
580        let Ok(payload) = serde_json::from_slice::<Value>(payload) else {
581            return;
582        };
583        let end = payload.get("delta").unwrap_or(&payload);
584        let verdict = AdapterVerdict {
585            finish_reason: end.str("finish_reason").map(|reason| sink.scrub(reason)),
586            block_reason: None,
587            detail: None,
588            model: None,
589        };
590        let response_id = payload.str("id").map(|id| sink.scrub(id));
591        sink.provider(verdict, response_id);
592    }
593}
594
595pub(crate) mod document;
596
597#[cfg(test)]
598mod tests;