use crate::types::{ContentBlock, Role, StopReason, Usage};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamEvent {
MessageStart { message: MessageMetadata },
ContentBlockStart {
index: usize,
content_block: ContentBlock,
},
ContentBlockDelta { index: usize, delta: ContentDelta },
ContentBlockStop { index: usize },
MessageDelta { delta: MessageDelta, usage: Usage },
MessageStop,
Ping,
Error { error: StreamError },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageMetadata {
pub id: String,
#[serde(rename = "type")]
pub message_type: String,
pub role: Role,
#[serde(default)]
pub content: Vec<ContentBlock>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentDelta {
TextDelta { text: String },
InputJsonDelta { partial_json: String },
ThinkingDelta { thinking: String },
SignatureDelta { signature: String },
}
impl ContentDelta {
pub fn text(&self) -> Option<&str> {
match self {
ContentDelta::TextDelta { text } => Some(text),
_ => None,
}
}
pub fn partial_json(&self) -> Option<&str> {
match self {
ContentDelta::InputJsonDelta { partial_json } => Some(partial_json),
_ => None,
}
}
pub fn thinking(&self) -> Option<&str> {
match self {
ContentDelta::ThinkingDelta { thinking } => Some(thinking),
_ => None,
}
}
pub fn signature(&self) -> Option<&str> {
match self {
ContentDelta::SignatureDelta { signature } => Some(signature),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamError {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_message_start() {
let json = r#"{
"type": "message_start",
"message": {
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": null,
"stop_sequence": null,
"usage": {
"input_tokens": 10,
"output_tokens": 0
}
}
}"#;
let event: StreamEvent = serde_json::from_str(json).unwrap();
match event {
StreamEvent::MessageStart { message } => {
assert_eq!(message.id, "msg_123");
assert_eq!(message.role, Role::Assistant);
}
_ => panic!("Expected MessageStart"),
}
}
#[test]
fn test_parse_content_block_delta() {
let json = r#"{
"type": "content_block_delta",
"index": 0,
"delta": {
"type": "text_delta",
"text": "Hello"
}
}"#;
let event: StreamEvent = serde_json::from_str(json).unwrap();
match event {
StreamEvent::ContentBlockDelta { index, delta } => {
assert_eq!(index, 0);
assert_eq!(delta.text(), Some("Hello"));
}
_ => panic!("Expected ContentBlockDelta"),
}
}
#[test]
fn test_content_delta_helpers() {
let text_delta = ContentDelta::TextDelta {
text: "test".to_string(),
};
assert_eq!(text_delta.text(), Some("test"));
assert_eq!(text_delta.partial_json(), None);
let json_delta = ContentDelta::InputJsonDelta {
partial_json: r#"{"key":"#.to_string(),
};
assert_eq!(json_delta.text(), None);
assert_eq!(json_delta.partial_json(), Some(r#"{"key":"#));
}
}