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