use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::types::{ContentBlock, Usage};
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamEvent {
MessageStart {
message: MessageStart,
},
ContentBlockStart {
index: usize,
content_block: ContentBlockStart,
},
ContentBlockDelta {
index: usize,
delta: Delta,
},
ContentBlockStop {
index: usize,
},
MessageDelta {
delta: MessageDeltaData,
usage: Usage,
},
MessageStop,
Ping,
Error {
error: ErrorData,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct MessageStart {
pub id: String,
#[serde(rename = "type")]
pub object_type: String,
pub role: String,
pub content: Vec<JsonValue>,
pub model: String,
pub stop_reason: Option<String>,
pub stop_sequence: Option<String>,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlockStart {
Text {
text: String,
},
ToolUse {
id: String,
name: String,
input: JsonValue,
},
Thinking {
thinking: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Delta {
TextDelta {
text: String,
},
InputJsonDelta {
partial_json: String,
},
ThinkingDelta {
thinking: String,
},
SignatureDelta {
signature: String,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct MessageDeltaData {
pub stop_reason: Option<String>,
pub stop_sequence: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ErrorData {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
}
impl ErrorData {
pub fn is_retryable(&self) -> bool {
matches!(
self.error_type.as_str(),
"rate_limit_error" | "overloaded_error" | "api_error"
)
}
}
#[derive(Debug, Clone, Default)]
pub struct AccumulatedBlock {
pub block_type: Option<String>,
pub text: String,
pub tool_id: Option<String>,
pub tool_name: Option<String>,
pub partial_json: String,
pub initial_input: Option<JsonValue>,
pub thinking: String,
pub signature: Option<String>,
}
impl AccumulatedBlock {
pub fn from_start(start: &ContentBlockStart) -> Self {
let mut block = Self::default();
match start {
ContentBlockStart::Text { .. } => {
block.block_type = Some("text".to_string());
}
ContentBlockStart::ToolUse { id, name, input } => {
block.block_type = Some("tool_use".to_string());
block.tool_id = Some(id.clone());
block.tool_name = Some(name.clone());
if !input.is_null() {
block.initial_input = Some(input.clone());
}
}
ContentBlockStart::Thinking { .. } => {
block.block_type = Some("thinking".to_string());
}
}
block
}
pub fn apply_delta(&mut self, delta: &Delta) {
match delta {
Delta::TextDelta { text } => {
self.text.push_str(text);
}
Delta::InputJsonDelta { partial_json } => {
self.partial_json.push_str(partial_json);
}
Delta::ThinkingDelta { thinking } => {
self.thinking.push_str(thinking);
}
Delta::SignatureDelta { signature } => {
self.signature = Some(signature.clone());
}
}
}
pub fn to_content_block(&self) -> Result<ContentBlock, serde_json::Error> {
match self.block_type.as_deref() {
Some("text") => Ok(ContentBlock::Text {
text: self.text.clone(),
cache_control: None,
}),
Some("tool_use") => {
let input: JsonValue = if self.partial_json.trim().is_empty() {
self.initial_input.clone()
} else {
serde_json::from_str(&self.partial_json).ok()
}
.or_else(|| self.initial_input.clone())
.unwrap_or_else(|| JsonValue::Object(Default::default()));
Ok(ContentBlock::ToolUse {
id: self.tool_id.clone().unwrap_or_default(),
name: self.tool_name.clone().unwrap_or_default(),
input,
cache_control: None,
})
}
Some("thinking") => Ok(ContentBlock::Thinking {
thinking: self.thinking.clone(),
signature: self.signature.clone().unwrap_or_default(),
}),
_ => Ok(ContentBlock::Text {
text: String::new(),
cache_control: None,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_accumulated_block_text() {
let start = ContentBlockStart::Text {
text: String::new(),
};
let mut block = AccumulatedBlock::from_start(&start);
block.apply_delta(&Delta::TextDelta {
text: "Hello".to_string(),
});
block.apply_delta(&Delta::TextDelta {
text: " world".to_string(),
});
assert_eq!(block.text, "Hello world");
let content = block.to_content_block().unwrap();
match content {
ContentBlock::Text { text, .. } => assert_eq!(text, "Hello world"),
_ => panic!("Expected text block"),
}
}
#[test]
fn test_accumulated_block_tool_use() {
let start = ContentBlockStart::ToolUse {
id: "call_123".to_string(),
name: "get_weather".to_string(),
input: json!({}),
};
let mut block = AccumulatedBlock::from_start(&start);
block.apply_delta(&Delta::InputJsonDelta {
partial_json: r#"{"location":"#.to_string(),
});
block.apply_delta(&Delta::InputJsonDelta {
partial_json: r#" "Paris"}"#.to_string(),
});
let content = block.to_content_block().unwrap();
match content {
ContentBlock::ToolUse {
id, name, input, ..
} => {
assert_eq!(id, "call_123");
assert_eq!(name, "get_weather");
assert_eq!(input["location"], "Paris");
}
_ => panic!("Expected tool_use block"),
}
}
#[test]
fn test_accumulated_block_tool_use_empty_input() {
let start = ContentBlockStart::ToolUse {
id: "call_456".to_string(),
name: "read_notes".to_string(),
input: json!({}),
};
let block = AccumulatedBlock::from_start(&start);
let content = block.to_content_block().unwrap();
match content {
ContentBlock::ToolUse { input, .. } => {
assert_eq!(input, json!({}));
}
_ => panic!("Expected tool_use block"),
}
}
#[test]
fn test_accumulated_block_tool_use_truncated_partial_fallback() {
let start = ContentBlockStart::ToolUse {
id: "call_789".to_string(),
name: "read_todo_list".to_string(),
input: json!({}),
};
let mut block = AccumulatedBlock::from_start(&start);
block.partial_json = r#"{"note": "unfinished"#.to_string();
let content = block.to_content_block().unwrap();
match content {
ContentBlock::ToolUse { input, .. } => {
assert_eq!(input, json!({}));
}
_ => panic!("Expected tool_use block"),
}
}
#[test]
fn test_accumulated_block_thinking() {
let start = ContentBlockStart::Thinking {
thinking: String::new(),
};
let mut block = AccumulatedBlock::from_start(&start);
block.apply_delta(&Delta::ThinkingDelta {
thinking: "Let me think...".to_string(),
});
block.apply_delta(&Delta::SignatureDelta {
signature: "sig123".to_string(),
});
let content = block.to_content_block().unwrap();
match content {
ContentBlock::Thinking {
thinking,
signature,
} => {
assert_eq!(thinking, "Let me think...");
assert_eq!(signature, "sig123");
}
_ => panic!("Expected thinking block"),
}
}
}