Skip to main content

openai_compat/types/responses/
events.rs

1//! Streaming events for `POST /responses` with `stream: true`, mirroring
2//! `openai-python/src/openai/types/responses/response_stream_event.py`.
3//!
4//! Unlike chat completions streaming (homogeneous delta chunks), the
5//! Responses API streams ~50 distinct, differently-shaped event types. This
6//! crate types the ~12 events needed to drive a typical create/consume loop;
7//! everything else deserializes into [`ResponseStreamEvent::Unknown`] rather
8//! than failing the stream, so newly-added upstream event types are forward
9//! compatible.
10//!
11//! `response.failed`/`response.incomplete` are typed `Ok` variants, not a
12//! stream [`crate::OpenAIError`] — the error detail is nested under
13//! `response.error`, so callers must inspect the variant themselves.
14
15use serde::Deserialize;
16
17use super::response::{OutputItem, Response};
18
19/// One event in a Responses API SSE stream.
20#[derive(Debug, Clone, Deserialize)]
21#[serde(tag = "type", rename_all = "snake_case")]
22#[non_exhaustive]
23pub enum ResponseStreamEvent {
24    #[serde(rename = "response.created")]
25    Created {
26        response: Response,
27        sequence_number: u64,
28    },
29    #[serde(rename = "response.in_progress")]
30    InProgress {
31        response: Response,
32        sequence_number: u64,
33    },
34    #[serde(rename = "response.queued")]
35    Queued {
36        response: Response,
37        sequence_number: u64,
38    },
39    /// Terminal: the response finished successfully.
40    #[serde(rename = "response.completed")]
41    Completed {
42        response: Response,
43        sequence_number: u64,
44    },
45    /// Terminal: the response failed. This is `Ok(Failed{..})`, not a stream
46    /// error — inspect `response.error` for details.
47    #[serde(rename = "response.failed")]
48    Failed {
49        response: Response,
50        sequence_number: u64,
51    },
52    /// Terminal: the response stopped early (e.g. hit `max_output_tokens`).
53    #[serde(rename = "response.incomplete")]
54    Incomplete {
55        response: Response,
56        sequence_number: u64,
57    },
58    #[serde(rename = "response.output_item.added")]
59    OutputItemAdded {
60        output_index: u32,
61        item: OutputItem,
62        sequence_number: u64,
63    },
64    #[serde(rename = "response.output_item.done")]
65    OutputItemDone {
66        output_index: u32,
67        item: OutputItem,
68        sequence_number: u64,
69    },
70    #[serde(rename = "response.content_part.added")]
71    ContentPartAdded {
72        item_id: String,
73        output_index: u32,
74        content_index: u32,
75        part: serde_json::Value,
76        sequence_number: u64,
77    },
78    #[serde(rename = "response.content_part.done")]
79    ContentPartDone {
80        item_id: String,
81        output_index: u32,
82        content_index: u32,
83        part: serde_json::Value,
84        sequence_number: u64,
85    },
86    #[serde(rename = "response.output_text.delta")]
87    OutputTextDelta {
88        item_id: String,
89        output_index: u32,
90        content_index: u32,
91        delta: String,
92        sequence_number: u64,
93    },
94    #[serde(rename = "response.output_text.done")]
95    OutputTextDone {
96        item_id: String,
97        output_index: u32,
98        content_index: u32,
99        text: String,
100        sequence_number: u64,
101    },
102    #[serde(rename = "response.function_call_arguments.delta")]
103    FunctionCallArgumentsDelta {
104        item_id: String,
105        output_index: u32,
106        delta: String,
107        sequence_number: u64,
108    },
109    #[serde(rename = "response.function_call_arguments.done")]
110    FunctionCallArgumentsDone {
111        item_id: String,
112        output_index: u32,
113        arguments: String,
114        sequence_number: u64,
115    },
116    #[serde(rename = "error")]
117    Error {
118        #[serde(default)]
119        code: Option<String>,
120        message: String,
121        #[serde(default)]
122        param: Option<String>,
123        sequence_number: u64,
124    },
125    /// Any event type not modeled above (audio, reasoning-summary,
126    /// code-interpreter, mcp, image-gen, refusal, annotation, ...).
127    #[serde(other)]
128    Unknown,
129}
130
131impl ResponseStreamEvent {
132    /// This event's position in the stream, for resuming a background
133    /// response via `retrieve_stream(id, starting_after)`. `None` only for
134    /// [`ResponseStreamEvent::Unknown`] (payload not captured).
135    pub fn sequence_number(&self) -> Option<u64> {
136        match self {
137            Self::Created { sequence_number, .. }
138            | Self::InProgress { sequence_number, .. }
139            | Self::Queued { sequence_number, .. }
140            | Self::Completed { sequence_number, .. }
141            | Self::Failed { sequence_number, .. }
142            | Self::Incomplete { sequence_number, .. }
143            | Self::OutputItemAdded { sequence_number, .. }
144            | Self::OutputItemDone { sequence_number, .. }
145            | Self::ContentPartAdded { sequence_number, .. }
146            | Self::ContentPartDone { sequence_number, .. }
147            | Self::OutputTextDelta { sequence_number, .. }
148            | Self::OutputTextDone { sequence_number, .. }
149            | Self::FunctionCallArgumentsDelta { sequence_number, .. }
150            | Self::FunctionCallArgumentsDone { sequence_number, .. }
151            | Self::Error { sequence_number, .. } => Some(*sequence_number),
152            Self::Unknown => None,
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn response_json() -> serde_json::Value {
162        serde_json::json!({
163            "id": "resp_1", "object": "response", "created_at": 1.0,
164            "model": "gpt-5", "status": "completed", "output": []
165        })
166    }
167
168    #[test]
169    fn stream_event_created() {
170        let json = serde_json::json!({
171            "type": "response.created", "response": response_json(), "sequence_number": 1
172        });
173        let event: ResponseStreamEvent = serde_json::from_value(json).unwrap();
174        assert!(matches!(event, ResponseStreamEvent::Created { .. }));
175        assert_eq!(event.sequence_number(), Some(1));
176    }
177
178    #[test]
179    fn stream_event_output_text_delta() {
180        let json = serde_json::json!({
181            "type": "response.output_text.delta",
182            "item_id": "msg_1", "output_index": 0, "content_index": 0, "delta": "Hel",
183            "sequence_number": 3
184        });
185        let event: ResponseStreamEvent = serde_json::from_value(json).unwrap();
186        assert_eq!(event.sequence_number(), Some(3));
187        match event {
188            ResponseStreamEvent::OutputTextDelta { delta, .. } => assert_eq!(delta, "Hel"),
189            _ => panic!("expected OutputTextDelta"),
190        }
191    }
192
193    #[test]
194    fn unknown_stream_event_maps_to_unknown() {
195        let json = serde_json::json!({
196            "type": "response.reasoning_summary_text.delta",
197            "item_id": "r_1", "delta": "thinking"
198        });
199        let event: ResponseStreamEvent = serde_json::from_value(json).unwrap();
200        assert!(matches!(event, ResponseStreamEvent::Unknown));
201        assert_eq!(event.sequence_number(), None);
202    }
203
204    #[test]
205    fn failed_event_is_typed_not_error() {
206        let mut response = response_json();
207        response["status"] = serde_json::json!("failed");
208        response["error"] = serde_json::json!({"code": "server_error", "message": "boom"});
209        let json = serde_json::json!({
210            "type": "response.failed", "response": response, "sequence_number": 5
211        });
212        let event: ResponseStreamEvent = serde_json::from_value(json).unwrap();
213        match event {
214            ResponseStreamEvent::Failed { response, .. } => {
215                assert_eq!(response.error.unwrap().code, "server_error");
216            }
217            _ => panic!("expected a typed Ok(Failed), not a stream error"),
218        }
219    }
220
221    #[test]
222    fn error_event_uses_bare_type_tag() {
223        let json = serde_json::json!({
224            "type": "error", "message": "boom", "code": "server_error", "sequence_number": 2
225        });
226        let event: ResponseStreamEvent = serde_json::from_value(json).unwrap();
227        match event {
228            ResponseStreamEvent::Error { message, code, .. } => {
229                assert_eq!(message, "boom");
230                assert_eq!(code.as_deref(), Some("server_error"));
231            }
232            _ => panic!("expected Error"),
233        }
234    }
235}