use aither_core::llm::{Event as LLMEvent, ReasoningState, Usage as TokenUsage};
use crate::PROVIDER_NAME;
use serde::Deserialize;
use serde_json::Value;
use zenwave::sse::Event;
use crate::error::ClaudeError;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct MessageStartEvent {
pub message: MessageObject,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct MessageObject {
pub id: String,
pub model: String,
#[serde(default)]
pub stop_reason: Option<String>,
#[serde(default)]
pub usage: Option<Usage>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
#[serde(default)]
pub cache_creation_input_tokens: Option<u32>,
#[serde(default)]
pub cache_read_input_tokens: Option<u32>,
#[serde(default)]
pub cache_creation: Option<CacheCreation>,
}
impl Usage {
fn cache_write_tokens(&self) -> Option<u32> {
self.cache_creation_input_tokens
.or_else(|| self.cache_creation.as_ref().and_then(CacheCreation::total))
}
}
#[derive(Debug, Deserialize)]
pub struct ContentBlockStartEvent {
pub index: usize,
pub content_block: ContentBlockType,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
#[allow(dead_code)]
pub enum ContentBlockType {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "thinking")]
Thinking {
thinking: String,
#[serde(default)]
signature: String,
},
#[serde(rename = "redacted_thinking")]
RedactedThinking {
data: String,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
#[serde(default)]
input: Value,
},
}
#[derive(Debug, Deserialize)]
pub struct ContentBlockDeltaEvent {
pub index: usize,
pub delta: DeltaType,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
#[allow(clippy::enum_variant_names)]
pub enum DeltaType {
#[serde(rename = "text_delta")]
TextDelta {
text: String,
},
#[serde(rename = "thinking_delta")]
ThinkingDelta {
thinking: String,
},
#[serde(rename = "signature_delta")]
SignatureDelta {
signature: String,
},
#[serde(rename = "input_json_delta")]
InputJsonDelta {
partial_json: String,
},
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct MessageDeltaEvent {
pub delta: MessageDelta,
#[serde(default)]
pub usage: Option<DeltaUsage>,
}
#[derive(Debug, Deserialize)]
pub struct MessageDelta {
pub stop_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct DeltaUsage {
#[serde(default)]
pub output_tokens: Option<u32>,
#[serde(default)]
pub cache_creation_input_tokens: Option<u32>,
#[serde(default)]
pub cache_read_input_tokens: Option<u32>,
#[serde(default)]
pub cache_creation: Option<CacheCreation>,
}
impl DeltaUsage {
fn cache_write_tokens(&self) -> Option<u32> {
self.cache_creation_input_tokens
.or_else(|| self.cache_creation.as_ref().and_then(CacheCreation::total))
}
}
#[derive(Debug, Deserialize)]
pub struct CacheCreation {
#[serde(default)]
pub ephemeral_5m_input_tokens: Option<u32>,
#[serde(default)]
pub ephemeral_1h_input_tokens: Option<u32>,
}
impl CacheCreation {
fn total(&self) -> Option<u32> {
match (
self.ephemeral_5m_input_tokens,
self.ephemeral_1h_input_tokens,
) {
(None, None) => None,
(a, b) => Some(a.unwrap_or(0).saturating_add(b.unwrap_or(0))),
}
}
}
#[derive(Debug, Clone)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub input: Value,
}
#[derive(Debug, Default)]
pub struct StreamState {
pub blocks: Vec<BlockState>,
pub tool_calls: Vec<ToolCall>,
pub stop_reason: Option<String>,
pub prompt_tokens: Option<u32>,
pub uncached_input_tokens: Option<u32>,
pub completion_tokens: Option<u32>,
pub cache_read_tokens: Option<u32>,
pub cache_write_tokens: Option<u32>,
pub usage_emitted: bool,
}
#[derive(Debug, Clone)]
pub enum BlockState {
Text(String),
Thinking {
text: String,
signature: String,
},
RedactedThinking(String),
ToolUse {
id: String,
name: String,
input_json: String,
},
}
impl StreamState {
pub fn new() -> Self {
Self::default()
}
fn maybe_usage_event(&mut self) -> Option<LLMEvent> {
if self.usage_emitted {
return None;
}
if self.prompt_tokens.is_none()
&& self.completion_tokens.is_none()
&& self.cache_read_tokens.is_none()
&& self.cache_write_tokens.is_none()
&& self.stop_reason.is_none()
{
return None;
}
self.usage_emitted = true;
let total_tokens = match (self.prompt_tokens, self.completion_tokens) {
(Some(prompt), Some(completion)) => Some(prompt + completion),
_ => None,
};
Some(LLMEvent::Usage(TokenUsage {
prompt_tokens: self.prompt_tokens,
completion_tokens: self.completion_tokens,
total_tokens,
reasoning_tokens: None,
cache_read_tokens: self.cache_read_tokens,
cache_write_tokens: self.cache_write_tokens,
cost_usd: None,
stop_reason: self.stop_reason.clone(),
}))
}
fn refresh_prompt_tokens(&mut self) {
let parts = [
self.uncached_input_tokens,
self.cache_read_tokens,
self.cache_write_tokens,
];
self.prompt_tokens = parts.iter().any(Option::is_some).then(|| {
parts
.iter()
.flatten()
.fold(0u32, |total, part| total.saturating_add(*part))
});
}
}
pub fn parse_event(event: &Event, state: &mut StreamState) -> Result<Vec<LLMEvent>, ClaudeError> {
let data = event.text_data();
match event.event().unwrap_or("") {
"message_start" => {
state.apply_message_start(serde_json::from_str(data)?);
Ok(Vec::new())
}
"content_block_start" => Ok(state.begin_block(serde_json::from_str(data)?)),
"content_block_delta" => Ok(state.apply_block_delta(serde_json::from_str(data)?)),
"content_block_stop" => {
#[derive(Deserialize)]
struct StopEvent {
index: usize,
}
let ev: StopEvent = serde_json::from_str(data)?;
Ok(state.finish_block(ev.index))
}
"message_delta" => {
state.apply_message_delta(serde_json::from_str(data)?);
Ok(Vec::new())
}
"message_stop" => Ok(state.maybe_usage_event().into_iter().collect()),
"ping" | "" => Ok(Vec::new()),
"error" => Err(parse_error_event(data)),
unknown => {
tracing::debug!("Unknown Claude SSE event type: {unknown}");
Ok(Vec::new())
}
}
}
fn parse_error_event(data: &str) -> ClaudeError {
#[derive(Deserialize)]
struct ErrorEvent {
error: ErrorDetail,
}
#[derive(Deserialize)]
struct ErrorDetail {
message: String,
}
serde_json::from_str::<ErrorEvent>(data).map_or_else(
|_| ClaudeError::Api(data.to_string()),
|ev| ClaudeError::Api(ev.error.message),
)
}
impl StreamState {
fn apply_message_start(&mut self, ev: MessageStartEvent) {
let Some(usage) = ev.message.usage else {
return;
};
self.uncached_input_tokens = Some(usage.input_tokens);
self.completion_tokens = Some(usage.output_tokens);
self.cache_write_tokens = usage.cache_write_tokens();
self.cache_read_tokens = usage.cache_read_input_tokens;
self.refresh_prompt_tokens();
}
fn begin_block(&mut self, ev: ContentBlockStartEvent) -> Vec<LLMEvent> {
ensure_block_capacity(self, ev.index);
match ev.content_block {
ContentBlockType::Text { text } => {
self.blocks[ev.index] = BlockState::Text(text.clone());
if text.is_empty() {
Vec::new()
} else {
vec![LLMEvent::Text(text)]
}
}
ContentBlockType::Thinking {
thinking,
signature,
} => {
self.blocks[ev.index] = BlockState::Thinking {
text: thinking.clone(),
signature,
};
if thinking.is_empty() {
Vec::new()
} else {
vec![LLMEvent::Reasoning(thinking)]
}
}
ContentBlockType::RedactedThinking { data } => {
self.blocks[ev.index] = BlockState::RedactedThinking(data);
Vec::new()
}
ContentBlockType::ToolUse { id, name, .. } => {
let started = LLMEvent::ToolCallDelta {
id: id.clone(),
name: name.clone(),
arguments_fragment: String::new(),
};
self.blocks[ev.index] = BlockState::ToolUse {
id,
name,
input_json: String::new(),
};
vec![started]
}
}
}
fn apply_block_delta(&mut self, ev: ContentBlockDeltaEvent) -> Vec<LLMEvent> {
let Some(block) = self.blocks.get_mut(ev.index) else {
return Vec::new();
};
match (block, ev.delta) {
(BlockState::Text(text), DeltaType::TextDelta { text: delta }) => {
text.push_str(&delta);
vec![LLMEvent::Text(delta)]
}
(BlockState::Thinking { text, .. }, DeltaType::ThinkingDelta { thinking: delta }) => {
text.push_str(&delta);
vec![LLMEvent::Reasoning(delta)]
}
(
BlockState::Thinking { signature, .. },
DeltaType::SignatureDelta {
signature: delta, ..
},
) => {
signature.push_str(&delta);
Vec::new()
}
(
BlockState::ToolUse {
id,
name,
input_json,
},
DeltaType::InputJsonDelta { partial_json },
) => {
input_json.push_str(&partial_json);
vec![LLMEvent::ToolCallDelta {
id: id.clone(),
name: name.clone(),
arguments_fragment: input_json.clone(),
}]
}
_ => Vec::new(),
}
}
fn finish_block(&mut self, index: usize) -> Vec<LLMEvent> {
match self.blocks.get(index) {
Some(BlockState::ToolUse {
id,
name,
input_json,
}) => {
let input = serde_json::from_str(input_json)
.unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
self.tool_calls.push(ToolCall {
id: id.clone(),
name: name.clone(),
input,
});
Vec::new()
}
Some(BlockState::Thinking { text, signature }) => {
if signature.is_empty() {
return Vec::new();
}
let block = serde_json::json!({
"type": "thinking",
"thinking": text,
"signature": signature,
});
vec![LLMEvent::ReasoningState(ReasoningState::new(
PROVIDER_NAME,
block.to_string(),
))]
}
Some(BlockState::RedactedThinking(data)) => {
let block = serde_json::json!({
"type": "redacted_thinking",
"data": data,
});
vec![LLMEvent::ReasoningState(ReasoningState::new(
PROVIDER_NAME,
block.to_string(),
))]
}
Some(BlockState::Text(_)) | None => Vec::new(),
}
}
fn apply_message_delta(&mut self, ev: MessageDeltaEvent) {
self.stop_reason = ev.delta.stop_reason;
let Some(usage) = ev.usage else {
return;
};
if let Some(output_tokens) = usage.output_tokens {
self.completion_tokens = Some(output_tokens);
}
if let Some(cache_write_tokens) = usage.cache_write_tokens() {
self.cache_write_tokens = Some(cache_write_tokens);
}
if let Some(cache_read_tokens) = usage.cache_read_input_tokens {
self.cache_read_tokens = Some(cache_read_tokens);
}
self.refresh_prompt_tokens();
}
}
fn ensure_block_capacity(state: &mut StreamState, index: usize) {
while state.blocks.len() <= index {
state.blocks.push(BlockState::Text(String::new()));
}
}
pub fn should_skip_event(event: &Event) -> bool {
let data = event.text_data();
data.is_empty() || event.event() == Some("ping")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_event_includes_claude_cache_token_stats() {
let mut state = StreamState::new();
state.prompt_tokens = Some(120);
state.completion_tokens = Some(30);
state.cache_read_tokens = Some(90);
state.cache_write_tokens = Some(45);
state.stop_reason = Some("end_turn".to_string());
let event = state
.maybe_usage_event()
.expect("usage event should be emitted");
match event {
LLMEvent::Usage(usage) => {
assert_eq!(usage.prompt_tokens, Some(120));
assert_eq!(usage.completion_tokens, Some(30));
assert_eq!(usage.total_tokens, Some(150));
assert_eq!(usage.cache_read_tokens, Some(90));
assert_eq!(usage.cache_write_tokens, Some(45));
assert_eq!(usage.stop_reason.as_deref(), Some("end_turn"));
}
_ => panic!("expected usage event"),
}
}
#[test]
fn usage_event_is_not_reemitted() {
let mut state = StreamState::new();
state.prompt_tokens = Some(1);
assert!(state.maybe_usage_event().is_some());
assert!(state.maybe_usage_event().is_none());
}
#[test]
fn cache_creation_total_sums_5m_and_1h_tokens() {
let cache_creation = CacheCreation {
ephemeral_5m_input_tokens: Some(20),
ephemeral_1h_input_tokens: Some(30),
};
assert_eq!(cache_creation.total(), Some(50));
}
#[test]
fn refresh_prompt_tokens_adds_uncached_and_cache_tokens() {
let mut state = StreamState::new();
state.uncached_input_tokens = Some(40);
state.cache_read_tokens = Some(30);
state.cache_write_tokens = Some(10);
state.refresh_prompt_tokens();
assert_eq!(state.prompt_tokens, Some(80));
}
}