crimson_crab/streaming.rs
1//! Server-sent-events (SSE) streaming for `POST /v1/messages` with
2//! `"stream": true`.
3//!
4//! [`MessageStream`] wraps the raw byte stream returned by the API, decodes the
5//! SSE records with a hand-rolled parser (no external eventsource dependency),
6//! and yields typed [`StreamEvent`]s. As events flow past it, the stream also
7//! *accumulates* them into a [`Message`] identical in shape to the one the
8//! non-streaming endpoint would have returned — retrievable with
9//! [`MessageStream::final_message`] while draining, or in one shot with
10//! [`MessageStream::collect_final`].
11//!
12//! Forward compatibility is preserved throughout: unknown event types and
13//! unknown delta types deserialize into `Unknown` catch-alls (retaining the raw
14//! JSON) rather than erroring, and an in-stream `error` event is surfaced to the
15//! caller as an `Err` item that terminates the stream.
16//!
17//! # Examples
18//!
19//! ```no_run
20//! use crimson_crab::api::MessagesRequest;
21//! use crimson_crab::streaming::{ContentDelta, StreamEvent};
22//! use crimson_crab::types::MessageParam;
23//! use futures_util::StreamExt;
24//!
25//! # async fn demo(client: &crimson_crab::Client) -> crimson_crab::Result<()> {
26//! let request = MessagesRequest::builder()
27//! .model("claude-opus-4-8")
28//! .max_tokens(1024)
29//! .messages(vec![MessageParam::user("Hello")])
30//! .build()?;
31//!
32//! let mut stream = client.messages().stream(&request).await?;
33//! while let Some(event) = stream.next().await {
34//! if let StreamEvent::ContentBlockDelta {
35//! delta: ContentDelta::TextDelta { text },
36//! ..
37//! } = event?
38//! {
39//! print!("{text}");
40//! }
41//! }
42//! # Ok(())
43//! # }
44//! ```
45
46use std::collections::HashMap;
47use std::pin::Pin;
48use std::task::{Context, Poll};
49
50use bytes::Bytes;
51use futures_core::Stream;
52use futures_util::StreamExt;
53use serde::de::{self, Deserializer};
54use serde::ser::SerializeMap;
55use serde::{Deserialize, Serialize, Serializer};
56
57use crate::error::{self, Error, Result};
58use crate::types::{ContentBlock, Message, StopDetails, StopReason, Usage};
59
60// ---------------------------------------------------------------------------
61// Stream event types.
62// ---------------------------------------------------------------------------
63
64/// A single decoded SSE event from a streaming `POST /v1/messages` response.
65///
66/// The variants mirror the Anthropic stream event sequence
67/// (`message_start` → `content_block_start`/`content_block_delta`/
68/// `content_block_stop` (repeated) → `message_delta` → `message_stop`), plus
69/// `ping` heartbeats and an in-stream `error`. Any event type the SDK does not
70/// model deserializes to [`StreamEvent::Unknown`], preserving the raw JSON.
71///
72/// [`MessageStream`] intercepts [`StreamEvent::Error`] and surfaces it as an
73/// `Err` item instead of yielding it, so consumers of the stream never observe
74/// this variant directly; it exists so the event model is complete and
75/// round-trips.
76///
77/// # Examples
78///
79/// ```
80/// use crimson_crab::streaming::StreamEvent;
81///
82/// let event: StreamEvent = serde_json::from_str(
83/// r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}"#,
84/// )
85/// .unwrap();
86/// assert!(matches!(event, StreamEvent::ContentBlockDelta { index: 0, .. }));
87///
88/// // An unknown event type is preserved rather than rejected.
89/// let novel: StreamEvent = serde_json::from_str(r#"{"type":"brand_new","x":1}"#).unwrap();
90/// assert!(matches!(novel, StreamEvent::Unknown(_)));
91/// ```
92#[derive(Clone, Debug, PartialEq)]
93#[non_exhaustive]
94pub enum StreamEvent {
95 /// The opening event; carries the base [`Message`] (empty `content`).
96 MessageStart {
97 /// The base message that subsequent events are accumulated into.
98 message: Message,
99 },
100 /// A content block begins at `index`.
101 ContentBlockStart {
102 /// The position of the block within the message `content` array.
103 index: usize,
104 /// The initial (usually empty) block.
105 content_block: ContentBlock,
106 },
107 /// An incremental update to the content block at `index`.
108 ContentBlockDelta {
109 /// The position of the block being updated.
110 index: usize,
111 /// The delta to apply.
112 delta: ContentDelta,
113 },
114 /// The content block at `index` is complete.
115 ContentBlockStop {
116 /// The position of the block that finished.
117 index: usize,
118 },
119 /// Top-level message updates (final `stop_reason`, cumulative `usage`).
120 MessageDelta {
121 /// The stop metadata for the message.
122 delta: MessageDeltaBody,
123 /// Cumulative usage totals, if present.
124 usage: Option<UsageDelta>,
125 },
126 /// The final event; the message is complete.
127 MessageStop,
128 /// A keep-alive heartbeat with no payload.
129 Ping,
130 /// An in-stream error; terminates the message.
131 Error {
132 /// The error body reported by the server.
133 error: StreamErrorBody,
134 },
135 /// Forward-compatible catch-all preserving the raw JSON of an unknown event.
136 Unknown(serde_json::Value),
137}
138
139/// An incremental update to a content block, carried by
140/// [`StreamEvent::ContentBlockDelta`].
141///
142/// # Examples
143///
144/// ```
145/// use crimson_crab::streaming::ContentDelta;
146///
147/// let delta: ContentDelta =
148/// serde_json::from_str(r#"{"type":"text_delta","text":"Hello"}"#).unwrap();
149/// assert_eq!(delta, ContentDelta::TextDelta { text: "Hello".to_string() });
150///
151/// let unknown: ContentDelta =
152/// serde_json::from_str(r#"{"type":"future_delta","x":1}"#).unwrap();
153/// assert!(matches!(unknown, ContentDelta::Unknown(_)));
154/// ```
155#[derive(Clone, Debug, PartialEq)]
156#[non_exhaustive]
157pub enum ContentDelta {
158 /// Text to append to a `text` block.
159 TextDelta {
160 /// The text fragment.
161 text: String,
162 },
163 /// A fragment of a `tool_use` block's JSON `input`; concatenate all
164 /// fragments and parse the result (an empty string means `{}`).
165 InputJsonDelta {
166 /// The partial JSON fragment.
167 partial_json: String,
168 },
169 /// Reasoning text to append to a `thinking` block.
170 ThinkingDelta {
171 /// The thinking fragment.
172 thinking: String,
173 },
174 /// The signature for a `thinking` block (arrives before its stop event).
175 SignatureDelta {
176 /// The opaque signature fragment.
177 signature: String,
178 },
179 /// A citation to append to a `text` block.
180 CitationsDelta {
181 /// The raw citation object.
182 citation: serde_json::Value,
183 },
184 /// Forward-compatible catch-all preserving the raw JSON of an unknown delta.
185 Unknown(serde_json::Value),
186}
187
188/// The `delta` object carried by a [`StreamEvent::MessageDelta`] event.
189///
190/// It conveys the final `stop_reason`/`stop_sequence` (and, on a refusal, the
191/// `stop_details`) once the model has finished; these are merged into the
192/// accumulated [`Message`].
193///
194/// # Examples
195///
196/// ```
197/// use crimson_crab::streaming::MessageDeltaBody;
198/// use crimson_crab::types::StopReason;
199///
200/// let body: MessageDeltaBody = serde_json::from_value(serde_json::json!({
201/// "stop_reason": "end_turn",
202/// "stop_sequence": null
203/// }))
204/// .unwrap();
205/// assert_eq!(body.stop_reason, Some(StopReason::EndTurn));
206/// ```
207#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
208pub struct MessageDeltaBody {
209 /// Why generation stopped, if known at this point.
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub stop_reason: Option<StopReason>,
212 /// The matched stop sequence, if any.
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub stop_sequence: Option<String>,
215 /// Refusal detail, present only on a refusal stop.
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub stop_details: Option<StopDetails>,
218 /// Forward-compatible catch-all for any other fields.
219 #[serde(flatten)]
220 pub extra: serde_json::Map<String, serde_json::Value>,
221}
222
223/// The `usage` object carried by a [`StreamEvent::MessageDelta`] event.
224///
225/// Each present field is a **cumulative total** that *replaces* (not adds to)
226/// the corresponding field on the accumulated message's [`Usage`].
227///
228/// # Examples
229///
230/// ```
231/// use crimson_crab::streaming::UsageDelta;
232///
233/// let usage: UsageDelta =
234/// serde_json::from_value(serde_json::json!({"output_tokens": 12})).unwrap();
235/// assert_eq!(usage.output_tokens, Some(12));
236/// ```
237#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
238pub struct UsageDelta {
239 /// Cumulative input tokens, if updated.
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub input_tokens: Option<u64>,
242 /// Cumulative output tokens, if updated.
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub output_tokens: Option<u64>,
245 /// Cumulative cache-creation input tokens, if updated.
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub cache_creation_input_tokens: Option<u64>,
248 /// Cumulative cache-read input tokens, if updated.
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub cache_read_input_tokens: Option<u64>,
251 /// Forward-compatible catch-all for any other usage fields.
252 #[serde(flatten)]
253 pub extra: serde_json::Map<String, serde_json::Value>,
254}
255
256/// The `error` object carried by an in-stream [`StreamEvent::Error`] event.
257///
258/// # Examples
259///
260/// ```
261/// use crimson_crab::streaming::StreamErrorBody;
262///
263/// let body: StreamErrorBody = serde_json::from_value(serde_json::json!({
264/// "type": "overloaded_error",
265/// "message": "Overloaded"
266/// }))
267/// .unwrap();
268/// assert_eq!(body.error_type, "overloaded_error");
269/// ```
270#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
271pub struct StreamErrorBody {
272 /// The machine-readable error type, e.g. `"overloaded_error"`.
273 #[serde(rename = "type")]
274 pub error_type: String,
275 /// A human-readable description of the error.
276 pub message: String,
277 /// Forward-compatible catch-all for any other fields.
278 #[serde(flatten)]
279 pub extra: serde_json::Map<String, serde_json::Value>,
280}
281
282// ---------------------------------------------------------------------------
283// Serde for the two hand-written enums.
284// ---------------------------------------------------------------------------
285
286impl Serialize for ContentDelta {
287 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
288 where
289 S: Serializer,
290 {
291 match self {
292 ContentDelta::TextDelta { text } => {
293 let mut map = serializer.serialize_map(Some(2))?;
294 map.serialize_entry("type", "text_delta")?;
295 map.serialize_entry("text", text)?;
296 map.end()
297 }
298 ContentDelta::InputJsonDelta { partial_json } => {
299 let mut map = serializer.serialize_map(Some(2))?;
300 map.serialize_entry("type", "input_json_delta")?;
301 map.serialize_entry("partial_json", partial_json)?;
302 map.end()
303 }
304 ContentDelta::ThinkingDelta { thinking } => {
305 let mut map = serializer.serialize_map(Some(2))?;
306 map.serialize_entry("type", "thinking_delta")?;
307 map.serialize_entry("thinking", thinking)?;
308 map.end()
309 }
310 ContentDelta::SignatureDelta { signature } => {
311 let mut map = serializer.serialize_map(Some(2))?;
312 map.serialize_entry("type", "signature_delta")?;
313 map.serialize_entry("signature", signature)?;
314 map.end()
315 }
316 ContentDelta::CitationsDelta { citation } => {
317 let mut map = serializer.serialize_map(Some(2))?;
318 map.serialize_entry("type", "citations_delta")?;
319 map.serialize_entry("citation", citation)?;
320 map.end()
321 }
322 ContentDelta::Unknown(value) => value.serialize(serializer),
323 }
324 }
325}
326
327impl<'de> Deserialize<'de> for ContentDelta {
328 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
329 where
330 D: Deserializer<'de>,
331 {
332 let value = serde_json::Value::deserialize(deserializer)?;
333 let tag = value
334 .get("type")
335 .and_then(|t| t.as_str())
336 .map(str::to_owned);
337 let delta = match tag.as_deref() {
338 Some("text_delta") => {
339 #[derive(Deserialize)]
340 struct R {
341 #[serde(default)]
342 text: String,
343 }
344 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
345 ContentDelta::TextDelta { text: r.text }
346 }
347 Some("input_json_delta") => {
348 #[derive(Deserialize)]
349 struct R {
350 #[serde(default)]
351 partial_json: String,
352 }
353 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
354 ContentDelta::InputJsonDelta {
355 partial_json: r.partial_json,
356 }
357 }
358 Some("thinking_delta") => {
359 #[derive(Deserialize)]
360 struct R {
361 #[serde(default)]
362 thinking: String,
363 }
364 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
365 ContentDelta::ThinkingDelta {
366 thinking: r.thinking,
367 }
368 }
369 Some("signature_delta") => {
370 #[derive(Deserialize)]
371 struct R {
372 #[serde(default)]
373 signature: String,
374 }
375 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
376 ContentDelta::SignatureDelta {
377 signature: r.signature,
378 }
379 }
380 Some("citations_delta") => {
381 #[derive(Deserialize)]
382 struct R {
383 #[serde(default)]
384 citation: serde_json::Value,
385 }
386 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
387 ContentDelta::CitationsDelta {
388 citation: r.citation,
389 }
390 }
391 _ => ContentDelta::Unknown(value),
392 };
393 Ok(delta)
394 }
395}
396
397impl Serialize for StreamEvent {
398 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
399 where
400 S: Serializer,
401 {
402 match self {
403 StreamEvent::MessageStart { message } => {
404 let mut map = serializer.serialize_map(Some(2))?;
405 map.serialize_entry("type", "message_start")?;
406 map.serialize_entry("message", message)?;
407 map.end()
408 }
409 StreamEvent::ContentBlockStart {
410 index,
411 content_block,
412 } => {
413 let mut map = serializer.serialize_map(Some(3))?;
414 map.serialize_entry("type", "content_block_start")?;
415 map.serialize_entry("index", index)?;
416 map.serialize_entry("content_block", content_block)?;
417 map.end()
418 }
419 StreamEvent::ContentBlockDelta { index, delta } => {
420 let mut map = serializer.serialize_map(Some(3))?;
421 map.serialize_entry("type", "content_block_delta")?;
422 map.serialize_entry("index", index)?;
423 map.serialize_entry("delta", delta)?;
424 map.end()
425 }
426 StreamEvent::ContentBlockStop { index } => {
427 let mut map = serializer.serialize_map(Some(2))?;
428 map.serialize_entry("type", "content_block_stop")?;
429 map.serialize_entry("index", index)?;
430 map.end()
431 }
432 StreamEvent::MessageDelta { delta, usage } => {
433 let len = if usage.is_some() { 3 } else { 2 };
434 let mut map = serializer.serialize_map(Some(len))?;
435 map.serialize_entry("type", "message_delta")?;
436 map.serialize_entry("delta", delta)?;
437 if let Some(usage) = usage {
438 map.serialize_entry("usage", usage)?;
439 }
440 map.end()
441 }
442 StreamEvent::MessageStop => {
443 let mut map = serializer.serialize_map(Some(1))?;
444 map.serialize_entry("type", "message_stop")?;
445 map.end()
446 }
447 StreamEvent::Ping => {
448 let mut map = serializer.serialize_map(Some(1))?;
449 map.serialize_entry("type", "ping")?;
450 map.end()
451 }
452 StreamEvent::Error { error } => {
453 let mut map = serializer.serialize_map(Some(2))?;
454 map.serialize_entry("type", "error")?;
455 map.serialize_entry("error", error)?;
456 map.end()
457 }
458 StreamEvent::Unknown(value) => value.serialize(serializer),
459 }
460 }
461}
462
463impl<'de> Deserialize<'de> for StreamEvent {
464 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
465 where
466 D: Deserializer<'de>,
467 {
468 let value = serde_json::Value::deserialize(deserializer)?;
469 let tag = value
470 .get("type")
471 .and_then(|t| t.as_str())
472 .map(str::to_owned);
473 let event = match tag.as_deref() {
474 Some("message_start") => {
475 #[derive(Deserialize)]
476 struct R {
477 message: Message,
478 }
479 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
480 StreamEvent::MessageStart { message: r.message }
481 }
482 Some("content_block_start") => {
483 #[derive(Deserialize)]
484 struct R {
485 index: usize,
486 content_block: ContentBlock,
487 }
488 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
489 StreamEvent::ContentBlockStart {
490 index: r.index,
491 content_block: r.content_block,
492 }
493 }
494 Some("content_block_delta") => {
495 #[derive(Deserialize)]
496 struct R {
497 index: usize,
498 delta: ContentDelta,
499 }
500 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
501 StreamEvent::ContentBlockDelta {
502 index: r.index,
503 delta: r.delta,
504 }
505 }
506 Some("content_block_stop") => {
507 #[derive(Deserialize)]
508 struct R {
509 index: usize,
510 }
511 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
512 StreamEvent::ContentBlockStop { index: r.index }
513 }
514 Some("message_delta") => {
515 #[derive(Deserialize)]
516 struct R {
517 delta: MessageDeltaBody,
518 #[serde(default)]
519 usage: Option<UsageDelta>,
520 }
521 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
522 StreamEvent::MessageDelta {
523 delta: r.delta,
524 usage: r.usage,
525 }
526 }
527 Some("message_stop") => StreamEvent::MessageStop,
528 Some("ping") => StreamEvent::Ping,
529 Some("error") => {
530 #[derive(Deserialize)]
531 struct R {
532 error: StreamErrorBody,
533 }
534 let r: R = serde_json::from_value(value).map_err(de::Error::custom)?;
535 StreamEvent::Error { error: r.error }
536 }
537 _ => StreamEvent::Unknown(value),
538 };
539 Ok(event)
540 }
541}
542
543// ---------------------------------------------------------------------------
544// SSE record decoder.
545// ---------------------------------------------------------------------------
546
547/// Splits an `SSE` line into its field name and value (`"data: x"` →
548/// `("data", "x")`), stripping a single optional space after the colon. A line
549/// with no colon is a field name with an empty value.
550fn parse_field(line: &str) -> (&str, &str) {
551 match line.find(':') {
552 Some(idx) => {
553 let field = &line[..idx];
554 let value = &line[idx + 1..];
555 let value = value.strip_prefix(' ').unwrap_or(value);
556 (field, value)
557 }
558 None => (line, ""),
559 }
560}
561
562/// An incremental SSE record decoder over an arbitrarily-chunked byte stream.
563///
564/// Bytes are buffered; complete newline-terminated lines are consumed one at a
565/// time (tolerating both `\n` and `\r\n`). `data:` lines accumulate (joined with
566/// `\n` for multi-line data), comment lines (`:` prefix) and other fields are
567/// ignored, and a blank line dispatches the accumulated event's `data` payload.
568#[derive(Default)]
569struct SseDecoder {
570 buffer: Vec<u8>,
571 data: String,
572 saw_data: bool,
573}
574
575impl SseDecoder {
576 fn push(&mut self, bytes: &[u8]) {
577 self.buffer.extend_from_slice(bytes);
578 }
579
580 /// Returns the next complete event's `data` payload, or `None` if more bytes
581 /// are required. Records without a `data` field are skipped.
582 fn next_event_data(&mut self) -> Option<String> {
583 loop {
584 let newline = self.buffer.iter().position(|&byte| byte == b'\n')?;
585 let line_bytes: Vec<u8> = self.buffer.drain(..=newline).collect();
586 let mut end = line_bytes.len() - 1; // exclude the trailing '\n'
587 if end > 0 && line_bytes[end - 1] == b'\r' {
588 end -= 1; // tolerate CRLF line endings
589 }
590 let line = String::from_utf8_lossy(&line_bytes[..end]);
591
592 if line.is_empty() {
593 if self.saw_data {
594 self.saw_data = false;
595 return Some(std::mem::take(&mut self.data));
596 }
597 self.data.clear();
598 continue;
599 }
600 if line.starts_with(':') {
601 continue; // comment / heartbeat line
602 }
603 let (field, value) = parse_field(line.as_ref());
604 if field == "data" {
605 if self.saw_data {
606 self.data.push('\n');
607 }
608 self.data.push_str(value);
609 self.saw_data = true;
610 }
611 // `event:`, `id:`, `retry:`, and unknown fields are ignored; the
612 // event's `type` is read from the JSON `data` payload instead.
613 }
614 }
615
616 /// Flushes a trailing event that the stream ended without terminating with a
617 /// blank line.
618 fn flush(&mut self) -> Option<String> {
619 if !self.buffer.is_empty() {
620 let line_bytes = std::mem::take(&mut self.buffer);
621 let mut end = line_bytes.len();
622 if end > 0 && line_bytes[end - 1] == b'\n' {
623 end -= 1;
624 }
625 if end > 0 && line_bytes[end - 1] == b'\r' {
626 end -= 1;
627 }
628 let line = String::from_utf8_lossy(&line_bytes[..end]);
629 if !line.is_empty() && !line.starts_with(':') {
630 let (field, value) = parse_field(line.as_ref());
631 if field == "data" {
632 if self.saw_data {
633 self.data.push('\n');
634 }
635 self.data.push_str(value);
636 self.saw_data = true;
637 }
638 }
639 }
640 if self.saw_data {
641 self.saw_data = false;
642 Some(std::mem::take(&mut self.data))
643 } else {
644 None
645 }
646 }
647}
648
649// ---------------------------------------------------------------------------
650// Accumulation.
651// ---------------------------------------------------------------------------
652
653/// An upper bound on the number of accumulated content blocks.
654///
655/// Blocks normally arrive contiguously from index 0, so any index at or beyond
656/// this bound indicates a malformed or hostile stream. The cap prevents a single
657/// `content_block_start` event with an enormous `index` (a few dozen bytes on the
658/// wire) from driving [`set_block`] to allocate placeholder blocks up to that
659/// index and exhausting memory.
660const MAX_STREAM_CONTENT_BLOCKS: usize = 1 << 16;
661
662/// Places `block` at `index` in `content`, extending with placeholder blocks if
663/// a gap appears (blocks normally arrive contiguously from index 0).
664///
665/// An `index` at or beyond [`MAX_STREAM_CONTENT_BLOCKS`] is ignored rather than
666/// gap-filled, so an untrusted byte source cannot force unbounded allocation.
667fn set_block(content: &mut Vec<ContentBlock>, index: usize, block: ContentBlock) {
668 if index < content.len() {
669 content[index] = block;
670 } else if index < MAX_STREAM_CONTENT_BLOCKS {
671 while content.len() < index {
672 content.push(ContentBlock::Unknown(serde_json::Value::Null));
673 }
674 content.push(block);
675 }
676 // else: an absurd index — drop the block to avoid unbounded allocation.
677}
678
679/// Merges a [`UsageDelta`] into a [`Usage`], replacing each present field with
680/// the delta's cumulative total.
681fn merge_usage(target: &mut Usage, delta: &UsageDelta) {
682 if let Some(value) = delta.input_tokens {
683 target.input_tokens = value;
684 }
685 if let Some(value) = delta.output_tokens {
686 target.output_tokens = value;
687 }
688 if let Some(value) = delta.cache_creation_input_tokens {
689 target.cache_creation_input_tokens = Some(value);
690 }
691 if let Some(value) = delta.cache_read_input_tokens {
692 target.cache_read_input_tokens = Some(value);
693 }
694 for (key, value) in &delta.extra {
695 target.extra.insert(key.clone(), value.clone());
696 }
697}
698
699/// Assembles a [`Message`] from the stream events as they pass by.
700#[derive(Default)]
701struct Accumulator {
702 message: Option<Message>,
703 /// Per-index buffers for `tool_use` blocks' `input_json_delta` fragments.
704 json_buffers: HashMap<usize, String>,
705}
706
707impl Accumulator {
708 fn apply(&mut self, event: &StreamEvent) {
709 match event {
710 StreamEvent::MessageStart { message } => {
711 self.message = Some(message.clone());
712 self.json_buffers.clear();
713 }
714 StreamEvent::ContentBlockStart {
715 index,
716 content_block,
717 } => {
718 if let Some(message) = self.message.as_mut() {
719 set_block(&mut message.content, *index, content_block.clone());
720 }
721 if matches!(
722 content_block,
723 ContentBlock::ToolUse(_) | ContentBlock::ServerToolUse(_)
724 ) {
725 self.json_buffers.insert(*index, String::new());
726 }
727 }
728 StreamEvent::ContentBlockDelta { index, delta } => {
729 self.apply_delta(*index, delta);
730 }
731 StreamEvent::ContentBlockStop { index } => {
732 self.finish_block(*index);
733 }
734 StreamEvent::MessageDelta { delta, usage } => {
735 if let Some(message) = self.message.as_mut() {
736 if delta.stop_reason.is_some() {
737 message.stop_reason = delta.stop_reason.clone();
738 }
739 if delta.stop_sequence.is_some() {
740 message.stop_sequence = delta.stop_sequence.clone();
741 }
742 if delta.stop_details.is_some() {
743 message.stop_details = delta.stop_details.clone();
744 }
745 if let Some(usage) = usage {
746 merge_usage(&mut message.usage, usage);
747 }
748 }
749 }
750 StreamEvent::MessageStop
751 | StreamEvent::Ping
752 | StreamEvent::Error { .. }
753 | StreamEvent::Unknown(_) => {}
754 }
755 }
756
757 fn apply_delta(&mut self, index: usize, delta: &ContentDelta) {
758 if let ContentDelta::InputJsonDelta { partial_json } = delta {
759 self.json_buffers
760 .entry(index)
761 .or_default()
762 .push_str(partial_json);
763 return;
764 }
765 let Some(message) = self.message.as_mut() else {
766 return;
767 };
768 let Some(block) = message.content.get_mut(index) else {
769 return;
770 };
771 match delta {
772 ContentDelta::TextDelta { text } => {
773 if let ContentBlock::Text(inner) = block {
774 inner.text.push_str(text);
775 }
776 }
777 ContentDelta::ThinkingDelta { thinking } => {
778 if let ContentBlock::Thinking(inner) = block {
779 inner.thinking.push_str(thinking);
780 }
781 }
782 ContentDelta::SignatureDelta { signature } => {
783 if let ContentBlock::Thinking(inner) = block {
784 inner.signature.push_str(signature);
785 }
786 }
787 ContentDelta::CitationsDelta { citation } => {
788 if let ContentBlock::Text(inner) = block {
789 inner
790 .citations
791 .get_or_insert_with(Vec::new)
792 .push(citation.clone());
793 }
794 }
795 ContentDelta::InputJsonDelta { .. } | ContentDelta::Unknown(_) => {}
796 }
797 }
798
799 fn finish_block(&mut self, index: usize) {
800 let Some(json) = self.json_buffers.remove(&index) else {
801 return;
802 };
803 let parsed = if json.trim().is_empty() {
804 // An empty input buffer means an empty tool input object.
805 serde_json::Value::Object(serde_json::Map::new())
806 } else {
807 // The concatenated fragments should parse as the tool_use `input`
808 // object. If they do not (a truncated or corrupted stream), preserve
809 // the raw accumulated bytes as a JSON string rather than silently
810 // discarding them as `null`, so the loss is detectable by the caller.
811 match serde_json::from_str(&json) {
812 Ok(value) => value,
813 Err(_) => serde_json::Value::String(json),
814 }
815 };
816 if let Some(message) = self.message.as_mut() {
817 if let Some(ContentBlock::ToolUse(inner) | ContentBlock::ServerToolUse(inner)) =
818 message.content.get_mut(index)
819 {
820 inner.input = parsed;
821 }
822 }
823 }
824}
825
826// ---------------------------------------------------------------------------
827// MessageStream.
828// ---------------------------------------------------------------------------
829
830/// The boxed byte stream a [`MessageStream`] reads from.
831/// `Send` on native targets, nothing on wasm (reqwest's wasm streams are not
832/// `Send`; single-threaded wasm doesn't need the bound).
833#[cfg(not(target_arch = "wasm32"))]
834pub trait MaybeSend: Send {}
835#[cfg(not(target_arch = "wasm32"))]
836impl<T: Send> MaybeSend for T {}
837/// `Send` on native targets, nothing on wasm.
838#[cfg(target_arch = "wasm32")]
839pub trait MaybeSend {}
840#[cfg(target_arch = "wasm32")]
841impl<T> MaybeSend for T {}
842
843#[cfg(not(target_arch = "wasm32"))]
844type ByteStream = Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>;
845#[cfg(target_arch = "wasm32")]
846type ByteStream = Pin<Box<dyn Stream<Item = Result<Bytes>>>>;
847
848/// A [`Stream`] of [`StreamEvent`]s decoded from a streaming `POST /v1/messages`
849/// response, which simultaneously accumulates a final [`Message`].
850///
851/// Drive it with your async runtime via [`futures_util::StreamExt`]. Each item
852/// is a `Result<StreamEvent>`: a decode failure or an in-stream `error` event
853/// yields `Err` and ends the stream. After the stream is exhausted (or at any
854/// point), [`final_message`](MessageStream::final_message) returns the message
855/// assembled so far; [`collect_final`](MessageStream::collect_final) drains the
856/// stream and returns the complete [`Message`].
857///
858/// # Examples
859///
860/// ```no_run
861/// use crimson_crab::api::MessagesRequest;
862/// use crimson_crab::types::MessageParam;
863///
864/// # async fn demo(client: &crimson_crab::Client) -> crimson_crab::Result<()> {
865/// let request = MessagesRequest::builder()
866/// .model("claude-opus-4-8")
867/// .max_tokens(1024)
868/// .messages(vec![MessageParam::user("Hi")])
869/// .build()?;
870/// let message = client.messages().stream(&request).await?.collect_final().await?;
871/// println!("{}", message.text());
872/// # Ok(())
873/// # }
874/// ```
875pub struct MessageStream {
876 source: ByteStream,
877 decoder: SseDecoder,
878 accumulator: Accumulator,
879 source_done: bool,
880 done: bool,
881}
882
883impl MessageStream {
884 /// Wraps a raw streaming HTTP response.
885 pub(crate) fn new(response: reqwest::Response) -> Self {
886 let source = response
887 .bytes_stream()
888 .map(|chunk| chunk.map_err(Error::from));
889 Self::from_pinned(Box::pin(source))
890 }
891
892 /// Builds a [`MessageStream`] from an arbitrary byte stream.
893 ///
894 /// This low-level constructor exists for custom transports and testing; most
895 /// callers obtain a stream via
896 /// [`Messages::stream`](crate::api::messages::Messages::stream).
897 ///
898 /// # Examples
899 ///
900 /// ```
901 /// use bytes::Bytes;
902 /// use crimson_crab::streaming::MessageStream;
903 /// use futures_util::stream;
904 ///
905 /// let chunks: Vec<crimson_crab::Result<Bytes>> =
906 /// vec![Ok(Bytes::from_static(b"event: ping\ndata: {\"type\":\"ping\"}\n\n"))];
907 /// let stream = MessageStream::from_byte_stream(stream::iter(chunks));
908 /// assert!(stream.final_message().is_none());
909 /// ```
910 #[doc(hidden)]
911 pub fn from_byte_stream<S>(stream: S) -> Self
912 where
913 S: Stream<Item = Result<Bytes>> + MaybeSend + 'static,
914 {
915 Self::from_pinned(Box::pin(stream))
916 }
917
918 fn from_pinned(source: ByteStream) -> Self {
919 Self {
920 source,
921 decoder: SseDecoder::default(),
922 accumulator: Accumulator::default(),
923 source_done: false,
924 done: false,
925 }
926 }
927
928 /// Returns the message accumulated so far, or `None` before the
929 /// `message_start` event has been seen.
930 ///
931 /// # Examples
932 ///
933 /// ```
934 /// use bytes::Bytes;
935 /// use crimson_crab::streaming::MessageStream;
936 /// use futures_util::stream;
937 ///
938 /// let stream =
939 /// MessageStream::from_byte_stream(stream::iter(Vec::<crimson_crab::Result<Bytes>>::new()));
940 /// assert!(stream.final_message().is_none());
941 /// ```
942 pub fn final_message(&self) -> Option<&Message> {
943 self.accumulator.message.as_ref()
944 }
945
946 /// Drains the stream to completion and returns the accumulated [`Message`].
947 ///
948 /// Returns the first error encountered (a decode failure, an in-stream
949 /// `error` event, or a transport error). Returns [`Error::Stream`] if the
950 /// stream ended without a `message_start` event.
951 ///
952 /// # Examples
953 ///
954 /// ```no_run
955 /// use crimson_crab::api::MessagesRequest;
956 /// use crimson_crab::types::MessageParam;
957 ///
958 /// # async fn demo(client: &crimson_crab::Client) -> crimson_crab::Result<()> {
959 /// let request = MessagesRequest::builder()
960 /// .model("claude-opus-4-8")
961 /// .max_tokens(1024)
962 /// .messages(vec![MessageParam::user("Hi")])
963 /// .build()?;
964 /// let message = client.messages().stream(&request).await?.collect_final().await?;
965 /// let _ = message.stop_reason;
966 /// # Ok(())
967 /// # }
968 /// ```
969 pub async fn collect_final(mut self) -> Result<Message> {
970 while let Some(item) = self.next().await {
971 item?;
972 }
973 self.accumulator
974 .message
975 .ok_or_else(|| Error::Stream("stream ended without a message_start event".to_string()))
976 }
977
978 /// Decodes one event `data` payload, accumulating it and returning the item
979 /// to yield (or `None` to skip an empty payload).
980 fn decode(&mut self, data: &str) -> Option<Result<StreamEvent>> {
981 if data.trim().is_empty() {
982 return None;
983 }
984 match serde_json::from_str::<StreamEvent>(data) {
985 Ok(StreamEvent::Error { error }) => {
986 self.done = true;
987 Some(Err(error::from_error_body(error.error_type, error.message)))
988 }
989 Ok(event) => {
990 self.accumulator.apply(&event);
991 Some(Ok(event))
992 }
993 Err(source) => {
994 self.done = true;
995 Some(Err(Error::from(source)))
996 }
997 }
998 }
999}
1000
1001impl Stream for MessageStream {
1002 type Item = Result<StreamEvent>;
1003
1004 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1005 let this = self.get_mut();
1006 if this.done {
1007 return Poll::Ready(None);
1008 }
1009 loop {
1010 if let Some(data) = this.decoder.next_event_data() {
1011 if let Some(item) = this.decode(&data) {
1012 return Poll::Ready(Some(item));
1013 }
1014 continue;
1015 }
1016
1017 if this.source_done {
1018 if let Some(data) = this.decoder.flush() {
1019 if let Some(item) = this.decode(&data) {
1020 return Poll::Ready(Some(item));
1021 }
1022 }
1023 this.done = true;
1024 return Poll::Ready(None);
1025 }
1026
1027 match this.source.as_mut().poll_next(cx) {
1028 Poll::Ready(Some(Ok(bytes))) => {
1029 this.decoder.push(&bytes);
1030 continue;
1031 }
1032 Poll::Ready(Some(Err(err))) => {
1033 this.done = true;
1034 return Poll::Ready(Some(Err(err)));
1035 }
1036 Poll::Ready(None) => {
1037 this.source_done = true;
1038 continue;
1039 }
1040 Poll::Pending => return Poll::Pending,
1041 }
1042 }
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049
1050 #[test]
1051 fn sse_decoder_tolerates_crlf_and_comments() {
1052 let mut decoder = SseDecoder::default();
1053 decoder.push(b": heartbeat comment\r\n");
1054 decoder.push(b"event: ping\r\ndata: {\"type\":\"ping\"}\r\n\r\n");
1055 let data = decoder.next_event_data().expect("one record");
1056 assert_eq!(data, "{\"type\":\"ping\"}");
1057 assert!(decoder.next_event_data().is_none());
1058 }
1059
1060 #[test]
1061 fn sse_decoder_joins_multiline_data() {
1062 let mut decoder = SseDecoder::default();
1063 decoder.push(b"data: line one\ndata: line two\n\n");
1064 let data = decoder.next_event_data().expect("one record");
1065 assert_eq!(data, "line one\nline two");
1066 }
1067
1068 #[test]
1069 fn content_delta_round_trips() {
1070 for raw in [
1071 r#"{"type":"text_delta","text":"Hi"}"#,
1072 r#"{"type":"input_json_delta","partial_json":"{\"a\":1}"}"#,
1073 r#"{"type":"thinking_delta","thinking":"hmm"}"#,
1074 r#"{"type":"signature_delta","signature":"sig"}"#,
1075 ] {
1076 let delta: ContentDelta = serde_json::from_str(raw).expect("parse");
1077 let out = serde_json::to_string(&delta).expect("serialize");
1078 let reparsed: ContentDelta = serde_json::from_str(&out).expect("reparse");
1079 assert_eq!(delta, reparsed);
1080 }
1081 }
1082
1083 #[test]
1084 fn empty_input_json_becomes_empty_object() {
1085 let mut acc = Accumulator::default();
1086 let message_start: StreamEvent = serde_json::from_str(
1087 r#"{"type":"message_start","message":{"id":"m1","type":"message","role":"assistant","model":"claude-opus-4-8","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":1}}}"#,
1088 )
1089 .expect("parse message_start");
1090 acc.apply(&message_start);
1091 let start: StreamEvent = serde_json::from_str(
1092 r#"{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"t1","name":"noop","input":{}}}"#,
1093 )
1094 .expect("parse start");
1095 acc.apply(&start);
1096 // No input_json_delta events at all -> empty buffer parses to `{}`.
1097 let stop: StreamEvent =
1098 serde_json::from_str(r#"{"type":"content_block_stop","index":0}"#).expect("parse stop");
1099 acc.apply(&stop);
1100 let message = acc.message.expect("no message");
1101 match &message.content[0] {
1102 ContentBlock::ToolUse(inner) => {
1103 assert_eq!(inner.input, serde_json::json!({}));
1104 }
1105 other => panic!("expected tool_use, got {other:?}"),
1106 }
1107 }
1108}