Skip to main content

rig_core/providers/anthropic/
streaming.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4use super::completion::{CompletionResponse, Content, anthropic_usage_totals, map_finish_reason};
5use crate::error::ProviderError;
6use crate::message::ReasoningContent;
7use crate::observe::ObservedError;
8use crate::operation::{
9    CallFragment, Completion, Finish, IfMalformed, ReasoningPart, Seal, TextPart,
10};
11use crate::providers::internal::wire;
12use crate::wire::{
13    AdapterEvent, AdapterUsage, AdapterVerdict, Decoder, Flow, ObservationSink, Out, WireEvent,
14    WireFrame,
15};
16use std::collections::HashMap;
17
18/// Recognized Messages event tags. Listed events must decode fully;
19/// unlisted tags classify as unknown. Novel nested delta tags remain
20/// [`ContentDelta::Unknown`].
21const KNOWN_EVENT_TYPES: &[&str] = &[
22    // Unary replies use the same classifier with a whole-message tag.
23    "message",
24    "message_start",
25    "content_block_start",
26    "content_block_delta",
27    "content_block_stop",
28    "message_delta",
29    "message_stop",
30    "ping",
31    "error",
32];
33
34#[derive(Debug, Deserialize)]
35#[serde(tag = "type", rename_all = "snake_case")]
36pub enum StreamingEvent {
37    MessageStart {
38        /// Initial message metadata. Absent or null messages are accepted as no-ops.
39        #[serde(default)]
40        message: Option<CompletionResponse>,
41    },
42    /// The whole message: what the endpoint answers when not streaming.
43    /// Its fields are exactly `message_start`'s, plus the stop reason and
44    /// usage a stream delivers on `message_delta`.
45    Message {
46        #[serde(flatten)]
47        message: CompletionResponse,
48    },
49    ContentBlockStart {
50        index: usize,
51        content_block: Content,
52    },
53    ContentBlockDelta {
54        index: usize,
55        delta: ContentDelta,
56    },
57    ContentBlockStop {
58        index: usize,
59    },
60    MessageDelta {
61        delta: MessageDelta,
62        usage: PartialUsage,
63    },
64    MessageStop,
65    /// Keep-alive; a Known no-op, not an unknown event to warn about.
66    Ping,
67    /// A provider error envelope with a required nested `error` field.
68    Error {
69        /// Required error payload used to validate the envelope shape.
70        #[allow(dead_code)]
71        error: serde_json::Value,
72        /// Original envelope bytes attached during classification.
73        /// Preserve sibling fields and key order in the reported provider error.
74        #[serde(skip)]
75        raw: String,
76    },
77}
78
79#[derive(Debug)]
80pub enum ContentDelta {
81    TextDelta {
82        text: String,
83    },
84    InputJsonDelta {
85        partial_json: String,
86    },
87    ThinkingDelta {
88        thinking: String,
89    },
90    SignatureDelta {
91        signature: String,
92    },
93    CitationsDelta {
94        citation: super::completion::Citation,
95    },
96    /// An unrecognized nested delta tag, preserved for a warning and skipped.
97    Unknown(serde_json::Value),
98}
99
100/// Decode known delta tags strictly and preserve unrecognized string tags.
101/// Reject non-object values, missing or non-string tags, and malformed known payloads.
102impl<'de> Deserialize<'de> for ContentDelta {
103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104    where
105        D: serde::Deserializer<'de>,
106    {
107        let value = serde_json::Value::deserialize(deserializer)?;
108        // Non-object values are malformed, not novel delta kinds.
109        if !value.is_object() {
110            return Err(serde::de::Error::custom("content delta must be an object"));
111        }
112        let str_field = |tag: &str, field: &str| -> Result<String, D::Error> {
113            value
114                .get(field)
115                .and_then(serde_json::Value::as_str)
116                .map(ToOwned::to_owned)
117                .ok_or_else(|| {
118                    serde::de::Error::custom(format!(
119                        "`{tag}` content delta is missing a string `{field}` field"
120                    ))
121                })
122        };
123        match value.get("type").cloned() {
124            Some(serde_json::Value::String(tag)) => match tag.as_str() {
125                "text_delta" => Ok(Self::TextDelta {
126                    text: str_field("text_delta", "text")?,
127                }),
128                "input_json_delta" => Ok(Self::InputJsonDelta {
129                    partial_json: str_field("input_json_delta", "partial_json")?,
130                }),
131                "thinking_delta" => Ok(Self::ThinkingDelta {
132                    thinking: str_field("thinking_delta", "thinking")?,
133                }),
134                "signature_delta" => Ok(Self::SignatureDelta {
135                    signature: str_field("signature_delta", "signature")?,
136                }),
137                "citations_delta" => {
138                    let citation = value.get("citation").cloned().ok_or_else(|| {
139                        serde::de::Error::custom(
140                            "`citations_delta` content delta is missing a `citation` field",
141                        )
142                    })?;
143                    Ok(Self::CitationsDelta {
144                        citation: serde_json::from_value(citation)
145                            .map_err(serde::de::Error::custom)?,
146                    })
147                }
148                _ => Ok(Self::Unknown(value)),
149            },
150            Some(_) => Err(serde::de::Error::custom(
151                "content delta `type` must be a string",
152            )),
153            // Missing tags must not silently turn discarded content into successful output.
154            None => Err(serde::de::Error::custom(
155                "content delta is missing a `type` field",
156            )),
157        }
158    }
159}
160
161#[derive(Debug, Deserialize)]
162pub struct MessageDelta {
163    pub stop_reason: Option<String>,
164    pub stop_sequence: Option<String>,
165}
166
167#[derive(Debug, Deserialize, Clone, Serialize, Default)]
168pub struct PartialUsage {
169    pub output_tokens: usize,
170    #[serde(default)]
171    pub input_tokens: Option<usize>,
172    #[serde(default)]
173    pub cache_creation_input_tokens: Option<u64>,
174    /// Per-TTL breakdown of `cache_creation_input_tokens`. Anthropic reports
175    /// it on `message_start`, not the terminal `message_delta`; the adapter
176    /// carries it forward onto the terminal usage.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub cache_creation: Option<super::completion::CacheCreation>,
179    #[serde(default)]
180    pub cache_read_input_tokens: Option<u64>,
181    /// Output-token breakdown reported by the terminal `message_delta`.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub output_tokens_details: Option<super::completion::OutputTokensDetails>,
184}
185
186impl From<&PartialUsage> for crate::completion::Usage {
187    fn from(value: &PartialUsage) -> crate::completion::Usage {
188        anthropic_usage_totals(
189            value.input_tokens.map(|tokens| tokens as u64),
190            value.output_tokens as u64,
191            value.cache_read_input_tokens,
192            value.cache_creation_input_tokens,
193            value.output_tokens_details,
194        )
195    }
196}
197
198impl From<PartialUsage> for crate::completion::Usage {
199    fn from(value: PartialUsage) -> crate::completion::Usage {
200        (&value).into()
201    }
202}
203
204// Hosted-tool input is assembled locally because it becomes raw text-block
205// metadata rather than an executable tool call.
206struct ServerToolUseState {
207    name: String,
208    id: String,
209    initial_input: Value,
210    input_json: String,
211}
212
213/// Decode Messages replies, a whole message or a stream of events.
214/// EOF without a `message_delta` stop reason is truncation.
215pub struct MessagesDecoder<'id> {
216    /// The text part of each open content block.
217    texts: HashMap<usize, TextPart<'id>>,
218    /// The thinking part of each open content block, and its signature:
219    /// fragments win over the opening value, and an absent one is `None`.
220    thinking: HashMap<usize, (ReasoningPart<'id>, String, String)>,
221    /// The content block of the open client tool call, whose index its
222    /// fragments are buffered under.
223    current_tool_call: Option<usize>,
224    server_tool_uses: HashMap<usize, ServerToolUseState>,
225    input_tokens: u64,
226    /// Per-TTL cache-write breakdown from `message_start`; the terminal
227    /// `message_delta` usage omits it.
228    cache_creation: Option<super::completion::CacheCreation>,
229    /// Cache reads and writes from `message_start`, for a terminal
230    /// `message_delta` that does not repeat them: rig's input counts them.
231    cache_read_input_tokens: Option<u64>,
232    cache_creation_input_tokens: Option<u64>,
233    message_id: Option<String>,
234    response_model: Option<String>,
235}
236
237impl Default for MessagesDecoder<'_> {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243impl MessagesDecoder<'_> {
244    /// A fresh decoder for one reply.
245    pub fn new() -> Self {
246        Self {
247            texts: HashMap::new(),
248            thinking: HashMap::new(),
249            current_tool_call: None,
250            server_tool_uses: HashMap::new(),
251            input_tokens: 0,
252            cache_creation: None,
253            cache_read_input_tokens: None,
254            cache_creation_input_tokens: None,
255            message_id: None,
256            response_model: None,
257        }
258    }
259}
260
261impl<'id> MessagesDecoder<'id> {
262    /// The content-block frames: `content_block_start` / `_delta` / `_stop`.
263    fn interpret_content(
264        &mut self,
265        event: StreamingEvent,
266        out: &mut Out<'id, Completion>,
267    ) -> Result<(), ProviderError> {
268        match event {
269            StreamingEvent::ContentBlockDelta { index, delta } => match delta {
270                ContentDelta::TextDelta { text } => {
271                    if self.current_tool_call.is_none() {
272                        let part = self.texts.entry(index).or_insert_with(|| out.text());
273                        out.push_text(part, &text);
274                    }
275                }
276                ContentDelta::InputJsonDelta { partial_json } => {
277                    if let Some(server_tool_use) = self.server_tool_uses.get_mut(&index) {
278                        server_tool_use.input_json.push_str(&partial_json);
279                        return Ok(());
280                    }
281                    if let Some(call) = self.current_tool_call {
282                        out.call_fragment(
283                            call,
284                            CallFragment {
285                                arguments: Some(partial_json.as_str()),
286                                ..CallFragment::default()
287                            },
288                        )?;
289                    }
290                }
291                ContentDelta::ThinkingDelta { thinking } => {
292                    let (part, _, _) = self
293                        .thinking
294                        .entry(index)
295                        .or_insert_with(|| (out.reasoning(), String::new(), String::new()));
296                    out.push_reasoning(part, &thinking);
297                }
298                ContentDelta::SignatureDelta { signature } => {
299                    let (_, fragments, _) = self
300                        .thinking
301                        .entry(index)
302                        .or_insert_with(|| (out.reasoning(), String::new(), String::new()));
303                    // The completed signature closes the thinking part.
304                    fragments.push_str(&signature);
305                }
306                ContentDelta::CitationsDelta { citation } => {
307                    if let Some(params) = crate::message::AdditionalParams::from_entries([(
308                        "citations",
309                        json!([citation]),
310                    )]) {
311                        let part = self.texts.entry(index).or_insert_with(|| out.text());
312                        out.text_params(part, params);
313                    }
314                }
315                ContentDelta::Unknown(value) => {
316                    // Log only the tag; unknown payloads may contain sensitive model output.
317                    tracing::warn!(
318                        delta_type = value.get("type").and_then(serde_json::Value::as_str),
319                        "skipping unrecognized Anthropic content delta type"
320                    );
321                }
322            },
323            StreamingEvent::ContentBlockStart {
324                index,
325                content_block,
326            } => match content_block {
327                // Text arrives through deltas; cache_control is request-only metadata.
328                Content::Text {
329                    text: _,
330                    citations,
331                    cache_control: _,
332                } => {
333                    let part = out.text();
334                    if let Some(params) = crate::message::AdditionalParams::from_entries(
335                        (!citations.is_empty()).then(|| ("citations", json!(citations))),
336                    ) {
337                        out.text_params(&part, params);
338                    }
339                    self.texts.insert(index, part);
340                }
341                Content::ServerToolUse { id, name, input } => {
342                    self.server_tool_uses.insert(
343                        index,
344                        ServerToolUseState {
345                            name,
346                            id,
347                            initial_input: input,
348                            input_json: String::new(),
349                        },
350                    );
351                }
352                raw @ (Content::WebSearchToolResult { .. }
353                | Content::CodeExecutionToolResult { .. }) => {
354                    if let Some(params) = crate::message::AdditionalParams::from_entries([(
355                        super::completion::ANTHROPIC_RAW_CONTENT_KEY,
356                        json!(raw),
357                    )]) {
358                        let part = out.text();
359                        out.text_params(&part, params);
360                        self.texts.insert(index, part);
361                    }
362                }
363                Content::ToolUse { id, name, .. } => {
364                    self.current_tool_call = Some(index);
365                    out.call_fragment(
366                        index,
367                        CallFragment {
368                            id: Some(id.as_str()),
369                            name: Some(name.as_str()),
370                            ..CallFragment::default()
371                        },
372                    )?;
373                }
374                Content::Thinking {
375                    thinking,
376                    signature,
377                } => {
378                    // Adaptive thinking may carry only a signature, so the
379                    // part opens even when the opening text is empty.
380                    let part = out.reasoning();
381                    out.push_reasoning(&part, &thinking);
382                    self.thinking
383                        .insert(index, (part, String::new(), signature.unwrap_or_default()));
384                }
385                Content::RedactedThinking { data } => {
386                    out.reasoning_block(crate::message::Reasoning {
387                        id: None,
388                        content: vec![ReasoningContent::Redacted { data }],
389                    });
390                }
391                // Request-side content kinds; an assistant stream never
392                // opens a block with them, and there is nothing to write.
393                Content::Image { .. } | Content::ToolResult { .. } | Content::Document { .. } => {}
394            },
395            StreamingEvent::ContentBlockStop { index } => {
396                // Signature-only thinking parts carry provider state required
397                // for replay.
398                if let Some((part, fragments, initial)) = self.thinking.remove(&index) {
399                    let signature = if fragments.is_empty() {
400                        initial
401                    } else {
402                        fragments
403                    };
404                    out.close_reasoning(
405                        part,
406                        Seal {
407                            signature: (!signature.is_empty()).then_some(signature),
408                            ..Seal::default()
409                        },
410                    );
411                    return Ok(());
412                }
413
414                if let Some(server_tool_use) = self.server_tool_uses.remove(&index) {
415                    let input = if server_tool_use.input_json.is_empty() {
416                        if server_tool_use.initial_input.is_null() {
417                            json!({})
418                        } else {
419                            server_tool_use.initial_input
420                        }
421                    } else {
422                        serde_json::from_str(&server_tool_use.input_json)?
423                    };
424                    if let Some(params) = crate::message::AdditionalParams::from_entries([(
425                        super::completion::ANTHROPIC_RAW_CONTENT_KEY,
426                        json!(Content::ServerToolUse {
427                            id: server_tool_use.id,
428                            name: server_tool_use.name,
429                            input,
430                        }),
431                    )]) {
432                        let part = out.text();
433                        out.text_params(&part, params);
434                        out.close_text(part);
435                    }
436                    return Ok(());
437                }
438
439                // `content_block_stop` promises a complete block: empty input
440                // finalizes to `{}`, and malformed input fails the reply.
441                if self.current_tool_call == Some(index) {
442                    self.current_tool_call = None;
443                    out.close_pending(index, IfMalformed::Fail)?;
444                    return Ok(());
445                }
446
447                if let Some(part) = self.texts.remove(&index) {
448                    out.close_text(part);
449                }
450            }
451            StreamingEvent::Message { .. }
452            | StreamingEvent::MessageStart { .. }
453            | StreamingEvent::MessageDelta { .. }
454            | StreamingEvent::MessageStop
455            | StreamingEvent::Ping
456            | StreamingEvent::Error { .. } => {}
457        }
458        Ok(())
459    }
460
461    /// A whole message, written block by block as a stream states it, then
462    /// ended. Empty content is refused unless the stop reason is `end_turn`,
463    /// or `stop_sequence` with a reported sequence.
464    fn interpret_whole_message(
465        &mut self,
466        message: CompletionResponse,
467        mut out: Out<'id, Completion>,
468    ) -> Result<Flow, ProviderError> {
469        self.input_tokens = message.usage.input_tokens;
470        self.cache_creation
471            .clone_from(&message.usage.cache_creation);
472        self.message_id = Some(message.id);
473        self.response_model = Some(message.model);
474
475        // An empty end_turn and a stripped stop sequence are valid answers.
476        let legal_empty_turn = match message.stop_reason.as_deref() {
477            Some("end_turn") => true,
478            Some("stop_sequence") => message.stop_sequence.is_some(),
479            _ => false,
480        };
481        if message.content.is_empty() && !legal_empty_turn {
482            return Err(ProviderError::Response(
483                crate::message::EMPTY_RESPONSE_ERROR.to_owned(),
484            ));
485        }
486
487        for (index, content) in message.content.into_iter().enumerate() {
488            // The payload a stream delivers by delta, for the part kinds
489            // that have one. Everything else is carried by the block's
490            // start frame alone.
491            let delta = match &content {
492                Content::Text { text, .. } if !text.is_empty() => {
493                    Some(ContentDelta::TextDelta { text: text.clone() })
494                }
495                Content::ToolUse { input, .. } => Some(ContentDelta::InputJsonDelta {
496                    partial_json: input.to_string(),
497                }),
498                _ => None,
499            };
500            self.interpret_content(
501                StreamingEvent::ContentBlockStart {
502                    index,
503                    content_block: content,
504                },
505                &mut out,
506            )?;
507            if let Some(delta) = delta {
508                self.interpret_content(
509                    StreamingEvent::ContentBlockDelta { index, delta },
510                    &mut out,
511                )?;
512            }
513            self.interpret_content(StreamingEvent::ContentBlockStop { index }, &mut out)?;
514        }
515
516        // A whole message completes the turn even without an explicit stop
517        // reason. Its `raw` is the message itself.
518        let usage = PartialUsage {
519            output_tokens: message.usage.output_tokens as usize,
520            input_tokens: usize::try_from(message.usage.input_tokens).ok(),
521            cache_creation_input_tokens: message.usage.cache_creation_input_tokens,
522            cache_creation: message.usage.cache_creation,
523            cache_read_input_tokens: message.usage.cache_read_input_tokens,
524            output_tokens_details: message.usage.output_tokens_details,
525        };
526        let native = StreamingCompletionResponse {
527            usage,
528            stop_reason: message.stop_reason,
529            stop_sequence: message.stop_sequence,
530            message_id: self.message_id.clone(),
531            model: self.response_model.clone(),
532        };
533        Ok(out.end(finish_of(&native)))
534    }
535}
536
537impl<'id> Decoder<'id, Completion> for MessagesDecoder<'id> {
538    type Event = StreamingEvent;
539
540    fn classify(&self, frame: WireFrame) -> WireEvent<StreamingEvent> {
541        let data = frame.as_str();
542        wire::classify_tagged_frame(&data, "type", |event_type| {
543            KNOWN_EVENT_TYPES.contains(&event_type)
544        })
545        .map(|event| match event {
546            // The one event whose payload leaves this crate as bytes rather
547            // than as decoded fields, so it is captured where the frame is
548            // still in hand: serde never sees the text it parsed.
549            StreamingEvent::Error { error, .. } => StreamingEvent::Error {
550                error,
551                raw: data.to_string(),
552            },
553            other => other,
554        })
555    }
556
557    fn decode(
558        &mut self,
559        event: StreamingEvent,
560        mut out: Out<'id, Completion>,
561    ) -> Result<Flow, ProviderError> {
562        match event {
563            StreamingEvent::Message { message } => self.interpret_whole_message(message, out),
564            StreamingEvent::MessageStart { message } => {
565                // Bedrock-compat quirk: a `message_start` without a message
566                // body is a no-op, not an error.
567                if let Some(message) = message {
568                    self.input_tokens = message.usage.input_tokens;
569                    self.cache_creation
570                        .clone_from(&message.usage.cache_creation);
571                    self.cache_read_input_tokens = message.usage.cache_read_input_tokens;
572                    self.cache_creation_input_tokens = message.usage.cache_creation_input_tokens;
573                    self.message_id = Some(message.id.clone());
574                    self.response_model = Some(message.model.clone());
575                }
576                Ok(Flow::More)
577            }
578            StreamingEvent::MessageDelta { delta, usage } => {
579                // Only a `message_delta` carrying a stop reason is the
580                // provider's end; without one it is a no-op.
581                let Some(reason) = delta.stop_reason else {
582                    return Ok(Flow::More);
583                };
584                // Prefer a positive terminal input count, falling back to message_start;
585                // zero-as-missing is a gateway heuristic, not a rule for cache counts.
586                let usage = PartialUsage {
587                    output_tokens: usage.output_tokens,
588                    input_tokens: usage
589                        .input_tokens
590                        .filter(|tokens| *tokens > 0)
591                        .or_else(|| usize::try_from(self.input_tokens).ok()),
592                    // A terminal frame that omits the cache counters keeps
593                    // `message_start`'s, so input still counts the cache.
594                    cache_creation_input_tokens: usage
595                        .cache_creation_input_tokens
596                        .or(self.cache_creation_input_tokens),
597                    cache_creation: usage.cache_creation.or(self.cache_creation),
598                    cache_read_input_tokens: usage
599                        .cache_read_input_tokens
600                        .or(self.cache_read_input_tokens),
601                    // The terminal frame owns the output count and its breakdown.
602                    output_tokens_details: usage.output_tokens_details,
603                };
604                let native = StreamingCompletionResponse {
605                    usage,
606                    stop_reason: Some(reason),
607                    // Rides the same `message_delta` as the stop reason, and
608                    // only that frame carries it: `message_start` always
609                    // opens with `null`.
610                    stop_sequence: delta.stop_sequence,
611                    message_id: self.message_id.clone(),
612                    model: self.response_model.clone(),
613                };
614                out.raw(serde_json::to_value(&native)?);
615                Ok(out.end(finish_of(&native)))
616            }
617            StreamingEvent::Error { raw, .. } => {
618                // Preserve the complete error envelope rather than re-encode modeled fields.
619                Err(crate::error::ProviderError::from_provider_body(raw))
620            }
621            event @ (StreamingEvent::ContentBlockStart { .. }
622            | StreamingEvent::ContentBlockDelta { .. }
623            | StreamingEvent::ContentBlockStop { .. }
624            | StreamingEvent::MessageStop
625            | StreamingEvent::Ping) => {
626                self.interpret_content(event, &mut out)?;
627                Ok(Flow::More)
628            }
629        }
630    }
631}
632
633impl MessagesDecoder<'_> {
634    /// Messages metadata projected before normalization can discard it: the
635    /// stop reason, the model, the message id, the usage and any error
636    /// envelope, on the unary reply and on the stream's `message_start`,
637    /// `message_delta` and `error` events.
638    pub(crate) fn project(payload: &[u8], sink: &mut ObservationSink<'_>) {
639        let Ok(payload) = serde_json::from_slice::<ObservedPayload>(payload) else {
640            return;
641        };
642        let usage = payload.usage;
643        let (id, model, stop_reason, nested_usage) = match payload.message {
644            Some(message) => (
645                message.id,
646                message.model,
647                message.stop_reason,
648                message.usage,
649            ),
650            None => (payload.id, payload.model, payload.stop_reason, None),
651        };
652        // Anthropic reports the prompt on `message_start` and the answer's
653        // running total on each `message_delta`: each is a snapshot of what it
654        // knows, never a sum.
655        if let Some(usage) = usage.or(nested_usage) {
656            sink.emit(AdapterEvent::Usage {
657                usage: AdapterUsage {
658                    input_tokens: usage.input_tokens,
659                    output_tokens: usage.output_tokens,
660                    total_tokens: None,
661                    cached_input_tokens: usage.cache_read_input_tokens,
662                    reasoning_tokens: usage
663                        .output_tokens_details
664                        .map(|details| details.thinking_tokens),
665                    tool_input_tokens: None,
666                },
667            });
668        }
669        let stop_reason = stop_reason.or(payload.delta.and_then(|delta| delta.stop_reason));
670        let verdict = AdapterVerdict {
671            finish_reason: stop_reason.map(|v| sink.scrub(&v)),
672            block_reason: None,
673            detail: None,
674            model: model.map(|v| sink.scrub(&v)),
675        };
676        let response_id = id.map(|v| sink.scrub(&v));
677        sink.provider(verdict, response_id);
678        if let Some(error) = payload.error {
679            error.emit(sink);
680        }
681    }
682}
683
684/// Observation fields from unary replies and stream events, including nested messages.
685#[derive(Deserialize)]
686struct ObservedPayload {
687    id: Option<String>,
688    model: Option<String>,
689    stop_reason: Option<String>,
690    usage: Option<ObservedUsage>,
691    message: Option<Box<ObservedPayload>>,
692    delta: Option<ObservedDelta>,
693    error: Option<ObservedError>,
694}
695
696#[derive(Deserialize)]
697struct ObservedUsage {
698    #[serde(default, deserialize_with = "crate::observe::lenient_count")]
699    input_tokens: Option<u64>,
700    #[serde(default, deserialize_with = "crate::observe::lenient_count")]
701    output_tokens: Option<u64>,
702    #[serde(default, deserialize_with = "crate::observe::lenient_count")]
703    cache_read_input_tokens: Option<u64>,
704    #[serde(default)]
705    output_tokens_details: Option<ObservedOutputDetails>,
706}
707
708#[derive(Deserialize)]
709struct ObservedOutputDetails {
710    #[serde(default)]
711    thinking_tokens: u64,
712}
713
714#[derive(Deserialize)]
715struct ObservedDelta {
716    stop_reason: Option<String>,
717}
718
719/// Anthropic's own terminal stream record, a streamed response's `raw`:
720/// callers who want the provider-native shape deserialize it from there.
721#[derive(Clone, Debug, Default, Deserialize, Serialize)]
722pub struct StreamingCompletionResponse {
723    /// Token usage carried by the terminal `message_delta` event.
724    pub usage: PartialUsage,
725    /// Anthropic's `stop_reason`, verbatim, when the stream reported one.
726    #[serde(default, skip_serializing_if = "Option::is_none")]
727    pub stop_reason: Option<String>,
728    /// Matched stop sequence reported by the terminal frame, preserved verbatim.
729    /// The provider strips this sequence from output text.
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub stop_sequence: Option<String>,
732    /// The `message_start` message ID, when the stream reported one.
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub message_id: Option<String>,
735    /// The model named by `message_start`, when the stream reported one.
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub model: Option<String>,
738}
739
740/// The provider's end of the reply, from Anthropic's terminal record.
741fn finish_of(response: &StreamingCompletionResponse) -> Finish {
742    Finish {
743        usage: crate::completion::Usage::from(&response.usage),
744        reason: response.stop_reason.as_deref().map(map_finish_reason),
745        message_id: response.message_id.clone(),
746        model: response.model.clone(),
747        ..Finish::default()
748    }
749}
750
751#[cfg(test)]
752mod tests;