Skip to main content

adk_anthropic/
accumulating_stream.rs

1//! Accumulates streaming events into a complete message while passing events through.
2
3use std::pin::Pin;
4
5use futures::Stream;
6use serde_json::Value;
7
8use crate::{
9    CacheControlEphemeral, Citation, ContentBlock, ContentBlockDelta, Error, Message,
10    MessageStreamEvent, ServerToolUseBlock, StopReason, TextBlock, TextCitation, ThinkingBlock,
11    ToolUseBlock,
12};
13
14/// A stream wrapper that accumulates `MessageStreamEvent`s into a complete `Message`.
15///
16/// This allows streaming tokens to the user while simultaneously building the final message
17/// without buffering. When the stream is fully drained, the accumulated message is sent via
18/// the oneshot channel returned by `new()`.
19///
20/// A mid-stream SSE `error` event (such as `overloaded_error`) is yielded as an `Err` item
21/// mapped through `From<ApiError> for Error`, and the same error is sent through the
22/// channel instead of the partial message.
23pub struct AccumulatingStream {
24    inner: Pin<Box<dyn Stream<Item = Result<MessageStreamEvent, Error>> + Send>>,
25    message_tx: Option<tokio::sync::oneshot::Sender<Result<Message, Error>>>,
26    message: Option<Message>,
27    content_blocks: Vec<ContentBlockBuilder>,
28}
29
30impl AccumulatingStream {
31    /// Wraps a `MessageStreamEvent` stream to accumulate events into a `Message`.
32    ///
33    /// Returns the stream and a receiver that will contain the accumulated `Message` once the
34    /// stream is fully drained.
35    pub fn new<S>(stream: S) -> (Self, tokio::sync::oneshot::Receiver<Result<Message, Error>>)
36    where
37        S: Stream<Item = Result<MessageStreamEvent, Error>> + Send + 'static,
38    {
39        Self::new_with_message(stream, None)
40    }
41
42    /// Wraps a `MessageStreamEvent` stream and seeds accumulation with a fallback message.
43    pub fn new_with_message<S>(
44        stream: S,
45        message: impl Into<Option<Message>>,
46    ) -> (Self, tokio::sync::oneshot::Receiver<Result<Message, Error>>)
47    where
48        S: Stream<Item = Result<MessageStreamEvent, Error>> + Send + 'static,
49    {
50        let (tx, rx) = tokio::sync::oneshot::channel();
51        let this = Self {
52            inner: Box::pin(stream),
53            message_tx: Some(tx),
54            message: message.into(),
55            content_blocks: Vec::new(),
56        };
57        (this, rx)
58    }
59
60    fn accumulate_event(&mut self, event: &MessageStreamEvent) {
61        match event {
62            MessageStreamEvent::MessageStart(start) => {
63                self.message = Some(start.message.clone());
64            }
65            MessageStreamEvent::ContentBlockStart(start) => {
66                let idx = start.index;
67                while self.content_blocks.len() <= idx {
68                    self.content_blocks.push(ContentBlockBuilder::Empty);
69                }
70                self.content_blocks[idx] =
71                    ContentBlockBuilder::from_content_block(start.content_block.clone());
72            }
73            MessageStreamEvent::ContentBlockDelta(delta_event) => {
74                let idx = delta_event.index;
75                if idx < self.content_blocks.len() {
76                    self.content_blocks[idx].apply_delta(delta_event.delta.clone());
77                }
78            }
79            MessageStreamEvent::ContentBlockStop(_) => {}
80            MessageStreamEvent::MessageDelta(delta_event) => {
81                if let Some(ref mut msg) = self.message {
82                    if delta_event.delta.stop_reason.is_some() {
83                        msg.stop_reason = delta_event.delta.stop_reason;
84                    }
85                    if delta_event.delta.stop_sequence.is_some() {
86                        msg.stop_sequence = delta_event.delta.stop_sequence.clone();
87                    }
88                    if let Some(input_tokens) = delta_event.usage.input_tokens {
89                        msg.usage.input_tokens = input_tokens;
90                    }
91                    msg.usage.output_tokens = delta_event.usage.output_tokens;
92                    if let Some(cache) = delta_event.usage.cache_creation_input_tokens {
93                        msg.usage.cache_creation_input_tokens = Some(cache);
94                    }
95                    if let Some(cache_read) = delta_event.usage.cache_read_input_tokens {
96                        msg.usage.cache_read_input_tokens = Some(cache_read);
97                    }
98                    if let Some(server_tool) = delta_event.usage.server_tool_use {
99                        msg.usage.server_tool_use = Some(server_tool);
100                    }
101                }
102            }
103            MessageStreamEvent::MessageStop(_) => {}
104            MessageStreamEvent::Ping => {}
105            // New event types that don't affect message accumulation
106            MessageStreamEvent::ToolInputStart { .. } => {}
107            MessageStreamEvent::ToolInputDelta { .. } => {}
108            MessageStreamEvent::CompactionEvent(_) => {}
109            // Handled in `poll_next`, which turns it into an `Err` item.
110            MessageStreamEvent::StreamError { .. } => {}
111        }
112    }
113
114    fn finalize(&mut self) -> Result<Message, Error> {
115        let mut msg = self
116            .message
117            .take()
118            .ok_or_else(|| Error::streaming("stream ended without a message start event", None))?;
119        let mut blocks = Vec::new();
120        for builder in std::mem::take(&mut self.content_blocks) {
121            if let Some(block) = builder.build(msg.stop_reason)? {
122                blocks.push(block);
123            }
124        }
125        msg.content = blocks;
126        Ok(msg)
127    }
128
129    /// Finalizes the currently accumulated message without draining the stream.
130    pub fn finalize_partial(&mut self) -> Result<Message, Error> {
131        self.message_tx.take();
132        self.finalize()
133    }
134}
135
136impl Stream for AccumulatingStream {
137    type Item = Result<MessageStreamEvent, Error>;
138
139    fn poll_next(
140        mut self: Pin<&mut Self>,
141        cx: &mut std::task::Context<'_>,
142    ) -> std::task::Poll<Option<Self::Item>> {
143        match self.inner.as_mut().poll_next(cx) {
144            // A mid-stream `error` event (for example `overloaded_error`) aborts the
145            // message, so the accumulated partial message must not be reported as success.
146            std::task::Poll::Ready(Some(Ok(MessageStreamEvent::StreamError { error }))) => {
147                let error = Error::from(error);
148                if let Some(tx) = self.message_tx.take() {
149                    let _ = tx.send(Err(error.clone()));
150                }
151                std::task::Poll::Ready(Some(Err(error)))
152            }
153            std::task::Poll::Ready(Some(Ok(event))) => {
154                self.accumulate_event(&event);
155                std::task::Poll::Ready(Some(Ok(event)))
156            }
157            std::task::Poll::Ready(Some(Err(e))) => std::task::Poll::Ready(Some(Err(e))),
158            std::task::Poll::Ready(None) => {
159                if let Some(tx) = self.message_tx.take() {
160                    let _ = tx.send(self.finalize());
161                }
162                std::task::Poll::Ready(None)
163            }
164            std::task::Poll::Pending => std::task::Poll::Pending,
165        }
166    }
167}
168
169enum ContentBlockBuilder {
170    Empty,
171    Text {
172        text: String,
173        citations: Option<Vec<TextCitation>>,
174        cache_control: Option<CacheControlEphemeral>,
175    },
176    ToolUse {
177        id: String,
178        name: String,
179        input: ToolInput,
180        cache_control: Option<CacheControlEphemeral>,
181    },
182    ServerToolUse {
183        id: String,
184        name: String,
185        input: ToolInput,
186        cache_control: Option<CacheControlEphemeral>,
187    },
188    Thinking {
189        thinking: String,
190        signature: String,
191    },
192    Complete(ContentBlock),
193}
194
195/// Input of a client or server tool block: the value from `content_block_start`,
196/// replaced by the concatenated `input_json_delta` fragments once any arrive.
197struct ToolInput {
198    initial: Value,
199    json: String,
200    saw_delta: bool,
201}
202
203impl ToolInput {
204    fn new(initial: Value) -> Self {
205        Self { initial, json: String::new(), saw_delta: false }
206    }
207
208    /// Returns `None` when `max_tokens` cut the streamed JSON off, so the
209    /// incomplete block is dropped rather than replayed.
210    fn resolve(self, stop_reason: Option<StopReason>) -> Option<Value> {
211        if !self.saw_delta {
212            return Some(self.initial);
213        }
214        if self.json.trim().is_empty() {
215            return Some(Value::Object(serde_json::Map::new()));
216        }
217        match serde_json::from_str::<Value>(&self.json) {
218            Ok(value) => Some(value),
219            Err(_) if stop_reason == Some(StopReason::MaxTokens) => None,
220            Err(_) => Some(Value::String(self.json)),
221        }
222    }
223}
224
225impl ContentBlockBuilder {
226    fn from_content_block(block: ContentBlock) -> Self {
227        match block {
228            ContentBlock::Text(text_block) => ContentBlockBuilder::Text {
229                text: text_block.text,
230                citations: text_block.citations,
231                cache_control: text_block.cache_control,
232            },
233            ContentBlock::ToolUse(tool_use) => ContentBlockBuilder::ToolUse {
234                id: tool_use.id,
235                name: tool_use.name,
236                input: ToolInput::new(tool_use.input),
237                cache_control: tool_use.cache_control,
238            },
239            ContentBlock::ServerToolUse(server_tool_use) => ContentBlockBuilder::ServerToolUse {
240                id: server_tool_use.id,
241                name: server_tool_use.name,
242                input: ToolInput::new(server_tool_use.input),
243                cache_control: server_tool_use.cache_control,
244            },
245            ContentBlock::Thinking(thinking) => ContentBlockBuilder::Thinking {
246                thinking: thinking.thinking,
247                signature: thinking.signature,
248            },
249            other => ContentBlockBuilder::Complete(other),
250        }
251    }
252
253    fn apply_delta(&mut self, delta: ContentBlockDelta) {
254        match (self, delta) {
255            (ContentBlockBuilder::Text { text, .. }, ContentBlockDelta::TextDelta(text_delta)) => {
256                text.push_str(&text_delta.text);
257            }
258            (
259                ContentBlockBuilder::Text { citations, .. },
260                ContentBlockDelta::CitationsDelta(citations_delta),
261            ) => {
262                let citation = match citations_delta.citation {
263                    Citation::CharLocation(loc) => TextCitation::CharLocation(loc),
264                    Citation::PageLocation(loc) => TextCitation::PageLocation(loc),
265                    Citation::ContentBlockLocation(loc) => TextCitation::ContentBlockLocation(loc),
266                    Citation::WebSearchResultLocation(loc) => {
267                        TextCitation::WebSearchResultLocation(loc)
268                    }
269                };
270                citations.get_or_insert_with(Vec::new).push(citation);
271            }
272            (
273                ContentBlockBuilder::ToolUse { input, .. }
274                | ContentBlockBuilder::ServerToolUse { input, .. },
275                ContentBlockDelta::InputJsonDelta(json_delta),
276            ) => {
277                input.saw_delta = true;
278                input.json.push_str(&json_delta.partial_json);
279            }
280            (
281                ContentBlockBuilder::Thinking { thinking, .. },
282                ContentBlockDelta::ThinkingDelta(thinking_delta),
283            ) => {
284                thinking.push_str(&thinking_delta.thinking);
285            }
286            (
287                ContentBlockBuilder::Thinking { signature, .. },
288                ContentBlockDelta::SignatureDelta(sig_delta),
289            ) => {
290                signature.push_str(&sig_delta.signature);
291            }
292            _ => {}
293        }
294    }
295
296    fn build(self, stop_reason: Option<StopReason>) -> Result<Option<ContentBlock>, Error> {
297        match self {
298            ContentBlockBuilder::Empty => Ok(None),
299            ContentBlockBuilder::Text { text, citations, cache_control } => {
300                Ok(Some(ContentBlock::Text(TextBlock { text, citations, cache_control })))
301            }
302            ContentBlockBuilder::ToolUse { id, name, input, cache_control } => {
303                Ok(input.resolve(stop_reason).map(|input| {
304                    ContentBlock::ToolUse(ToolUseBlock { id, name, input, cache_control })
305                }))
306            }
307            ContentBlockBuilder::ServerToolUse { id, name, input, cache_control } => {
308                Ok(input.resolve(stop_reason).map(|input| {
309                    ContentBlock::ServerToolUse(ServerToolUseBlock {
310                        id,
311                        name,
312                        input,
313                        cache_control,
314                    })
315                }))
316            }
317            ContentBlockBuilder::Thinking { thinking, signature } => {
318                Ok(Some(ContentBlock::Thinking(ThinkingBlock { thinking, signature })))
319            }
320            ContentBlockBuilder::Complete(block) => Ok(Some(block)),
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::{
329        ContentBlockDeltaEvent, ContentBlockStartEvent, ContentBlockStopEvent, InputJsonDelta,
330        KnownModel, MessageDelta, MessageDeltaEvent, MessageDeltaUsage, MessageStartEvent, Model,
331        TextDelta, Usage,
332    };
333    use futures::stream;
334
335    #[tokio::test]
336    async fn server_inputs_preserve_initial_values_and_indexed_deltas() {
337        use futures::StreamExt;
338        use serde_json::json;
339        for fragments in [false, true] {
340            let mut values = vec![
341                json!({"type":"message_start","message":{"id":"fixture","type":"message",
342                    "role":"assistant","model":"fixture","content":[],"stop_reason":null,
343                    "stop_sequence":null,"usage":{"input_tokens":8,"output_tokens":0}}}),
344                json!({"type":"content_block_start","index":0,"content_block":{
345                    "type":"server_tool_use","id":"search","name":"web_search",
346                    "input":if fragments { json!({}) } else { json!({"query":"source 🙂"}) }}}),
347                json!({"type":"content_block_start","index":1,"content_block":{
348                    "type":"tool_use","id":"read","name":"read_file","input":{}}}),
349            ];
350            if fragments {
351                values.push(json!({"type":"content_block_delta","index":0,
352                    "delta":{"type":"input_json_delta","partial_json":"{\"query\":\"source "}}));
353            }
354            values.push(json!({"type":"content_block_delta","index":1,
355                "delta":{"type":"input_json_delta","partial_json":"{\"path\":\"file.txt\"}"}}));
356            if fragments {
357                values.push(json!({"type":"content_block_delta","index":0,
358                    "delta":{"type":"input_json_delta","partial_json":"🙂\"}"}}));
359            }
360            values.push(json!({"type":"message_delta","delta":{"stop_reason":"tool_use"},
361                "usage":{"output_tokens":4}}));
362            values.push(json!({"type":"message_stop"}));
363            let events: Vec<_> = values
364                .into_iter()
365                .map(|value| Ok(serde_json::from_value(value).unwrap()))
366                .collect();
367            let (mut stream, result) = AccumulatingStream::new(stream::iter(events));
368            while let Some(event) = stream.next().await {
369                event.unwrap();
370            }
371            let message = result.await.unwrap().unwrap();
372            assert!(matches!(&message.content[0], ContentBlock::ServerToolUse(tool)
373                if tool.id == "search" && tool.input == json!({"query":"source 🙂"})));
374            assert!(matches!(&message.content[1], ContentBlock::ToolUse(tool)
375                if tool.id == "read" && tool.input == json!({"path":"file.txt"})));
376        }
377    }
378
379    /// Verifies that cache tokens from message_start are preserved through streaming.
380    #[tokio::test]
381    async fn cache_tokens_from_message_start_preserved() {
382        // Build a message_start event with cache tokens in the usage
383        let usage_with_cache = Usage::new(100, 0)
384            .with_cache_creation_input_tokens(50)
385            .with_cache_read_input_tokens(25);
386
387        let start_message = Message::new(
388            "msg_test".to_string(),
389            Vec::new(),
390            Model::Known(KnownModel::ClaudeSonnet46),
391            usage_with_cache,
392        );
393        let start_event = MessageStreamEvent::MessageStart(MessageStartEvent::new(start_message));
394
395        // Build content_block_start for text
396        let text_block = ContentBlock::Text(TextBlock::new(String::new()));
397        let content_start =
398            MessageStreamEvent::ContentBlockStart(ContentBlockStartEvent::new(text_block, 0));
399
400        // Build content_block_delta with text
401        let text_delta = TextDelta::new("Hello".to_string());
402        let content_delta = MessageStreamEvent::ContentBlockDelta(
403            crate::ContentBlockDeltaEvent::new(ContentBlockDelta::TextDelta(text_delta), 0),
404        );
405
406        // Build message_delta with final output tokens (no cache tokens - they were in start)
407        let delta_usage = MessageDeltaUsage::new(10);
408        let message_delta = MessageDelta::new().with_stop_reason(StopReason::EndTurn);
409        let delta_event =
410            MessageStreamEvent::MessageDelta(MessageDeltaEvent::new(message_delta, delta_usage));
411
412        // Create the stream
413        let events = vec![Ok(start_event), Ok(content_start), Ok(content_delta), Ok(delta_event)];
414        let event_stream = stream::iter(events);
415
416        let (mut acc_stream, rx) = AccumulatingStream::new(event_stream);
417
418        // Drain the stream
419        use futures::StreamExt;
420        while acc_stream.next().await.is_some() {}
421
422        // Get the accumulated message
423        let message = rx.await.expect("channel closed").expect("accumulation failed");
424
425        // Verify cache tokens were preserved from message_start
426        assert_eq!(
427            message.usage.cache_creation_input_tokens,
428            Some(50),
429            "cache_creation_input_tokens should be preserved from message_start"
430        );
431        assert_eq!(
432            message.usage.cache_read_input_tokens,
433            Some(25),
434            "cache_read_input_tokens should be preserved from message_start"
435        );
436        assert_eq!(message.usage.output_tokens, 10, "output_tokens should be from message_delta");
437    }
438
439    /// Verifies that tool use with empty input JSON becomes an empty object, not null.
440    #[tokio::test]
441    async fn empty_tool_input_becomes_empty_object() {
442        let usage = Usage::new(100, 0);
443        let start_message = Message::new(
444            "msg_test".to_string(),
445            Vec::new(),
446            Model::Known(KnownModel::ClaudeSonnet46),
447            usage,
448        );
449        let start_event = MessageStreamEvent::MessageStart(MessageStartEvent::new(start_message));
450
451        // Build content_block_start for tool_use with initial empty input
452        let tool_use_block =
453            ContentBlock::ToolUse(ToolUseBlock::new("tool_123", "get_document", Value::Null));
454        let content_start =
455            MessageStreamEvent::ContentBlockStart(ContentBlockStartEvent::new(tool_use_block, 0));
456
457        // Build content_block_delta with empty JSON (simulating no input parameters)
458        let json_delta = InputJsonDelta::new(String::new());
459        let content_delta = MessageStreamEvent::ContentBlockDelta(ContentBlockDeltaEvent::new(
460            ContentBlockDelta::InputJsonDelta(json_delta),
461            0,
462        ));
463
464        // Build content_block_stop
465        let content_stop = MessageStreamEvent::ContentBlockStop(ContentBlockStopEvent::new(0));
466
467        // Build message_delta
468        let delta_usage = MessageDeltaUsage::new(10);
469        let message_delta = MessageDelta::new().with_stop_reason(StopReason::ToolUse);
470        let delta_event =
471            MessageStreamEvent::MessageDelta(MessageDeltaEvent::new(message_delta, delta_usage));
472
473        let events = vec![
474            Ok(start_event),
475            Ok(content_start),
476            Ok(content_delta),
477            Ok(content_stop),
478            Ok(delta_event),
479        ];
480        let event_stream = stream::iter(events);
481
482        let (mut acc_stream, rx) = AccumulatingStream::new(event_stream);
483
484        use futures::StreamExt;
485        while acc_stream.next().await.is_some() {}
486
487        let message = rx.await.expect("channel closed").expect("accumulation failed");
488
489        assert_eq!(message.content.len(), 1, "Should have one content block");
490        let tool_use = message.content[0].as_tool_use().expect("Expected ToolUseBlock");
491
492        // Empty input should be an empty object, not null
493        assert!(
494            tool_use.input.is_object(),
495            "Empty tool input should be an object, not null. Got: {:?}",
496            tool_use.input
497        );
498        assert!(
499            tool_use.input.as_object().expect("input should be object").is_empty(),
500            "Empty tool input should be an empty object"
501        );
502    }
503
504    /// Verifies that tool use with no delta events uses initial input_value.
505    #[tokio::test]
506    async fn tool_input_without_delta_uses_initial_value() {
507        let usage = Usage::new(100, 0);
508        let start_message = Message::new(
509            "msg_test".to_string(),
510            Vec::new(),
511            Model::Known(KnownModel::ClaudeSonnet46),
512            usage,
513        );
514        let start_event = MessageStreamEvent::MessageStart(MessageStartEvent::new(start_message));
515
516        // Build content_block_start for tool_use with an actual input value
517        let input = serde_json::json!({"key": "value"});
518        let tool_use_block =
519            ContentBlock::ToolUse(ToolUseBlock::new("tool_123", "get_document", input.clone()));
520        let content_start =
521            MessageStreamEvent::ContentBlockStart(ContentBlockStartEvent::new(tool_use_block, 0));
522
523        // No delta events - the input should come from the initial value
524
525        let content_stop = MessageStreamEvent::ContentBlockStop(ContentBlockStopEvent::new(0));
526
527        let delta_usage = MessageDeltaUsage::new(10);
528        let message_delta = MessageDelta::new().with_stop_reason(StopReason::ToolUse);
529        let delta_event =
530            MessageStreamEvent::MessageDelta(MessageDeltaEvent::new(message_delta, delta_usage));
531
532        let events = vec![Ok(start_event), Ok(content_start), Ok(content_stop), Ok(delta_event)];
533        let event_stream = stream::iter(events);
534
535        let (mut acc_stream, rx) = AccumulatingStream::new(event_stream);
536
537        use futures::StreamExt;
538        while acc_stream.next().await.is_some() {}
539
540        let message = rx.await.expect("channel closed").expect("accumulation failed");
541
542        let tool_use = message.content[0].as_tool_use().expect("Expected ToolUseBlock");
543
544        assert_eq!(tool_use.input, input, "Tool input should match initial value");
545    }
546
547    fn start_event() -> MessageStreamEvent {
548        MessageStreamEvent::MessageStart(MessageStartEvent::new(Message::new(
549            "msg_test".to_string(),
550            Vec::new(),
551            Model::Known(KnownModel::ClaudeSonnet46),
552            Usage::new(100, 0),
553        )))
554    }
555
556    fn input_json_delta(index: usize, partial_json: &str) -> MessageStreamEvent {
557        MessageStreamEvent::ContentBlockDelta(ContentBlockDeltaEvent::new(
558            ContentBlockDelta::InputJsonDelta(InputJsonDelta::new(partial_json.to_string())),
559            index,
560        ))
561    }
562
563    /// Server tool input arrives through `input_json_delta` exactly like client tool input.
564    #[tokio::test]
565    async fn server_tool_use_applies_input_json_deltas() {
566        use futures::StreamExt;
567
568        let server_tool_use = ContentBlock::ServerToolUse(ServerToolUseBlock::new(
569            "srvtoolu_1",
570            serde_json::json!({}),
571        ));
572        let events = vec![
573            Ok(start_event()),
574            Ok(MessageStreamEvent::ContentBlockStart(ContentBlockStartEvent::new(
575                server_tool_use,
576                0,
577            ))),
578            Ok(input_json_delta(0, r#"{"query": "rust "#)),
579            Ok(input_json_delta(0, r#"async"}"#)),
580            Ok(MessageStreamEvent::ContentBlockStop(ContentBlockStopEvent::new(0))),
581            Ok(MessageStreamEvent::MessageDelta(MessageDeltaEvent::new(
582                MessageDelta::new().with_stop_reason(StopReason::EndTurn),
583                MessageDeltaUsage::new(10),
584            ))),
585        ];
586
587        let (mut acc_stream, rx) = AccumulatingStream::new(stream::iter(events));
588        while acc_stream.next().await.is_some() {}
589        let message = rx.await.expect("channel closed").expect("accumulation failed");
590
591        assert_eq!(
592            message.content,
593            vec![ContentBlock::ServerToolUse(ServerToolUseBlock::new(
594                "srvtoolu_1",
595                serde_json::json!({"query": "rust async"}),
596            ))]
597        );
598    }
599
600    /// A mid-stream `error` event fails the stream and the accumulated message.
601    #[tokio::test]
602    async fn stream_error_event_is_returned_as_error_not_partial_message() {
603        use futures::StreamExt;
604
605        let events = vec![
606            Ok(start_event()),
607            Ok(MessageStreamEvent::ContentBlockStart(ContentBlockStartEvent::new(
608                ContentBlock::Text(TextBlock::new(String::new())),
609                0,
610            ))),
611            Ok(MessageStreamEvent::ContentBlockDelta(ContentBlockDeltaEvent::new(
612                ContentBlockDelta::TextDelta(TextDelta::new("partial".to_string())),
613                0,
614            ))),
615            Ok(MessageStreamEvent::StreamError {
616                error: crate::ApiError {
617                    error_type: "overloaded_error".to_string(),
618                    message: "Overloaded".to_string(),
619                },
620            }),
621        ];
622
623        let (acc_stream, rx) = AccumulatingStream::new(stream::iter(events));
624        let items: Vec<_> = acc_stream.collect().await;
625
626        let stream_error = items.last().expect("stream yields items").as_ref().unwrap_err();
627        assert!(
628            matches!(stream_error, Error::ServiceUnavailable { message, retry_after: None } if message == "Overloaded"),
629            "unexpected stream error: {stream_error:?}"
630        );
631        assert!(stream_error.is_retryable());
632
633        let message_error = rx.await.expect("channel closed").unwrap_err();
634        assert!(
635            matches!(message_error, Error::ServiceUnavailable { ref message, .. } if message == "Overloaded"),
636            "unexpected message error: {message_error:?}"
637        );
638    }
639}