Skip to main content

claude_sdk/
streaming.rs

1//! Streaming support for Claude API.
2//!
3//! This module handles Server-Sent Events (SSE) streaming from the Claude API,
4//! providing real-time token generation and typed event parsing.
5//!
6//! # Streaming vs Non-Streaming
7//!
8//! | Aspect | Streaming | Non-Streaming |
9//! |--------|-----------|---------------|
10//! | Latency | First token immediately | Wait for full response |
11//! | UX | Progressive display | All-at-once |
12//! | Memory | Constant | Grows with response |
13//! | Use Case | Chat interfaces | Background processing |
14//!
15//! # Event Types
16//!
17//! The stream emits events in this order:
18//!
19//! 1. [`StreamEvent::MessageStart`] - Stream begins, provides message metadata
20//! 2. [`StreamEvent::ContentBlockStart`] - A new content block (text/tool/thinking) begins
21//! 3. [`StreamEvent::ContentBlockDelta`] - Incremental content updates
22//! 4. [`StreamEvent::ContentBlockStop`] - Content block complete
23//! 5. [`StreamEvent::MessageDelta`] - Final usage stats and stop reason
24//! 6. [`StreamEvent::MessageStop`] - Stream complete
25//!
26//! # Basic Streaming Example
27//!
28//! ```rust,no_run
29//! use claude_sdk::{ClaudeClient, MessagesRequest, Message, StreamEvent, ContentDelta};
30//! use futures::StreamExt;
31//!
32//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33//! let client = ClaudeClient::anthropic(std::env::var("ANTHROPIC_API_KEY")?);
34//!
35//! let request = MessagesRequest::new(
36//!     "claude-sonnet-4-5-20250929",
37//!     1024,
38//!     vec![Message::user("Write a poem about Rust.")],
39//! );
40//!
41//! let mut stream = client.send_streaming(request).await?;
42//!
43//! while let Some(event) = stream.next().await {
44//!     match event? {
45//!         StreamEvent::ContentBlockDelta { delta, .. } => {
46//!             if let ContentDelta::TextDelta { text } = delta {
47//!                 print!("{}", text);  // Stream to console
48//!             }
49//!         }
50//!         StreamEvent::MessageDelta { usage, .. } => {
51//!             println!("\n[Used {} output tokens]", usage.output_tokens);
52//!         }
53//!         StreamEvent::MessageStop => break,
54//!         _ => {}
55//!     }
56//! }
57//! # Ok(())
58//! # }
59//! ```
60//!
61//! # Handling Tool Use in Streams
62//!
63//! Tool calls are streamed as JSON deltas that need to be accumulated:
64//!
65//! ```rust,no_run
66//! use claude_sdk::{StreamEvent, ContentDelta};
67//!
68//! # fn example(event: StreamEvent) {
69//! match event {
70//!     StreamEvent::ContentBlockStart { content_block, .. } => {
71//!         // Tool use block starting - capture the tool name
72//!     }
73//!     StreamEvent::ContentBlockDelta { delta, .. } => {
74//!         if let ContentDelta::InputJsonDelta { partial_json } = delta {
75//!             // Accumulate JSON fragments
76//!             // parse when ContentBlockStop is received
77//!         }
78//!     }
79//!     StreamEvent::ContentBlockStop { .. } => {
80//!         // Tool JSON complete - now parse and execute
81//!     }
82//!     _ => {}
83//! }
84//! # }
85//! ```
86//!
87//! # Extended Thinking in Streams
88//!
89//! When extended thinking is enabled, thinking blocks are streamed first:
90//!
91//! ```rust,no_run
92//! use claude_sdk::{StreamEvent, ContentDelta};
93//!
94//! # fn example(event: StreamEvent) {
95//! match event {
96//!     StreamEvent::ContentBlockDelta { delta, .. } => {
97//!         match delta {
98//!             ContentDelta::ThinkingDelta { thinking } => {
99//!                 // Claude's reasoning process
100//!                 print!("[thinking] {}", thinking);
101//!             }
102//!             ContentDelta::TextDelta { text } => {
103//!                 // Final response
104//!                 print!("{}", text);
105//!             }
106//!             _ => {}
107//!         }
108//!     }
109//!     _ => {}
110//! }
111//! # }
112//! ```
113
114use crate::types::{ContentBlock, Role, StopReason, Usage};
115use serde::{Deserialize, Serialize};
116
117/// Events emitted during streaming responses
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum StreamEvent {
121    /// Stream started - provides message metadata
122    MessageStart { message: MessageMetadata },
123
124    /// A content block has started
125    ContentBlockStart {
126        index: usize,
127        content_block: ContentBlock,
128    },
129
130    /// Delta update to a content block
131    ContentBlockDelta { index: usize, delta: ContentDelta },
132
133    /// A content block has finished
134    ContentBlockStop { index: usize },
135
136    /// Delta update to the message (includes final usage)
137    MessageDelta { delta: MessageDelta, usage: Usage },
138
139    /// Stream has ended
140    MessageStop,
141
142    /// Ping event (keepalive)
143    Ping,
144
145    /// Error event
146    Error { error: StreamError },
147}
148
149/// Message metadata provided at stream start
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct MessageMetadata {
152    pub id: String,
153
154    #[serde(rename = "type")]
155    pub message_type: String,
156
157    pub role: Role,
158
159    #[serde(default)]
160    pub content: Vec<ContentBlock>,
161
162    pub model: String,
163
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub stop_reason: Option<StopReason>,
166
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub stop_sequence: Option<String>,
169
170    pub usage: Usage,
171}
172
173/// Delta update to content
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(tag = "type", rename_all = "snake_case")]
176pub enum ContentDelta {
177    /// Text delta
178    TextDelta { text: String },
179
180    /// Input JSON delta for tool use
181    InputJsonDelta { partial_json: String },
182
183    /// Thinking delta (extended thinking)
184    ThinkingDelta { thinking: String },
185
186    /// Signature delta for thinking blocks
187    SignatureDelta { signature: String },
188}
189
190impl ContentDelta {
191    /// Extract text if this is a text delta
192    pub fn text(&self) -> Option<&str> {
193        match self {
194            ContentDelta::TextDelta { text } => Some(text),
195            _ => None,
196        }
197    }
198
199    /// Extract partial JSON if this is an input JSON delta
200    pub fn partial_json(&self) -> Option<&str> {
201        match self {
202            ContentDelta::InputJsonDelta { partial_json } => Some(partial_json),
203            _ => None,
204        }
205    }
206
207    /// Extract thinking if this is a thinking delta
208    pub fn thinking(&self) -> Option<&str> {
209        match self {
210            ContentDelta::ThinkingDelta { thinking } => Some(thinking),
211            _ => None,
212        }
213    }
214
215    /// Extract signature if this is a signature delta
216    pub fn signature(&self) -> Option<&str> {
217        match self {
218            ContentDelta::SignatureDelta { signature } => Some(signature),
219            _ => None,
220        }
221    }
222}
223
224/// Delta update to the message
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct MessageDelta {
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub stop_reason: Option<StopReason>,
229
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub stop_sequence: Option<String>,
232}
233
234/// Error in the stream
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct StreamError {
237    #[serde(rename = "type")]
238    pub error_type: String,
239
240    pub message: String,
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_parse_message_start() {
249        let json = r#"{
250            "type": "message_start",
251            "message": {
252                "id": "msg_123",
253                "type": "message",
254                "role": "assistant",
255                "content": [],
256                "model": "claude-sonnet-4-5-20250929",
257                "stop_reason": null,
258                "stop_sequence": null,
259                "usage": {
260                    "input_tokens": 10,
261                    "output_tokens": 0
262                }
263            }
264        }"#;
265
266        let event: StreamEvent = serde_json::from_str(json).unwrap();
267        match event {
268            StreamEvent::MessageStart { message } => {
269                assert_eq!(message.id, "msg_123");
270                assert_eq!(message.role, Role::Assistant);
271            }
272            _ => panic!("Expected MessageStart"),
273        }
274    }
275
276    #[test]
277    fn test_parse_content_block_delta() {
278        let json = r#"{
279            "type": "content_block_delta",
280            "index": 0,
281            "delta": {
282                "type": "text_delta",
283                "text": "Hello"
284            }
285        }"#;
286
287        let event: StreamEvent = serde_json::from_str(json).unwrap();
288        match event {
289            StreamEvent::ContentBlockDelta { index, delta } => {
290                assert_eq!(index, 0);
291                assert_eq!(delta.text(), Some("Hello"));
292            }
293            _ => panic!("Expected ContentBlockDelta"),
294        }
295    }
296
297    #[test]
298    fn test_content_delta_helpers() {
299        let text_delta = ContentDelta::TextDelta {
300            text: "test".to_string(),
301        };
302        assert_eq!(text_delta.text(), Some("test"));
303        assert_eq!(text_delta.partial_json(), None);
304
305        let json_delta = ContentDelta::InputJsonDelta {
306            partial_json: r#"{"key":"#.to_string(),
307        };
308        assert_eq!(json_delta.text(), None);
309        assert_eq!(json_delta.partial_json(), Some(r#"{"key":"#));
310    }
311}