//! Anthropic SSEイベントパース //! //! Anthropic Messages APIのSSEイベントをパースし、統一Event型に変換 use crate::llm_client::{ ClientError, event::{ BlockDelta, BlockMetadata, BlockStart, BlockStop, BlockType, DeltaContent, ErrorEvent, Event, PingEvent, ResponseStatus, StatusEvent, UsageEvent, }, }; use serde::Deserialize; use super::AnthropicScheme; use super::scheme_impl::{AnthropicState, PendingThinking}; /// Anthropic SSEイベントタイプ #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum AnthropicEventType { MessageStart, ContentBlockStart, ContentBlockDelta, ContentBlockStop, MessageDelta, MessageStop, Ping, Error, } impl AnthropicEventType { /// イベントタイプ文字列からパース pub(crate) fn parse(s: &str) -> Option { match s { "message_start" => Some(Self::MessageStart), "content_block_start" => Some(Self::ContentBlockStart), "content_block_delta" => Some(Self::ContentBlockDelta), "content_block_stop" => Some(Self::ContentBlockStop), "message_delta" => Some(Self::MessageDelta), "message_stop" => Some(Self::MessageStop), "ping" => Some(Self::Ping), "error" => Some(Self::Error), _ => None, } } } // ============================================================================ // SSEイベントのJSON構造 // ============================================================================ /// message_start イベント #[derive(Debug, Deserialize)] pub(crate) struct MessageStartEvent { pub message: MessageStartMessage, } #[allow(dead_code)] #[derive(Debug, Deserialize)] pub(crate) struct MessageStartMessage { pub id: String, pub model: String, pub usage: Option, } /// content_block_start イベント #[derive(Debug, Deserialize)] pub(crate) struct ContentBlockStartEvent { pub index: usize, pub content_block: ContentBlock, } #[allow(dead_code)] #[derive(Debug, Deserialize)] #[serde(tag = "type")] pub(crate) enum ContentBlock { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "thinking")] Thinking { #[serde(default)] thinking: String, /// 非ストリーミングレスポンス由来の初期 signature(通常はストリームでは /// 空 → `signature_delta` で埋まる)。 #[serde(default)] signature: Option, }, #[serde(rename = "redacted_thinking")] RedactedThinking { /// 暗号化された opaque blob。signature ではなく、まるごと /// `redacted_thinking.data` として送り返す必要がある。 #[serde(default)] data: String, }, #[serde(rename = "tool_use")] ToolUse { id: String, name: String, input: serde_json::Value, }, } /// content_block_delta イベント #[derive(Debug, Deserialize)] pub(crate) struct ContentBlockDeltaEvent { pub index: usize, pub delta: DeltaBlock, } #[allow(dead_code)] #[derive(Debug, Deserialize)] #[serde(tag = "type")] pub(crate) enum DeltaBlock { #[serde(rename = "text_delta")] TextDelta { text: String }, #[serde(rename = "thinking_delta")] ThinkingDelta { thinking: String }, #[serde(rename = "input_json_delta")] InputJsonDelta { partial_json: String }, #[serde(rename = "signature_delta")] SignatureDelta { signature: String }, } /// content_block_stop イベント #[derive(Debug, Deserialize)] pub(crate) struct ContentBlockStopEvent { pub index: usize, } /// message_delta イベント #[allow(dead_code)] #[derive(Debug, Deserialize)] pub(crate) struct MessageDeltaEvent { pub delta: MessageDeltaData, pub usage: Option, } #[allow(dead_code)] #[derive(Debug, Deserialize)] pub(crate) struct MessageDeltaData { pub stop_reason: Option, pub stop_sequence: Option, } /// 使用量データ #[derive(Debug, Deserialize)] pub(crate) struct UsageData { pub input_tokens: Option, pub output_tokens: Option, pub cache_read_input_tokens: Option, pub cache_creation_input_tokens: Option, } /// エラーイベント #[derive(Debug, Deserialize)] pub(crate) struct ErrorEventData { pub error: ErrorDetail, } #[derive(Debug, Deserialize)] pub(crate) struct ErrorDetail { #[serde(rename = "type")] pub error_type: String, pub message: String, } // ============================================================================ // イベント変換 // ============================================================================ impl AnthropicScheme { /// SSEイベントをEvent型に変換 /// /// # Arguments /// * `event_type` - SSEイベントタイプ /// * `data` - イベントデータJSON文字列 /// /// # Returns /// * `Ok(Some(Event))` - 変換成功 /// * `Ok(None)` - イベントを無視(unknown event等) /// * `Err(ClientError)` - パースエラー pub(crate) fn parse_event( &self, event_type: &str, data: &str, ) -> Result, ClientError> { let Some(event_type) = AnthropicEventType::parse(event_type) else { // Unknown event type, ignore return Ok(None); }; match event_type { AnthropicEventType::MessageStart => { let event: MessageStartEvent = serde_json::from_str(data)?; // message_start時にUsageイベントがあれば出力 if let Some(usage) = event.message.usage { return Ok(Some(Event::Usage(self.convert_usage(&usage)))); } // Statusイベントとして開始を通知 Ok(Some(Event::Status(StatusEvent { status: ResponseStatus::Started, }))) } AnthropicEventType::ContentBlockStart => { let event: ContentBlockStartEvent = serde_json::from_str(data)?; Ok(Some(self.convert_block_start(&event))) } AnthropicEventType::ContentBlockDelta => { let event: ContentBlockDeltaEvent = serde_json::from_str(data)?; Ok(self.convert_block_delta(&event)) } AnthropicEventType::ContentBlockStop => { let event: ContentBlockStopEvent = serde_json::from_str(data)?; // Note: BlockStopにはblock_typeが必要だが、AnthropicはStopイベントに含めない // Timeline層がBlockStartを追跡して正しいblock_typeを知る Ok(Some(Event::BlockStop(BlockStop { index: event.index, block_type: BlockType::Text, // Timeline層で上書きされる stop_reason: None, reasoning: None, }))) } AnthropicEventType::MessageDelta => { let event: MessageDeltaEvent = serde_json::from_str(data)?; // Usage情報があれば出力 if let Some(usage) = event.usage { return Ok(Some(Event::Usage(self.convert_usage(&usage)))); } Ok(None) } AnthropicEventType::MessageStop => Ok(Some(Event::Status(StatusEvent { status: ResponseStatus::Completed, }))), AnthropicEventType::Ping => Ok(Some(Event::Ping(PingEvent { timestamp: None }))), AnthropicEventType::Error => { let event: ErrorEventData = serde_json::from_str(data)?; Ok(Some(Event::Error(ErrorEvent { code: Some(event.error.error_type), message: event.error.message, }))) } } } fn convert_block_start(&self, event: &ContentBlockStartEvent) -> Event { let (block_type, metadata) = match &event.content_block { ContentBlock::Text { .. } => (BlockType::Text, BlockMetadata::Text), ContentBlock::Thinking { .. } | ContentBlock::RedactedThinking { .. } => { (BlockType::Thinking, BlockMetadata::Thinking) } ContentBlock::ToolUse { id, name, .. } => ( BlockType::ToolUse, BlockMetadata::ToolUse { id: id.clone(), name: name.clone(), }, ), }; Event::BlockStart(BlockStart { index: event.index, block_type, metadata, }) } fn convert_block_delta(&self, event: &ContentBlockDeltaEvent) -> Option { let delta = match &event.delta { DeltaBlock::TextDelta { text } => DeltaContent::Text(text.clone()), DeltaBlock::ThinkingDelta { thinking } => DeltaContent::Thinking(thinking.clone()), DeltaBlock::InputJsonDelta { partial_json } => { DeltaContent::InputJson(partial_json.clone()) } DeltaBlock::SignatureDelta { .. } => { // signature_delta は無視 return None; } }; Some(Event::BlockDelta(BlockDelta { index: event.index, delta, })) } /// state を持ち回す上位パース。 /// /// `parse_event` の単発 Event に加えて、以下を行う: /// - `content_block_stop` の `block_type` を直前の Start 値で書き戻す /// - `thinking` / `redacted_thinking` ブロックの本体・signature・data を /// `state.pending_thinking` に蓄積し、`content_block_stop` の Thinking /// BlockStop metadata に載せる /// - `signature_delta` を蓄積(Stream channel には流さず、reasoning metadata /// にだけ反映する) pub(crate) fn parse_with_state( &self, event_type: &str, data: &str, state: &mut AnthropicState, ) -> Result, ClientError> { let Some(parsed_event_type) = AnthropicEventType::parse(event_type) else { return Ok(Vec::new()); }; // signature_delta はストリーム表示には流さず、state にだけ蓄積。 // それ以外は parse_event で標準 Event 化する。 let mut emitted: Vec = Vec::new(); match parsed_event_type { AnthropicEventType::ContentBlockStart => { let raw: ContentBlockStartEvent = serde_json::from_str(data)?; state.current_block_type = Some(match &raw.content_block { ContentBlock::Text { .. } => BlockType::Text, ContentBlock::Thinking { .. } | ContentBlock::RedactedThinking { .. } => { BlockType::Thinking } ContentBlock::ToolUse { .. } => BlockType::ToolUse, }); match &raw.content_block { ContentBlock::Thinking { thinking, signature, } => { state.pending_thinking = Some(PendingThinking { text: thinking.clone(), signature: signature.clone(), redacted_data: None, }); } ContentBlock::RedactedThinking { data: blob } => { state.pending_thinking = Some(PendingThinking { text: String::new(), signature: None, redacted_data: Some(blob.clone()), }); } _ => {} } emitted.push(self.convert_block_start(&raw)); } AnthropicEventType::ContentBlockDelta => { let raw: ContentBlockDeltaEvent = serde_json::from_str(data)?; match &raw.delta { DeltaBlock::ThinkingDelta { thinking } => { if let Some(pending) = state.pending_thinking.as_mut() { pending.text.push_str(thinking); } emitted.push(Event::BlockDelta(BlockDelta { index: raw.index, delta: DeltaContent::Thinking(thinking.clone()), })); } DeltaBlock::SignatureDelta { signature } => { if let Some(pending) = state.pending_thinking.as_mut() { // 通常 1 回しか来ないが、複数 fragment 来ても連結しておく match &mut pending.signature { Some(acc) => acc.push_str(signature), None => pending.signature = Some(signature.clone()), } } } DeltaBlock::TextDelta { text } => { emitted.push(Event::BlockDelta(BlockDelta { index: raw.index, delta: DeltaContent::Text(text.clone()), })); } DeltaBlock::InputJsonDelta { partial_json } => { emitted.push(Event::BlockDelta(BlockDelta { index: raw.index, delta: DeltaContent::InputJson(partial_json.clone()), })); } } } AnthropicEventType::ContentBlockStop => { let raw: ContentBlockStopEvent = serde_json::from_str(data)?; let block_type = state.current_block_type.take().unwrap_or(BlockType::Text); let reasoning = if matches!(block_type, BlockType::Thinking) { state .pending_thinking .take() .map(PendingThinking::into_reasoning) } else { state.pending_thinking.take(); None }; emitted.push(Event::BlockStop(BlockStop { index: raw.index, block_type, stop_reason: None, reasoning, })); } // 残りは state を必要としない。既存 parse_event に委譲。 _ => { if let Some(event) = self.parse_event(event_type, data)? { emitted.push(event); } } } Ok(emitted) } fn convert_usage(&self, usage: &UsageData) -> UsageEvent { // Anthropic の `input_tokens` は **キャッシュ外** の入力トークンのみで、 // プロンプト全長は input_tokens + cache_read + cache_creation。 // UsageEvent の `input_tokens` には「占有量(プロンプト全長)」を載せる // 規約に合わせて、ここでキャッシュ分を足し込む。 // cache_read_input_tokens / cache_creation_input_tokens は内訳として // 別フィールドに残るので、料金計算側で `input - cache_read - cache_creation` // により非キャッシュ入力分は逆算可能。 let raw_input = usage.input_tokens.unwrap_or(0); let cache_read = usage.cache_read_input_tokens.unwrap_or(0); let cache_creation = usage.cache_creation_input_tokens.unwrap_or(0); let input_total = raw_input + cache_read + cache_creation; let output = usage.output_tokens.unwrap_or(0); UsageEvent { input_tokens: usage.input_tokens.map(|_| input_total), output_tokens: usage.output_tokens, total_tokens: Some(input_total + output), cache_read_input_tokens: usage.cache_read_input_tokens, cache_creation_input_tokens: usage.cache_creation_input_tokens, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_message_start() { let scheme = AnthropicScheme::new(); let data = r#"{"type":"message_start","message":{"id":"msg_123","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-20250514","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":0}}}"#; let event = scheme.parse_event("message_start", data).unwrap().unwrap(); match event { Event::Usage(u) => { // キャッシュなしなので input_total = raw_input = 10 assert_eq!(u.input_tokens, Some(10)); } _ => panic!("Expected Usage event"), } } #[test] fn test_convert_usage_includes_cache_in_input_total() { // Anthropic の input_tokens はキャッシュ外のみで、占有量は // input + cache_read + cache_creation。 // UsageEvent.input_tokens は占有量に正規化される。 let scheme = AnthropicScheme::new(); let usage = UsageData { input_tokens: Some(100), output_tokens: Some(50), cache_read_input_tokens: Some(800), cache_creation_input_tokens: Some(200), }; let event = scheme.convert_usage(&usage); // 100 + 800 + 200 = 1100 assert_eq!(event.input_tokens, Some(1100)); assert_eq!(event.cache_read_input_tokens, Some(800)); assert_eq!(event.cache_creation_input_tokens, Some(200)); assert_eq!(event.total_tokens, Some(1150)); } #[test] fn test_parse_content_block_start_text() { let scheme = AnthropicScheme::new(); let data = r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#; let event = scheme .parse_event("content_block_start", data) .unwrap() .unwrap(); match event { Event::BlockStart(s) => { assert_eq!(s.index, 0); assert_eq!(s.block_type, BlockType::Text); } _ => panic!("Expected BlockStart event"), } } #[test] fn test_parse_content_block_delta_text() { let scheme = AnthropicScheme::new(); let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#; let event = scheme .parse_event("content_block_delta", data) .unwrap() .unwrap(); match event { Event::BlockDelta(d) => { assert_eq!(d.index, 0); match d.delta { DeltaContent::Text(t) => assert_eq!(t, "Hello"), _ => panic!("Expected Text delta"), } } _ => panic!("Expected BlockDelta event"), } } #[test] fn test_parse_tool_use_start() { let scheme = AnthropicScheme::new(); let data = r#"{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_123","name":"get_weather","input":{}}}"#; let event = scheme .parse_event("content_block_start", data) .unwrap() .unwrap(); match event { Event::BlockStart(s) => { assert_eq!(s.block_type, BlockType::ToolUse); match s.metadata { BlockMetadata::ToolUse { id, name } => { assert_eq!(id, "toolu_123"); assert_eq!(name, "get_weather"); } _ => panic!("Expected ToolUse metadata"), } } _ => panic!("Expected BlockStart event"), } } #[test] fn thinking_block_stop_carries_reasoning_with_signature() { // thinking ブロックが完了したら reasoning metadata に text+signature が乗ること let scheme = AnthropicScheme::new(); let mut state = AnthropicState::default(); let evs = scheme .parse_with_state( "content_block_start", r#"{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}"#, &mut state, ) .unwrap(); assert!(matches!(evs[0], Event::BlockStart(_))); scheme .parse_with_state( "content_block_delta", r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"hello "}}"#, &mut state, ) .unwrap(); scheme .parse_with_state( "content_block_delta", r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"world"}}"#, &mut state, ) .unwrap(); scheme .parse_with_state( "content_block_delta", r#"{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"SIG-XYZ"}}"#, &mut state, ) .unwrap(); let stop_evs = scheme .parse_with_state( "content_block_stop", r#"{"type":"content_block_stop","index":0}"#, &mut state, ) .unwrap(); assert_eq!(stop_evs.len(), 1); let Event::BlockStop(stop) = &stop_evs[0] else { panic!("expected BlockStop, got {:?}", stop_evs[0]); }; let reasoning = stop.reasoning.as_ref().expect("reasoning metadata"); assert_eq!(reasoning.text.as_deref(), Some("hello world")); assert_eq!(reasoning.signature.as_deref(), Some("SIG-XYZ")); assert!(reasoning.encrypted_content.is_none()); } #[test] fn redacted_thinking_stop_carries_reasoning_with_data() { let scheme = AnthropicScheme::new(); let mut state = AnthropicState::default(); scheme .parse_with_state( "content_block_start", r#"{"type":"content_block_start","index":0,"content_block":{"type":"redacted_thinking","data":"opaque-blob"}}"#, &mut state, ) .unwrap(); let stop_evs = scheme .parse_with_state( "content_block_stop", r#"{"type":"content_block_stop","index":0}"#, &mut state, ) .unwrap(); assert_eq!(stop_evs.len(), 1); let Event::BlockStop(stop) = &stop_evs[0] else { panic!("expected BlockStop"); }; let reasoning = stop.reasoning.as_ref().expect("reasoning metadata"); assert_eq!(reasoning.text.as_deref(), Some("")); assert!(reasoning.signature.is_none()); assert_eq!(reasoning.encrypted_content.as_deref(), Some("opaque-blob")); } #[test] fn text_block_stop_has_no_reasoning_metadata() { let scheme = AnthropicScheme::new(); let mut state = AnthropicState::default(); scheme .parse_with_state( "content_block_start", r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#, &mut state, ) .unwrap(); scheme .parse_with_state( "content_block_delta", r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}"#, &mut state, ) .unwrap(); let stop_evs = scheme .parse_with_state( "content_block_stop", r#"{"type":"content_block_stop","index":0}"#, &mut state, ) .unwrap(); assert_eq!(stop_evs.len(), 1); let Event::BlockStop(stop) = &stop_evs[0] else { panic!("expected BlockStop"); }; assert!(stop.reasoning.is_none()); } #[test] fn test_parse_ping() { let scheme = AnthropicScheme::new(); let data = r#"{"type":"ping"}"#; let event = scheme.parse_event("ping", data).unwrap().unwrap(); match event { Event::Ping(_) => {} _ => panic!("Expected Ping event"), } } }