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.catalog_cost(&billed);
363        }
364        Ok(out.end(Finish {
365            usage,
366            reason: reason.map(finish_of),
367            response_id: self.message_id.clone(),
368            model: None,
369            error,
370        }))
371    }
372
373    /// A whole reply, written block by block through the calls a stream
374    /// makes, then ended.
375    fn whole(
376        &mut self,
377        reply: &Value,
378        mut out: Out<'_, Completion>,
379    ) -> Result<Flow, ProviderError> {
380        self.message_id = reply.str("id").map(str::to_owned);
381        // A `message` string is Cohere's error body, sent with a success
382        // status.
383        let message = match reply.get("message") {
384            Some(Value::String(_)) => {
385                return Err(ProviderError::from_provider_body(reply.to_string()));
386            }
387            message => message.unwrap_or(&Value::Null),
388        };
389        // The plan leads, as it streams first.
390        if let Some(plan) = message.str(PLAN).filter(|plan| !plan.is_empty()) {
391            self.plan(plan, &mut out)?;
392            self.stop(PLAN_INDEX, &mut out)?;
393        }
394        // A part or call that is not an object states nothing to keep.
395        for (index, part) in message.arr("content").iter().enumerate() {
396            if let Some(part) = part.as_object() {
397                self.content(index, part.clone(), &mut out)?;
398                self.stop(index, &mut out)?;
399            }
400        }
401        for (index, call) in message.arr("tool_calls").iter().enumerate() {
402            if let Some(call) = call.as_object() {
403                self.call(index, call.clone(), &mut out)?;
404                self.stop(CALLS + index, &mut out)?;
405            }
406        }
407        for citation in message.arr("citations") {
408            self.cite(citation.clone(), &mut out)?;
409        }
410        self.end(reply.get("usage"), reply.str("finish_reason"), None, out)
411    }
412}
413
414/// How a native `finish_reason` ends the turn. `ERROR`, `TIMEOUT` and any
415/// reason Cohere does not document are [`FinishReason::Other`], which fails
416/// the turn.
417fn finish_of(reason: &str) -> FinishReason {
418    match reason {
419        "COMPLETE" | "STOP_SEQUENCE" => FinishReason::Stop,
420        "MAX_TOKENS" => FinishReason::Length,
421        "TOOL_CALL" => FinishReason::ToolCalls,
422        other => FinishReason::Other(other.to_owned()),
423    }
424}
425
426/// One native citation as a [`WireCitation`]: a span of its content part
427/// in characters, checked against the `text` Cohere quotes, cited by each
428/// document or tool output it names. A citation without both offsets is
429/// `None` and stays only in the item.
430fn citation_of(citation: &Value) -> Option<WireCitation> {
431    let mut span = WireSpan::new(
432        citation.u64("start")?,
433        citation.u64("end")?,
434        SpanUnit::Chars,
435    );
436    if let Some(text) = citation.str("text") {
437        span = span.quoted(text);
438    }
439    let sources = citation
440        .arr("sources")
441        .iter()
442        .filter_map(|source| {
443            let id = source.str("id")?.to_owned();
444            match source.str("type") {
445                Some("document") => {
446                    let cited = Source::new(SourceLocation::Document {
447                        index: None,
448                        id: Some(id),
449                        within: None,
450                    });
451                    Some(match source.at("/document/title").and_then(Value::as_str) {
452                        Some(title) => cited.title(title),
453                        None => cited,
454                    })
455                }
456                Some("tool") => Some(Source::new(SourceLocation::ToolOutput { id })),
457                _ => None,
458            }
459        })
460        .collect();
461    Some(WireCitation::new(Some(span), sources))
462}
463
464/// The `billed_units` Cohere charges for, as usage to price: input and
465/// output, which leave out the tokens Cohere adds and does not bill.
466/// `None` unless both are reported.
467fn billed_of(usage: Option<&Value>) -> Option<Usage> {
468    let count = |pointer: &str| usage?.at(pointer)?.as_u64_lenient();
469    let input = count("/billed_units/input_tokens")?;
470    let output = count("/billed_units/output_tokens")?;
471    Some(Usage::new().input_tokens(input).output_tokens(output))
472}
473
474/// Rig's usage from Cohere's: the `tokens` the model read and wrote,
475/// else the `billed_units`, with `cached_tokens` among the input.
476fn usage_of(usage: Option<&Value>) -> Usage {
477    let count = |pointer: &str| usage?.at(pointer)?.as_u64_lenient();
478    let input = count("/tokens/input_tokens").or_else(|| count("/billed_units/input_tokens"));
479    let output = count("/tokens/output_tokens").or_else(|| count("/billed_units/output_tokens"));
480    Usage {
481        input_tokens: input,
482        output_tokens: output,
483        cached_input_tokens: count("/cached_tokens"),
484        cache_creation_input_tokens: None,
485        reasoning_tokens: count("/tokens/reasoning_tokens"),
486        total_tokens: input.zip(output).map(|(input, output)| input + output),
487        tool_use_prompt_tokens: None,
488        cost: None,
489    }
490}
491
492impl<'id> Decoder<'id, Completion> for ChatDecoder {
493    type Event = ChatEvent;
494
495    /// A stream event by its `type`; a frame without one is the whole
496    /// reply, recognized by its `message`.
497    fn classify(&self, frame: WireFrame) -> WireEvent<ChatEvent> {
498        let data = frame.as_str();
499        wire::classify_or_untagged(
500            &data,
501            "type",
502            |tag| KNOWN_EVENT_TYPES.contains(&tag),
503            |fields| ChatEvent { fields },
504            |data| {
505                wire::classify_marker_keyed_frame(data, &["message"])
506                    .map(|fields| ChatEvent { fields })
507            },
508        )
509    }
510
511    fn decode(
512        &mut self,
513        event: ChatEvent,
514        mut out: Out<'id, Completion>,
515    ) -> Result<Flow, ProviderError> {
516        match event.kind() {
517            "" => return self.whole(&event.fields, out),
518            "message-start" => self.message_id = event.fields.str("id").map(str::to_owned),
519            "content-start" => {
520                let part = event.message("content").cloned().unwrap_or_default();
521                self.content(event.index()?, part, &mut out)?;
522            }
523            "content-delta" => {
524                let empty = Map::new();
525                let delta = event.message("content").unwrap_or(&empty);
526                self.grow(event.index()?, delta, &mut out)?;
527            }
528            "content-end" => self.stop(event.index()?, &mut out)?,
529            "tool-plan-delta" => {
530                if let Some(text) = event
531                    .fields
532                    .at("/delta/message/tool_plan")
533                    .and_then(Value::as_str)
534                {
535                    self.plan(text, &mut out)?;
536                }
537            }
538            "tool-call-start" => {
539                let call = event.message("tool_calls").cloned().unwrap_or_default();
540                self.call(event.index()?, call, &mut out)?;
541            }
542            "tool-call-delta" => {
543                let fragment = event
544                    .fields
545                    .at("/delta/message/tool_calls/function/arguments")
546                    .and_then(Value::as_str)
547                    .unwrap_or_default();
548                self.arguments(CALLS + event.index()?, fragment, &mut out)?;
549            }
550            "tool-call-end" => self.stop(CALLS + event.index()?, &mut out)?,
551            "citation-start" => {
552                if let Some(citation) = event.message("citations") {
553                    self.cite(Value::Object(citation.clone()), &mut out)?;
554                }
555            }
556            "message-end" => {
557                let delta = event.fields.get("delta");
558                return self.end(
559                    delta.and_then(|delta| delta.get("usage")),
560                    delta.and_then(|delta| delta.str("finish_reason")),
561                    delta.and_then(|delta| delta.str("error")),
562                    out,
563                );
564            }
565            // `citation-end` and `debug`: the classifier passes only the
566            // listed tags.
567            _ => {}
568        }
569        Ok(Flow::More)
570    }
571}
572
573impl ChatDecoder {
574    /// Native chat metadata projected before normalization can discard it:
575    /// the finish reason and the reply id, on the unary reply
576    /// and on the stream's `message-start` and `message-end` events.
577    pub(crate) fn project(payload: &[u8], sink: &mut ObservationSink<'_>) {
578        let Ok(payload) = serde_json::from_slice::<Value>(payload) else {
579            return;
580        };
581        let end = payload.get("delta").unwrap_or(&payload);
582        let verdict = AdapterVerdict {
583            finish_reason: end.str("finish_reason").map(|reason| sink.scrub(reason)),
584            block_reason: None,
585            detail: None,
586            model: None,
587        };
588        let response_id = payload.str("id").map(|id| sink.scrub(id));
589        sink.provider(verdict, response_id);
590    }
591}
592
593pub(crate) mod document;
594
595#[cfg(test)]
596mod tests;