use serde::Deserialize;
use crate::responses::{Response, ResponseOutputItem};
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum ResponseStreamEvent {
#[serde(rename = "response.created")]
ResponseCreated {
#[serde(default)]
sequence_number: Option<u64>,
response: Response,
},
#[serde(rename = "response.in_progress")]
ResponseInProgress {
#[serde(default)]
sequence_number: Option<u64>,
response: Response,
},
#[serde(rename = "response.completed")]
ResponseCompleted {
#[serde(default)]
sequence_number: Option<u64>,
response: Response,
},
#[serde(rename = "response.incomplete")]
ResponseIncomplete {
#[serde(default)]
sequence_number: Option<u64>,
response: Response,
},
#[serde(rename = "response.failed")]
ResponseFailed {
#[serde(default)]
sequence_number: Option<u64>,
response: Response,
},
#[serde(rename = "error")]
Error {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
message: Option<String>,
#[serde(default)]
code: Option<String>,
#[serde(default)]
param: Option<String>,
},
#[serde(rename = "response.output_item.added")]
OutputItemAdded {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
output_index: Option<u32>,
item: ResponseOutputItem,
},
#[serde(rename = "response.output_item.done")]
OutputItemDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
output_index: Option<u32>,
item: ResponseOutputItem,
},
#[serde(rename = "response.content_part.added")]
ContentPartAdded {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
#[serde(default)]
content_index: Option<u32>,
part: EventContentPart,
},
#[serde(rename = "response.content_part.done")]
ContentPartDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
#[serde(default)]
content_index: Option<u32>,
part: EventContentPart,
},
#[serde(rename = "response.output_text.delta")]
OutputTextDelta {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
#[serde(default)]
content_index: Option<u32>,
delta: String,
},
#[serde(rename = "response.output_text.done")]
OutputTextDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
#[serde(default)]
content_index: Option<u32>,
text: String,
},
#[serde(rename = "response.reasoning_text.delta")]
ReasoningTextDelta {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
#[serde(default)]
content_index: Option<u32>,
delta: String,
},
#[serde(rename = "response.reasoning_text.done")]
ReasoningTextDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
#[serde(default)]
content_index: Option<u32>,
text: String,
},
#[serde(rename = "response.function_call_arguments.delta")]
FunctionCallArgumentsDelta {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
delta: String,
},
#[serde(rename = "response.function_call_arguments.done")]
FunctionCallArgumentsDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
arguments: String,
},
#[serde(rename = "response.custom_tool_call_input.delta")]
CustomToolCallInputDelta {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
delta: String,
},
#[serde(rename = "response.custom_tool_call_input.done")]
CustomToolCallInputDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
input: String,
},
#[serde(rename = "response.web_search_call.in_progress")]
WebSearchCallInProgress {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[serde(rename = "response.web_search_call.searching")]
WebSearchCallSearching {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[serde(rename = "response.web_search_call.completed")]
WebSearchCallCompleted {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[cfg(feature = "qwen")]
#[serde(rename = "response.mcp_call_arguments.delta")]
McpCallArgumentsDelta {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
delta: String,
},
#[cfg(feature = "qwen")]
#[serde(rename = "response.mcp_call_arguments.done")]
McpCallArgumentsDone {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
arguments: String,
},
#[cfg(feature = "qwen")]
#[serde(rename = "response.mcp_call.completed")]
McpCallCompleted {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[cfg(feature = "qwen")]
#[serde(rename = "response.file_search_call.in_progress")]
FileSearchCallInProgress {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[cfg(feature = "qwen")]
#[serde(rename = "response.file_search_call.searching")]
FileSearchCallSearching {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[cfg(feature = "qwen")]
#[serde(rename = "response.file_search_call.completed")]
FileSearchCallCompleted {
#[serde(default)]
sequence_number: Option<u64>,
#[serde(default)]
item_id: Option<String>,
#[serde(default)]
output_index: Option<u32>,
},
#[serde(other)]
Other,
}
impl ResponseStreamEvent {
pub fn final_response(&self) -> Option<&Response> {
match self {
Self::ResponseCompleted { response, .. }
| Self::ResponseIncomplete { response, .. }
| Self::ResponseFailed { response, .. } => Some(response),
_ => None,
}
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventContentPart {
OutputText {
#[serde(default)]
text: Option<String>,
},
ReasoningText {
#[serde(default)]
text: Option<String>,
},
Refusal {
#[serde(default)]
refusal: Option<String>,
},
#[serde(other)]
Other,
}
crate::impl_from_str!(ResponseStreamEvent);
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::responses::{ResponseObject, ResponseOutputItem};
#[test]
fn parses_documented_stream_events() {
let created = ResponseStreamEvent::from_str(
r#"{"type": "response.created", "sequence_number": 0, "response": {"id": "resp_1", "object": "response", "created_at": 0, "status": "in_progress", "model": "deepseek-v4-flash", "output": []}}"#,
).unwrap();
assert!(matches!(
created,
ResponseStreamEvent::ResponseCreated { .. }
));
let delta = ResponseStreamEvent::from_str(
r#"{"type": "response.reasoning_text.delta", "sequence_number": 4, "item_id": "rs_1", "output_index": 0, "content_index": 0, "delta": "The user"}"#,
).unwrap();
match delta {
ResponseStreamEvent::ReasoningTextDelta { delta, item_id, .. } => {
assert_eq!(delta, "The user");
assert_eq!(item_id.as_deref(), Some("rs_1"));
}
other => panic!("unexpected event: {other:?}"),
}
let text_delta = ResponseStreamEvent::from_str(
r#"{"type": "response.output_text.delta", "sequence_number": 11, "item_id": "msg_1", "output_index": 1, "content_index": 0, "delta": "Hello"}"#,
).unwrap();
assert!(matches!(
text_delta,
ResponseStreamEvent::OutputTextDelta { .. }
));
}
#[test]
fn parses_completed_event_and_extracts_response() {
let event = ResponseStreamEvent::from_str(
r#"{"type": "response.completed", "sequence_number": 20, "response": {"id": "resp_1", "object": "response", "created_at": 0, "status": "completed", "model": "deepseek-v4-flash", "output": [{"type": "message", "id": "msg_1", "status": "completed", "role": "assistant", "content": [{"type": "output_text", "text": "Hi!", "annotations": []}]}], "usage": {"input_tokens": 1, "output_tokens": 2, "total_tokens": 3}}}"#,
).unwrap();
let response = event.final_response().expect("terminal event");
assert_eq!(response.object, ResponseObject::Response);
assert!(response.is_completed());
assert_eq!(response.output_text(), "Hi!");
assert!(matches!(
response.output.first(),
Some(ResponseOutputItem::Message(_))
));
}
#[test]
fn unknown_event_type_is_preserved() {
let event = ResponseStreamEvent::from_str(
r#"{"type": "response.some_future_event", "sequence_number": 3}"#,
)
.unwrap();
assert!(matches!(event, ResponseStreamEvent::Other));
}
}