Skip to main content

rig_core/providers/anthropic/
streaming.rs

1//! The Messages reply decoder, for a whole message and for the event stream
2//! alike. Each content block becomes one block, in wire order, whose
3//! provider item is the block as the provider states it complete, and only
4//! when the wire would take that item back.
5//!
6//! ```
7//! use rig_core::providers::anthropic::streaming::MessagesDecoder;
8//!
9//! let decoder = MessagesDecoder::new(false);
10//! # let _ = decoder;
11//! ```
12
13use std::collections::{BTreeMap, BTreeSet};
14
15use serde_json::{Map, Value, json};
16
17use super::completion::object;
18use crate::completion::FinishReason;
19use crate::error::ProviderError;
20use crate::json_utils::Lenient;
21use crate::message::{CallId, DocumentRange, Source, SourceLocation, ToolName};
22use crate::observe::ObservedError;
23use crate::operation::{Block, CallFragment, Completion, Finish};
24use crate::providers::internal::wire;
25use crate::wire::{
26    AdapterEvent, AdapterUsage, AdapterVerdict, Decoder, Flow, ObservationSink, Out, WireCitation,
27    WireEvent, WireFrame,
28};
29
30/// Recognized Messages event tags; any other tag classifies as unknown.
31/// `message` is the whole message a unary reply is.
32const KNOWN_EVENT_TYPES: &[&str] = &[
33    "message",
34    "message_start",
35    "content_block_start",
36    "content_block_delta",
37    "content_block_stop",
38    "message_delta",
39    "message_stop",
40    "ping",
41    "error",
42];
43
44/// One Messages event, or the whole message a unary reply is, as the
45/// provider sent it. The decoder reads each field it needs on its own, so
46/// an invented field, or a known one of another type, never fails a reply.
47#[derive(Debug, Clone, PartialEq)]
48pub struct MessagesEvent {
49    /// The event as sent.
50    pub fields: Value,
51    /// The frame's text when it is an `error` event, whose envelope is
52    /// reported verbatim.
53    raw: Option<String>,
54}
55
56impl MessagesEvent {
57    fn kind(&self) -> &str {
58        self.fields.str("type").unwrap_or_default()
59    }
60
61    /// The content block the event addresses.
62    fn index(&self) -> Result<usize, ProviderError> {
63        self.fields
64            .u64("index")
65            .and_then(|index| usize::try_from(index).ok())
66            .ok_or_else(|| {
67                ProviderError::Response(format!("Anthropic `{}` names no block index", self.kind()))
68            })
69    }
70
71    /// The block or delta under `key`, which must name its `type`.
72    fn item(&self, key: &str) -> Result<Map<String, Value>, ProviderError> {
73        match self.fields.get(key) {
74            Some(Value::Object(item)) if item.get("type").is_some_and(Value::is_string) => {
75                Ok(item.clone())
76            }
77            _ => Err(ProviderError::Response(format!(
78                "Anthropic `{}` carries no `{key}` with a string `type`",
79                self.kind()
80            ))),
81        }
82    }
83}
84
85/// Anthropic's usage counters, read leniently.
86#[derive(Debug, Clone, Default, PartialEq)]
87struct Counts {
88    input: Option<u64>,
89    output: Option<u64>,
90    cache_read: Option<u64>,
91    cache_creation: Option<u64>,
92    /// The per-TTL breakdown of `cache_creation`, as sent.
93    cache_creation_split: Option<Value>,
94    thinking: Option<u64>,
95}
96
97impl Counts {
98    fn of(usage: Option<&Value>) -> Self {
99        let count = |pointer: &str| usage?.at(pointer)?.as_u64_lenient();
100        Self {
101            input: count("/input_tokens"),
102            output: count("/output_tokens"),
103            cache_read: count("/cache_read_input_tokens"),
104            cache_creation: count("/cache_creation_input_tokens"),
105            cache_creation_split: usage
106                .and_then(|usage| usage.get("cache_creation"))
107                .filter(|split| split.is_object())
108                .cloned(),
109            thinking: count("/output_tokens_details/thinking_tokens"),
110        }
111    }
112
113    /// Rig's usage: its input is `input_tokens` plus the cache reads and
114    /// writes counted beside it, its output `output_tokens` (thinking
115    /// included), and its total their sum when both are known.
116    fn usage(&self) -> crate::completion::Usage {
117        let input = self.input.map(|uncached| {
118            uncached + self.cache_read.unwrap_or(0) + self.cache_creation.unwrap_or(0)
119        });
120        crate::completion::Usage {
121            input_tokens: input,
122            output_tokens: self.output,
123            cached_input_tokens: self.cache_read,
124            cache_creation_input_tokens: self.cache_creation,
125            reasoning_tokens: self.thinking,
126            total_tokens: input.zip(self.output).map(|(input, output)| input + output),
127            tool_use_prompt_tokens: None,
128            cost: None,
129        }
130    }
131}
132
133/// How a Messages `stop_reason` ends the turn, and the error a refusal
134/// reports. A reason Anthropic does not document is
135/// [`FinishReason::Other`], which fails the turn.
136fn finish_of(reason: &str, details: Option<&Value>) -> (Option<FinishReason>, Option<String>) {
137    let reason = match reason {
138        // `pause_turn` is a server-tool loop that stopped at its limit: the
139        // turn is replayed as it is to resume it (pi's rule).
140        "end_turn" | "stop_sequence" | "pause_turn" => FinishReason::Stop,
141        "max_tokens" | "model_context_window_exceeded" => FinishReason::Length,
142        "tool_use" => FinishReason::ToolCalls,
143        // A refusal fails the turn with its explanation (pi's rule).
144        "refusal" => {
145            let explanation = details
146                .and_then(|details| details.str("explanation"))
147                .filter(|explanation| !explanation.is_empty())
148                .unwrap_or("The model refused to complete the request");
149            return (
150                Some(FinishReason::ContentFilter),
151                Some(explanation.to_owned()),
152            );
153        }
154        other => FinishReason::Other(other.to_owned()),
155    };
156    (Some(reason), None)
157}
158
159/// One Messages citation as a whole-block [`WireCitation`]: Anthropic
160/// cites each text block as a whole. A location kind rig does not know, or
161/// one without the field that names its source, is `None` and stays only in
162/// the block's native item, as does `encrypted_index`. Document ranges end
163/// exclusive, pages count from 1, characters and blocks from 0.
164fn citation_of(citation: &Value) -> Option<WireCitation> {
165    let number = |key: &str| citation.u64(key).and_then(|n| u32::try_from(n).ok());
166    let range = |start: &str, end: &str| Some(number(start)?..number(end)?);
167    let location = match citation.str("type")? {
168        "char_location" => SourceLocation::Document {
169            index: number("document_index"),
170            id: citation.str("file_id").map(str::to_owned),
171            within: citation
172                .u64("start_char_index")
173                .zip(citation.u64("end_char_index"))
174                .map(|(start, end)| DocumentRange::Chars(start..end)),
175        },
176        "page_location" => SourceLocation::Document {
177            index: number("document_index"),
178            id: citation.str("file_id").map(str::to_owned),
179            within: range("start_page_number", "end_page_number").map(DocumentRange::Pages),
180        },
181        "content_block_location" => SourceLocation::Document {
182            index: number("document_index"),
183            id: citation.str("file_id").map(str::to_owned),
184            within: range("start_block_index", "end_block_index").map(DocumentRange::Blocks),
185        },
186        "search_result_location" => SourceLocation::SearchResult {
187            index: number("search_result_index")?,
188            source: citation.str("source")?.to_owned(),
189            blocks: range("start_block_index", "end_block_index"),
190        },
191        "web_search_result_location" => SourceLocation::Url {
192            url: citation.str("url")?.to_owned(),
193        },
194        _ => return None,
195    };
196    let mut source = Source::new(location);
197    if let Some(title) = citation
198        .str("document_title")
199        .or_else(|| citation.str("title"))
200    {
201        source = source.title(title);
202    }
203    if let Some(cited) = citation.str("cited_text") {
204        source = source.cited_text(cited);
205    }
206    Some(WireCitation::new(None, vec![source]))
207}
208
209/// What an open content block is, for the checks its end makes.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211enum Kind {
212    Text,
213    Thinking,
214    Redacted,
215    Call,
216    Opaque,
217}
218
219/// Decodes Messages replies, a whole message or a stream of events.
220/// `content_block_stop` states a block complete; a stop reason states every
221/// block still open complete, but a call whose input is not yet JSON.
222#[derive(Debug, Default)]
223pub struct MessagesDecoder {
224    /// Each open block's kind and the input JSON streamed to it.
225    open: BTreeMap<usize, (Kind, String)>,
226    /// Every index a block opened at, closed or not.
227    started: BTreeSet<usize>,
228    /// Whether a block other than a leading `fallback` marker opened.
229    opened: bool,
230    /// Whether the dialect takes thinking back without a signature.
231    unsigned_thinking: bool,
232    /// The counters `message_start` reported, for a terminal `message_delta`
233    /// that does not repeat them.
234    start: Counts,
235    message_id: Option<String>,
236    response_model: Option<String>,
237    container: Option<Value>,
238}
239
240impl MessagesDecoder {
241    /// A fresh decoder for one reply. `unsigned_thinking` is whether the
242    /// dialect takes thinking back without a signature.
243    pub fn new(unsigned_thinking: bool) -> Self {
244        Self {
245            unsigned_thinking,
246            ..Self::default()
247        }
248    }
249
250    /// Open the content block at `index` as the provider states it.
251    fn start(
252        &mut self,
253        index: usize,
254        block: Map<String, Value>,
255        out: &mut Out<'_, Completion>,
256    ) -> Result<(), ProviderError> {
257        self.started.insert(index);
258        let block = Value::Object(block);
259        let kind = block.str("type").unwrap_or_default();
260        // A leading `fallback` names the model that took over; one after
261        // output began is a fallback rig cannot represent (pi's rule).
262        if kind == "fallback" {
263            if self.opened {
264                return Err(ProviderError::Response(
265                    "Anthropic performed an unsupported mid-output model fallback".to_owned(),
266                ));
267            }
268            self.open.insert(index, (Kind::Opaque, String::new()));
269            return out.open(index, Block::Opaque { replay: false }, block);
270        }
271        self.opened = true;
272        let (opened, kind, text) = match kind {
273            "text" => (Block::Text, Kind::Text, block.str("text")),
274            "thinking" => (
275                Block::Reasoning { redacted: false },
276                Kind::Thinking,
277                block.str("thinking"),
278            ),
279            "redacted_thinking" => (Block::Reasoning { redacted: true }, Kind::Redacted, None),
280            "tool_use" => {
281                self.open.insert(index, (Kind::Call, String::new()));
282                let id = block.str("id").unwrap_or_default().to_owned();
283                let input = block.get("input").cloned().unwrap_or_default();
284                match ToolName::new(block.str("name").unwrap_or_default()) {
285                    Ok(name) => {
286                        let id = CallId::from_wire(&id);
287                        out.open(index, Block::Call { id, name }, block)?;
288                    }
289                    // A nameless call: the writer drops it with a warning.
290                    Err(_) => out.fragment(
291                        Some(index),
292                        CallFragment {
293                            id: Some(&id),
294                            ..CallFragment::default()
295                        },
296                    )?,
297                }
298                // A whole reply states the input on the block; a stream
299                // streams it, and the fragments win.
300                return out.announce(index, input);
301            }
302            _ => (Block::Opaque { replay: true }, Kind::Opaque, None),
303        };
304        let text = text.unwrap_or_default().to_owned();
305        let citations: Vec<WireCitation> = match kind {
306            Kind::Text => block
307                .arr("citations")
308                .iter()
309                .filter_map(citation_of)
310                .collect(),
311            _ => Vec::new(),
312        };
313        self.open.insert(index, (kind, String::new()));
314        out.open(index, opened, block)?;
315        for citation in citations {
316            out.cite(index, citation);
317        }
318        out.push(index, &text)
319    }
320
321    /// Apply a delta to the open block at `index`: text and reasoning grow
322    /// both the block and its item, input JSON is assembled, a citation is
323    /// appended to the item and cited, and any other delta merges into the
324    /// item by key.
325    fn delta(
326        &mut self,
327        index: usize,
328        delta: Map<String, Value>,
329        out: &mut Out<'_, Completion>,
330    ) -> Result<(), ProviderError> {
331        let kind = delta
332            .get("type")
333            .and_then(Value::as_str)
334            .unwrap_or_default();
335        // The fragment a block is built from must be text.
336        let fragment = |key: &str| {
337            delta.get(key).and_then(Value::as_str).ok_or_else(|| {
338                ProviderError::Response(format!("Anthropic `{kind}` carries no string `{key}`"))
339            })
340        };
341        // A gateway that skips `content_block_start` still streams the
342        // block's text, so the first delta opens it.
343        if !self.started.contains(&index) {
344            let opened = match kind {
345                "text_delta" => Some(json!({"type": "text", "text": ""})),
346                "thinking_delta" => Some(json!({"type": "thinking", "thinking": ""})),
347                _ => None,
348            };
349            if let Some(Value::Object(block)) = opened {
350                self.start(index, block, out)?;
351            }
352        }
353        match kind {
354            "text_delta" => out.push(index, fragment("text")?)?,
355            "thinking_delta" => out.push(index, fragment("thinking")?)?,
356            "input_json_delta" => {
357                let fragment = fragment("partial_json")?;
358                let Some((kind @ (Kind::Call | Kind::Opaque), json)) = self.open.get_mut(&index)
359                else {
360                    return Err(ProviderError::Response(format!(
361                        "Anthropic streamed input to content block {index}, which takes none"
362                    )));
363                };
364                json.push_str(fragment);
365                if *kind == Kind::Call {
366                    out.push(index, fragment)?;
367                }
368                return Ok(());
369            }
370            "citations_delta" => {
371                let citation = delta.get("citation").cloned().unwrap_or_default();
372                let cited = citation_of(&citation);
373                out.edit(index, |item| {
374                    if let Some(item) = item.as_object_mut() {
375                        match item.get_mut("citations") {
376                            Some(Value::Array(citations)) => citations.push(citation),
377                            _ => {
378                                item.insert("citations".to_owned(), Value::Array(vec![citation]));
379                            }
380                        }
381                    }
382                })?;
383                if let Some(cited) = cited {
384                    out.cite(index, cited);
385                }
386                return Ok(());
387            }
388            _ => {}
389        }
390        // Text and signatures concatenate; `compaction_delta` and kinds
391        // rig has never seen land in the item too.
392        out.edit(index, |item| {
393            crate::operation::completion::merge(item, &delta)
394        })
395    }
396
397    /// End the block at `index` as stated complete. Its item becomes the
398    /// block's native only when the wire takes it back: text that is not
399    /// blank, thinking with its signature (unless the dialect takes it
400    /// unsigned), redacted thinking with its data, and a call with an
401    /// object `input`, set from what streamed. A hosted item whose input is
402    /// not an object never completed.
403    fn stop(&mut self, index: usize, out: &mut Out<'_, Completion>) -> Result<(), ProviderError> {
404        let Some((kind, json)) = self.open.remove(&index) else {
405            return out.finish(index);
406        };
407        let streamed = (!json.is_empty()).then(|| crate::json_utils::parse_tool_arguments(&json));
408        let unsigned = self.unsigned_thinking;
409        let mut complete = true;
410        out.edit(index, |item| {
411            let input = match streamed {
412                Some(Ok(parsed)) => Ok(Some(parsed)),
413                Some(Err(_)) => Err(()),
414                None => Ok(item.get("input").cloned()),
415            };
416            let kept = match (kind, input) {
417                (Kind::Text, _) => !item.str("text").unwrap_or_default().trim().is_empty(),
418                (Kind::Thinking, _) => {
419                    unsigned || item.str("signature").is_some_and(|s| !s.is_empty())
420                }
421                (Kind::Redacted, _) => item.str("data").is_some_and(|data| !data.is_empty()),
422                (Kind::Opaque, Ok(None)) => true,
423                (Kind::Call | Kind::Opaque, Ok(Some(input @ Value::Object(_)))) => {
424                    crate::operation::completion::merge(item, &object([("input", Some(input))]));
425                    true
426                }
427                (Kind::Call, Ok(None | Some(Value::Null))) => {
428                    crate::operation::completion::merge(
429                        item,
430                        &object([("input", Some(json!({})))]),
431                    );
432                    true
433                }
434                (Kind::Call, _) => false,
435                (Kind::Opaque, _) => {
436                    complete = false;
437                    true
438                }
439            };
440            if !kept {
441                *item = Value::Null;
442            }
443        })?;
444        if complete {
445            out.finish(index)
446        } else {
447            out.close(index)
448        }
449    }
450
451    /// Note the metadata a whole message or `message_start` states.
452    fn metadata(&mut self, message: &Value) {
453        self.start = Counts::of(message.get("usage"));
454        self.message_id = message.str("id").map(str::to_owned);
455        self.response_model = message.str("model").map(str::to_owned);
456        self.note_container(message.get("container"));
457    }
458
459    fn note_container(&mut self, container: Option<&Value>) {
460        self.container = container
461            .filter(|c| !c.is_null())
462            .or(self.container.as_ref())
463            .cloned();
464    }
465
466    /// End the reply with Anthropic's terminal record. Every block still
467    /// open is complete, but a call whose input is not JSON yet: the end of
468    /// the reply closes it unfinished. The container the reply ran in is a
469    /// last opaque block.
470    fn end(
471        &mut self,
472        usage: &Counts,
473        stop_reason: Option<&str>,
474        details: Option<&Value>,
475        mut out: Out<'_, Completion>,
476    ) -> Result<Flow, ProviderError> {
477        let open: Vec<usize> = self
478            .open
479            .iter()
480            .filter(|(_, (kind, json))| {
481                *kind != Kind::Call
482                    || json.is_empty()
483                    || crate::json_utils::parse_tool_arguments(json).is_ok_and(|v| v.is_object())
484            })
485            .map(|(index, _)| *index)
486            .collect();
487        for index in open {
488            self.stop(index, &mut out)?;
489        }
490        if let Some(container) = &self.container {
491            let index = out.fresh_index();
492            let item = json!({ "type": "container", "container": container });
493            out.whole(index, Block::Opaque { replay: true }, item, "")?;
494        }
495        let (reason, error) = stop_reason.map_or((None, None), |reason| finish_of(reason, details));
496        Ok(out.end(Finish {
497            usage: usage.usage(),
498            reason,
499            response_id: self.message_id.clone(),
500            model: self.response_model.clone(),
501            error,
502        }))
503    }
504
505    /// A whole message, written block by block through the calls a stream
506    /// makes, then ended. Empty content is a turn like any other, as it is
507    /// streamed (pi's rule): the stop reason decides how it ends.
508    fn whole(
509        &mut self,
510        message: Value,
511        mut out: Out<'_, Completion>,
512    ) -> Result<Flow, ProviderError> {
513        self.metadata(&message);
514        for (index, block) in message.arr("content").iter().enumerate() {
515            let Some(block) = block
516                .as_object()
517                .filter(|block| block.get("type").is_some_and(Value::is_string))
518            else {
519                return Err(ProviderError::Response(format!(
520                    "Anthropic content block {index} has no string `type`"
521                )));
522            };
523            self.start(index, block.clone(), &mut out)?;
524            self.stop(index, &mut out)?;
525        }
526        let usage = self.start.clone();
527        self.end(
528            &usage,
529            message.str("stop_reason"),
530            message.get("stop_details"),
531            out,
532        )
533    }
534
535    /// The stream's terminal `message_delta` counters, falling back to
536    /// `message_start`'s for those it omits.
537    fn terminal(&self, usage: Option<&Value>) -> Counts {
538        let (terminal, start) = (Counts::of(usage), self.start.clone());
539        Counts {
540            // Zero-as-missing is a gateway heuristic for the input count
541            // only, not a rule for cache counts.
542            input: terminal.input.filter(|tokens| *tokens > 0).or(start.input),
543            cache_read: terminal.cache_read.or(start.cache_read),
544            cache_creation: terminal.cache_creation.or(start.cache_creation),
545            cache_creation_split: terminal.cache_creation_split.or(start.cache_creation_split),
546            ..terminal
547        }
548    }
549}
550
551impl<'id> Decoder<'id, Completion> for MessagesDecoder {
552    type Event = MessagesEvent;
553
554    fn classify(&self, frame: WireFrame) -> WireEvent<MessagesEvent> {
555        let data = frame.as_str();
556        wire::classify_tagged_frame::<Value>(&data, "type", |tag| KNOWN_EVENT_TYPES.contains(&tag))
557            .map(|fields| {
558                // The one event whose payload leaves this crate as bytes rather
559                // than as decoded fields, so it is captured where the frame is
560                // still in hand.
561                let raw = (fields.str("type") == Some("error")).then(|| data.to_string());
562                MessagesEvent { fields, raw }
563            })
564    }
565
566    fn decode(
567        &mut self,
568        event: MessagesEvent,
569        mut out: Out<'id, Completion>,
570    ) -> Result<Flow, ProviderError> {
571        match event.kind() {
572            "message" => return self.whole(event.fields, out),
573            // A `message_start` without a message body (a Bedrock-compatible
574            // gateway sends one) is a no-op.
575            "message_start" => {
576                if let Some(message) = event.fields.get("message").filter(|m| m.is_object()) {
577                    self.metadata(message);
578                }
579            }
580            "content_block_start" => {
581                let block = event.item("content_block")?;
582                self.start(event.index()?, block, &mut out)?;
583            }
584            "content_block_delta" => {
585                let delta = event.item("delta")?;
586                self.delta(event.index()?, delta, &mut out)?;
587            }
588            "content_block_stop" => self.stop(event.index()?, &mut out)?,
589            "message_delta" => {
590                let delta = event.fields.get("delta");
591                self.note_container(delta.and_then(|delta| delta.get("container")));
592                // Only a `message_delta` carrying a stop reason is the
593                // provider's end; without one it is a no-op.
594                let Some(reason) = delta.and_then(|delta| delta.str("stop_reason")) else {
595                    return Ok(Flow::More);
596                };
597                let usage = self.terminal(event.fields.get("usage"));
598                let details = delta.and_then(|delta| delta.get("stop_details"));
599                return self.end(&usage, Some(reason), details, out);
600            }
601            // Preserve the complete error envelope rather than re-encode
602            // modeled fields.
603            "error" => {
604                return Err(ProviderError::from_provider_body(
605                    event.raw.unwrap_or_default(),
606                ));
607            }
608            // `message_stop`, `ping`, and nothing else: the classifier
609            // passes only the listed tags.
610            _ => {}
611        }
612        Ok(Flow::More)
613    }
614}
615
616impl MessagesDecoder {
617    /// Messages metadata projected before normalization can discard it: the
618    /// stop reason, the model, the message id, the usage and any error
619    /// envelope, on the unary reply and on the stream's `message_start`,
620    /// `message_delta` and `error` events.
621    pub(crate) fn project(payload: &[u8], sink: &mut ObservationSink<'_>) {
622        let Ok(payload) = serde_json::from_slice::<Value>(payload) else {
623            return;
624        };
625        let fields = payload
626            .get("message")
627            .filter(|m| m.is_object())
628            .unwrap_or(&payload);
629        // Anthropic reports the prompt on `message_start` and the answer's
630        // running total on each `message_delta`: each is a snapshot of what it
631        // knows, never a sum.
632        if let Some(usage) = payload.get("usage").or_else(|| fields.get("usage")) {
633            let counts = Counts::of(Some(usage));
634            sink.emit(AdapterEvent::Usage {
635                usage: AdapterUsage {
636                    input_tokens: counts.input,
637                    output_tokens: counts.output,
638                    total_tokens: None,
639                    cached_input_tokens: counts.cache_read,
640                    reasoning_tokens: counts.thinking,
641                    tool_input_tokens: None,
642                },
643            });
644        }
645        let stop_reason = fields
646            .str("stop_reason")
647            .or_else(|| payload.at("/delta/stop_reason").and_then(Value::as_str));
648        let verdict = AdapterVerdict {
649            finish_reason: stop_reason.map(|reason| sink.scrub(reason)),
650            block_reason: None,
651            detail: None,
652            model: fields.str("model").map(|model| sink.scrub(model)),
653        };
654        let response_id = fields.str("id").map(|id| sink.scrub(id));
655        sink.provider(verdict, response_id);
656        if let Some(error) = payload
657            .get("error")
658            .and_then(|error| serde::Deserialize::deserialize(error).ok())
659        {
660            ObservedError::emit(error, sink);
661        }
662    }
663}
664
665pub(crate) mod document;
666
667#[cfg(test)]
668mod tests;