//! Open Responses Event Parser //! //! Parses SSE events from the Open Responses API into internal Event types. use serde::Deserialize; use crate::llm_client::{ event::{ BlockMetadata, BlockStart, BlockStop, DeltaContent, ErrorEvent, Event, ResponseStatus, StatusEvent, StopReason, UsageEvent, }, ClientError, }; // ============================================================================= // Open Responses SSE Event Types // ============================================================================= /// Response created event #[derive(Debug, Deserialize)] pub struct ResponseCreatedEvent { pub response: ResponseObject, } /// Response object #[derive(Debug, Deserialize)] pub struct ResponseObject { pub id: String, pub status: String, #[serde(default)] pub output: Vec, pub usage: Option, } /// Output item in response #[derive(Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum OutputItem { Message { id: String, role: String, #[serde(default)] content: Vec, }, FunctionCall { id: String, call_id: String, name: String, arguments: String, }, Reasoning { id: String, #[serde(default)] text: String, }, } /// Content part object #[derive(Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentPartObject { OutputText { text: String }, InputText { text: String }, Refusal { refusal: String }, } /// Usage object #[derive(Debug, Deserialize)] pub struct UsageObject { pub input_tokens: Option, pub output_tokens: Option, pub total_tokens: Option, } /// Output item added event #[derive(Debug, Deserialize)] pub struct OutputItemAddedEvent { pub output_index: usize, pub item: OutputItem, } /// Text delta event #[derive(Debug, Deserialize)] pub struct TextDeltaEvent { pub output_index: usize, pub content_index: usize, pub delta: String, } /// Text done event #[derive(Debug, Deserialize)] pub struct TextDoneEvent { pub output_index: usize, pub content_index: usize, pub text: String, } /// Function call arguments delta event #[derive(Debug, Deserialize)] pub struct FunctionCallArgumentsDeltaEvent { pub output_index: usize, pub call_id: String, pub delta: String, } /// Function call arguments done event #[derive(Debug, Deserialize)] pub struct FunctionCallArgumentsDoneEvent { pub output_index: usize, pub call_id: String, pub arguments: String, } /// Reasoning delta event #[derive(Debug, Deserialize)] pub struct ReasoningDeltaEvent { pub output_index: usize, pub delta: String, } /// Reasoning done event #[derive(Debug, Deserialize)] pub struct ReasoningDoneEvent { pub output_index: usize, pub text: String, } /// Content part done event #[derive(Debug, Deserialize)] pub struct ContentPartDoneEvent { pub output_index: usize, pub content_index: usize, pub part: ContentPartObject, } /// Output item done event #[derive(Debug, Deserialize)] pub struct OutputItemDoneEvent { pub output_index: usize, pub item: OutputItem, } /// Response done event #[derive(Debug, Deserialize)] pub struct ResponseDoneEvent { pub response: ResponseObject, } /// Error event from API #[derive(Debug, Deserialize)] pub struct ApiErrorEvent { pub error: ApiError, } /// API error details #[derive(Debug, Deserialize)] pub struct ApiError { pub code: Option, pub message: String, } // ============================================================================= // Event Parsing // ============================================================================= /// Parse SSE event into internal Event(s) /// /// Returns `Ok(None)` for events that should be ignored (e.g., heartbeats) /// Returns `Ok(Some(vec))` for events that produce one or more internal Events pub fn parse_event(event_type: &str, data: &str) -> Result>, ClientError> { // Skip empty data if data.is_empty() || data == "[DONE]" { return Ok(None); } let events = match event_type { // Response lifecycle "response.created" => { let _event: ResponseCreatedEvent = parse_json(data)?; Some(vec![Event::Status(StatusEvent { status: ResponseStatus::Started, })]) } "response.in_progress" => { // Just a status update, no action needed None } "response.completed" | "response.done" => { let event: ResponseDoneEvent = parse_json(data)?; let mut events = Vec::new(); // Emit usage if present if let Some(usage) = event.response.usage { events.push(Event::Usage(UsageEvent { input_tokens: usage.input_tokens, output_tokens: usage.output_tokens, total_tokens: usage.total_tokens, cache_read_input_tokens: None, cache_creation_input_tokens: None, })); } events.push(Event::Status(StatusEvent { status: ResponseStatus::Completed, })); Some(events) } "response.failed" => { // Try to parse error if let Ok(error_event) = parse_json::(data) { Some(vec![ Event::Error(ErrorEvent { code: error_event.error.code, message: error_event.error.message, }), Event::Status(StatusEvent { status: ResponseStatus::Failed, }), ]) } else { Some(vec![Event::Status(StatusEvent { status: ResponseStatus::Failed, })]) } } // Output item events "response.output_item.added" => { let event: OutputItemAddedEvent = parse_json(data)?; Some(vec![convert_item_added(&event)]) } "response.output_item.done" => { let event: OutputItemDoneEvent = parse_json(data)?; Some(vec![convert_item_done(&event)]) } // Text content events "response.output_text.delta" => { let event: TextDeltaEvent = parse_json(data)?; Some(vec![Event::text_delta(event.output_index, &event.delta)]) } "response.output_text.done" => { // Text done - we'll handle stop in output_item.done let _event: TextDoneEvent = parse_json(data)?; None } // Content part events "response.content_part.added" => { // Content part added - we handle this via output_item.added None } "response.content_part.done" => { // Content part done - we handle stop in output_item.done None } // Function call events "response.function_call_arguments.delta" => { let event: FunctionCallArgumentsDeltaEvent = parse_json(data)?; Some(vec![Event::BlockDelta(crate::llm_client::event::BlockDelta { index: event.output_index, delta: DeltaContent::InputJson(event.delta), })]) } "response.function_call_arguments.done" => { // Arguments done - we handle stop in output_item.done let _event: FunctionCallArgumentsDoneEvent = parse_json(data)?; None } // Reasoning events "response.reasoning.delta" | "response.reasoning_summary_text.delta" => { let event: ReasoningDeltaEvent = parse_json(data)?; Some(vec![Event::BlockDelta(crate::llm_client::event::BlockDelta { index: event.output_index, delta: DeltaContent::Thinking(event.delta), })]) } "response.reasoning.done" | "response.reasoning_summary_text.done" => { // Reasoning done - we handle stop in output_item.done let _event: ReasoningDoneEvent = parse_json(data)?; None } // Error event "error" => { let event: ApiErrorEvent = parse_json(data)?; Some(vec![Event::Error(ErrorEvent { code: event.error.code, message: event.error.message, })]) } // Unknown event type - ignore _ => { tracing::debug!(event_type = event_type, "Unknown Open Responses event type"); None } }; Ok(events) } fn parse_json(data: &str) -> Result { serde_json::from_str(data).map_err(|e| ClientError::Parse(e.to_string())) } fn convert_item_added(event: &OutputItemAddedEvent) -> Event { match &event.item { OutputItem::Message { id, role: _, content: _ } => Event::BlockStart(BlockStart { index: event.output_index, block_type: crate::llm_client::event::BlockType::Text, metadata: BlockMetadata::Text, }), OutputItem::FunctionCall { id, call_id, name, arguments: _, } => Event::BlockStart(BlockStart { index: event.output_index, block_type: crate::llm_client::event::BlockType::ToolUse, metadata: BlockMetadata::ToolUse { id: call_id.clone(), name: name.clone(), }, }), OutputItem::Reasoning { id, text: _ } => Event::BlockStart(BlockStart { index: event.output_index, block_type: crate::llm_client::event::BlockType::Thinking, metadata: BlockMetadata::Thinking, }), } } fn convert_item_done(event: &OutputItemDoneEvent) -> Event { let stop_reason = match &event.item { OutputItem::FunctionCall { .. } => Some(StopReason::ToolUse), _ => Some(StopReason::EndTurn), }; Event::BlockStop(BlockStop { index: event.output_index, stop_reason, }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_response_created() { let data = r#"{"response":{"id":"resp_123","status":"in_progress","output":[]}}"#; let events = parse_event("response.created", data).unwrap().unwrap(); assert_eq!(events.len(), 1); assert!(matches!( events[0], Event::Status(StatusEvent { status: ResponseStatus::Started }) )); } #[test] fn test_parse_text_delta() { let data = r#"{"output_index":0,"content_index":0,"delta":"Hello"}"#; let events = parse_event("response.output_text.delta", data) .unwrap() .unwrap(); assert_eq!(events.len(), 1); if let Event::BlockDelta(delta) = &events[0] { assert_eq!(delta.index, 0); assert!(matches!(&delta.delta, DeltaContent::Text(t) if t == "Hello")); } else { panic!("Expected BlockDelta"); } } #[test] fn test_parse_output_item_added_message() { let data = r#"{"output_index":0,"item":{"type":"message","id":"msg_123","role":"assistant","content":[]}}"#; let events = parse_event("response.output_item.added", data) .unwrap() .unwrap(); assert_eq!(events.len(), 1); if let Event::BlockStart(start) = &events[0] { assert_eq!(start.index, 0); assert!(matches!( start.block_type, crate::llm_client::event::BlockType::Text )); } else { panic!("Expected BlockStart"); } } #[test] fn test_parse_output_item_added_function_call() { let data = r#"{"output_index":1,"item":{"type":"function_call","id":"fc_123","call_id":"call_456","name":"get_weather","arguments":""}}"#; let events = parse_event("response.output_item.added", data) .unwrap() .unwrap(); assert_eq!(events.len(), 1); if let Event::BlockStart(start) = &events[0] { assert_eq!(start.index, 1); assert!(matches!( start.block_type, crate::llm_client::event::BlockType::ToolUse )); if let BlockMetadata::ToolUse { id, name } = &start.metadata { assert_eq!(id, "call_456"); assert_eq!(name, "get_weather"); } else { panic!("Expected ToolUse metadata"); } } else { panic!("Expected BlockStart"); } } #[test] fn test_parse_function_call_arguments_delta() { let data = r#"{"output_index":1,"call_id":"call_456","delta":"{\"city\":"}"#; let events = parse_event("response.function_call_arguments.delta", data) .unwrap() .unwrap(); assert_eq!(events.len(), 1); if let Event::BlockDelta(delta) = &events[0] { assert_eq!(delta.index, 1); assert!(matches!( &delta.delta, DeltaContent::InputJson(s) if s == "{\"city\":" )); } else { panic!("Expected BlockDelta"); } } #[test] fn test_parse_response_completed() { let data = r#"{"response":{"id":"resp_123","status":"completed","output":[],"usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30}}}"#; let events = parse_event("response.completed", data).unwrap().unwrap(); assert_eq!(events.len(), 2); // First event should be usage if let Event::Usage(usage) = &events[0] { assert_eq!(usage.input_tokens, Some(10)); assert_eq!(usage.output_tokens, Some(20)); assert_eq!(usage.total_tokens, Some(30)); } else { panic!("Expected Usage event"); } // Second event should be status assert!(matches!( events[1], Event::Status(StatusEvent { status: ResponseStatus::Completed }) )); } #[test] fn test_parse_error() { let data = r#"{"error":{"code":"rate_limit","message":"Too many requests"}}"#; let events = parse_event("error", data).unwrap().unwrap(); assert_eq!(events.len(), 1); if let Event::Error(err) = &events[0] { assert_eq!(err.code, Some("rate_limit".to_string())); assert_eq!(err.message, "Too many requests"); } else { panic!("Expected Error event"); } } #[test] fn test_parse_unknown_event() { let data = r#"{}"#; let events = parse_event("some.unknown.event", data).unwrap(); assert!(events.is_none()); } }