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}