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