1use 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
14pub 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 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 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 MessageStreamEvent::ToolInputStart { .. } => {}
107 MessageStreamEvent::ToolInputDelta { .. } => {}
108 MessageStreamEvent::CompactionEvent(_) => {}
109 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 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 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
195struct 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 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 #[tokio::test]
381 async fn cache_tokens_from_message_start_preserved() {
382 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 let text_block = ContentBlock::Text(TextBlock::new(String::new()));
397 let content_start =
398 MessageStreamEvent::ContentBlockStart(ContentBlockStartEvent::new(text_block, 0));
399
400 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 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 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 use futures::StreamExt;
420 while acc_stream.next().await.is_some() {}
421
422 let message = rx.await.expect("channel closed").expect("accumulation failed");
424
425 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 #[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 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 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 let content_stop = MessageStreamEvent::ContentBlockStop(ContentBlockStopEvent::new(0));
466
467 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 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 #[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 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 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 #[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 #[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}