Skip to main content

openai_interface/responses/create/
response.rs

1//! Streaming event types for the Responses API.
2//!
3//! When `stream: true`, the server returns a sequence of semantic SSE
4//! events instead of a single response object. Every event carries an
5//! incrementing `sequence_number`; the stream ends with
6//! `response.completed`, `response.incomplete`, or `response.failed`
7//! (there is **no** `data: [DONE]` sentinel).
8//!
9//! The event set follows the OpenAI Responses streaming events together
10//! with the subsets documented by
11//! [DeepSeek](https://api-docs.deepseek.com/guides/responses_api#streaming)
12//! and
13//! [Qwen](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-responses).
14//! Events not modeled explicitly deserialize to
15//! [`ResponseStreamEvent::Other`].
16
17use serde::Deserialize;
18
19use crate::responses::{Response, ResponseOutputItem};
20
21/// One event of a streamed Responses API call.
22///
23/// Use [`Self::final_response`] to extract the finished [`Response`] from
24/// the terminal `response.completed` / `response.incomplete` /
25/// `response.failed` events.
26#[derive(Debug, Deserialize, Clone)]
27#[serde(tag = "type")]
28pub enum ResponseStreamEvent {
29    /// First event: the response was created, status `in_progress`.
30    #[serde(rename = "response.created")]
31    ResponseCreated {
32        #[serde(default)]
33        sequence_number: Option<u64>,
34        response: Response,
35    },
36
37    /// The response is being generated.
38    #[serde(rename = "response.in_progress")]
39    ResponseInProgress {
40        #[serde(default)]
41        sequence_number: Option<u64>,
42        response: Response,
43    },
44
45    /// The response completed normally; final event with the full
46    /// response object including `usage`.
47    #[serde(rename = "response.completed")]
48    ResponseCompleted {
49        #[serde(default)]
50        sequence_number: Option<u64>,
51        response: Response,
52    },
53
54    /// The response was truncated (e.g. `max_output_tokens` reached);
55    /// final event with the full response object.
56    #[serde(rename = "response.incomplete")]
57    ResponseIncomplete {
58        #[serde(default)]
59        sequence_number: Option<u64>,
60        response: Response,
61    },
62
63    /// The response failed; final event with the full response object
64    /// including the `error` details.
65    #[serde(rename = "response.failed")]
66    ResponseFailed {
67        #[serde(default)]
68        sequence_number: Option<u64>,
69        response: Response,
70    },
71
72    /// An error event emitted mid-stream.
73    #[serde(rename = "error")]
74    Error {
75        #[serde(default)]
76        sequence_number: Option<u64>,
77        /// A human-readable description of the error.
78        #[serde(default)]
79        message: Option<String>,
80        /// The machine-readable error code.
81        #[serde(default)]
82        code: Option<String>,
83        /// The request parameter that caused the error, if any.
84        #[serde(default)]
85        param: Option<String>,
86    },
87
88    /// An output item (reasoning, message, tool call, ...) started.
89    #[serde(rename = "response.output_item.added")]
90    OutputItemAdded {
91        #[serde(default)]
92        sequence_number: Option<u64>,
93        /// The index of the item in the `output` array.
94        #[serde(default)]
95        output_index: Option<u32>,
96        /// The item that was added.
97        item: ResponseOutputItem,
98    },
99
100    /// An output item completed.
101    #[serde(rename = "response.output_item.done")]
102    OutputItemDone {
103        #[serde(default)]
104        sequence_number: Option<u64>,
105        #[serde(default)]
106        output_index: Option<u32>,
107        /// The completed item.
108        item: ResponseOutputItem,
109    },
110
111    /// A content part started within an output item.
112    #[serde(rename = "response.content_part.added")]
113    ContentPartAdded {
114        #[serde(default)]
115        sequence_number: Option<u64>,
116        /// The ID of the item the content part belongs to.
117        #[serde(default)]
118        item_id: Option<String>,
119        #[serde(default)]
120        output_index: Option<u32>,
121        /// The index of the content part within the item.
122        #[serde(default)]
123        content_index: Option<u32>,
124        /// The content part that was added.
125        part: EventContentPart,
126    },
127
128    /// A content part completed.
129    #[serde(rename = "response.content_part.done")]
130    ContentPartDone {
131        #[serde(default)]
132        sequence_number: Option<u64>,
133        #[serde(default)]
134        item_id: Option<String>,
135        #[serde(default)]
136        output_index: Option<u32>,
137        #[serde(default)]
138        content_index: Option<u32>,
139        /// The completed content part.
140        part: EventContentPart,
141    },
142
143    /// An incremental text delta of an output message.
144    #[serde(rename = "response.output_text.delta")]
145    OutputTextDelta {
146        #[serde(default)]
147        sequence_number: Option<u64>,
148        #[serde(default)]
149        item_id: Option<String>,
150        #[serde(default)]
151        output_index: Option<u32>,
152        #[serde(default)]
153        content_index: Option<u32>,
154        /// The incremental text.
155        delta: String,
156    },
157
158    /// The full output text of a message content part.
159    #[serde(rename = "response.output_text.done")]
160    OutputTextDone {
161        #[serde(default)]
162        sequence_number: Option<u64>,
163        #[serde(default)]
164        item_id: Option<String>,
165        #[serde(default)]
166        output_index: Option<u32>,
167        #[serde(default)]
168        content_index: Option<u32>,
169        /// The complete text.
170        text: String,
171    },
172
173    /// An incremental reasoning (chain-of-thought) text delta.
174    #[serde(rename = "response.reasoning_text.delta")]
175    ReasoningTextDelta {
176        #[serde(default)]
177        sequence_number: Option<u64>,
178        #[serde(default)]
179        item_id: Option<String>,
180        #[serde(default)]
181        output_index: Option<u32>,
182        #[serde(default)]
183        content_index: Option<u32>,
184        /// The incremental reasoning text.
185        delta: String,
186    },
187
188    /// The full reasoning (chain-of-thought) text of a reasoning item.
189    #[serde(rename = "response.reasoning_text.done")]
190    ReasoningTextDone {
191        #[serde(default)]
192        sequence_number: Option<u64>,
193        #[serde(default)]
194        item_id: Option<String>,
195        #[serde(default)]
196        output_index: Option<u32>,
197        #[serde(default)]
198        content_index: Option<u32>,
199        /// The complete reasoning text.
200        text: String,
201    },
202
203    /// An incremental delta of function call arguments.
204    #[serde(rename = "response.function_call_arguments.delta")]
205    FunctionCallArgumentsDelta {
206        #[serde(default)]
207        sequence_number: Option<u64>,
208        #[serde(default)]
209        item_id: Option<String>,
210        #[serde(default)]
211        output_index: Option<u32>,
212        /// The incremental arguments JSON fragment.
213        delta: String,
214    },
215
216    /// The full arguments of a function call.
217    #[serde(rename = "response.function_call_arguments.done")]
218    FunctionCallArgumentsDone {
219        #[serde(default)]
220        sequence_number: Option<u64>,
221        #[serde(default)]
222        item_id: Option<String>,
223        #[serde(default)]
224        output_index: Option<u32>,
225        /// The complete arguments JSON string.
226        arguments: String,
227    },
228
229    /// An incremental delta of custom tool call input (e.g. the
230    /// `apply_patch` tool).
231    #[serde(rename = "response.custom_tool_call_input.delta")]
232    CustomToolCallInputDelta {
233        #[serde(default)]
234        sequence_number: Option<u64>,
235        #[serde(default)]
236        item_id: Option<String>,
237        #[serde(default)]
238        output_index: Option<u32>,
239        /// The incremental input fragment.
240        delta: String,
241    },
242
243    /// The full input of a custom tool call.
244    #[serde(rename = "response.custom_tool_call_input.done")]
245    CustomToolCallInputDone {
246        #[serde(default)]
247        sequence_number: Option<u64>,
248        #[serde(default)]
249        item_id: Option<String>,
250        #[serde(default)]
251        output_index: Option<u32>,
252        /// The complete input.
253        input: String,
254    },
255
256    /// A server-side web search started.
257    #[serde(rename = "response.web_search_call.in_progress")]
258    WebSearchCallInProgress {
259        #[serde(default)]
260        sequence_number: Option<u64>,
261        #[serde(default)]
262        item_id: Option<String>,
263        #[serde(default)]
264        output_index: Option<u32>,
265    },
266
267    /// A server-side web search is searching.
268    #[serde(rename = "response.web_search_call.searching")]
269    WebSearchCallSearching {
270        #[serde(default)]
271        sequence_number: Option<u64>,
272        #[serde(default)]
273        item_id: Option<String>,
274        #[serde(default)]
275        output_index: Option<u32>,
276    },
277
278    /// A server-side web search completed.
279    #[serde(rename = "response.web_search_call.completed")]
280    WebSearchCallCompleted {
281        #[serde(default)]
282        sequence_number: Option<u64>,
283        #[serde(default)]
284        item_id: Option<String>,
285        #[serde(default)]
286        output_index: Option<u32>,
287    },
288
289    /// Qwen: an incremental delta of MCP tool call arguments.
290    #[cfg(feature = "qwen")]
291    #[serde(rename = "response.mcp_call_arguments.delta")]
292    McpCallArgumentsDelta {
293        #[serde(default)]
294        sequence_number: Option<u64>,
295        #[serde(default)]
296        item_id: Option<String>,
297        #[serde(default)]
298        output_index: Option<u32>,
299        /// The incremental arguments JSON fragment.
300        delta: String,
301    },
302
303    /// Qwen: the full arguments of an MCP tool call.
304    #[cfg(feature = "qwen")]
305    #[serde(rename = "response.mcp_call_arguments.done")]
306    McpCallArgumentsDone {
307        #[serde(default)]
308        sequence_number: Option<u64>,
309        #[serde(default)]
310        item_id: Option<String>,
311        #[serde(default)]
312        output_index: Option<u32>,
313        /// The complete arguments JSON string.
314        arguments: String,
315    },
316
317    /// Qwen: an MCP tool call completed.
318    #[cfg(feature = "qwen")]
319    #[serde(rename = "response.mcp_call.completed")]
320    McpCallCompleted {
321        #[serde(default)]
322        sequence_number: Option<u64>,
323        #[serde(default)]
324        item_id: Option<String>,
325        #[serde(default)]
326        output_index: Option<u32>,
327    },
328
329    /// Qwen: a knowledge-base search started.
330    #[cfg(feature = "qwen")]
331    #[serde(rename = "response.file_search_call.in_progress")]
332    FileSearchCallInProgress {
333        #[serde(default)]
334        sequence_number: Option<u64>,
335        #[serde(default)]
336        item_id: Option<String>,
337        #[serde(default)]
338        output_index: Option<u32>,
339    },
340
341    /// Qwen: a knowledge-base search is searching.
342    #[cfg(feature = "qwen")]
343    #[serde(rename = "response.file_search_call.searching")]
344    FileSearchCallSearching {
345        #[serde(default)]
346        sequence_number: Option<u64>,
347        #[serde(default)]
348        item_id: Option<String>,
349        #[serde(default)]
350        output_index: Option<u32>,
351    },
352
353    /// Qwen: a knowledge-base search completed.
354    #[cfg(feature = "qwen")]
355    #[serde(rename = "response.file_search_call.completed")]
356    FileSearchCallCompleted {
357        #[serde(default)]
358        sequence_number: Option<u64>,
359        #[serde(default)]
360        item_id: Option<String>,
361        #[serde(default)]
362        output_index: Option<u32>,
363    },
364
365    /// Any event type not covered by the variants above.
366    #[serde(other)]
367    Other,
368}
369
370impl ResponseStreamEvent {
371    /// Returns the final response object if this is one of the terminal
372    /// events (`response.completed`, `response.incomplete`,
373    /// `response.failed`).
374    pub fn final_response(&self) -> Option<&Response> {
375        match self {
376            Self::ResponseCompleted { response, .. }
377            | Self::ResponseIncomplete { response, .. }
378            | Self::ResponseFailed { response, .. } => Some(response),
379            _ => None,
380        }
381    }
382}
383
384/// A content part carried by `response.content_part.added` /
385/// `response.content_part.done` events.
386#[derive(Debug, Deserialize, Clone)]
387#[serde(tag = "type", rename_all = "snake_case")]
388pub enum EventContentPart {
389    /// A text output part.
390    OutputText {
391        /// The text content.
392        #[serde(default)]
393        text: Option<String>,
394    },
395    /// A reasoning text part.
396    ReasoningText {
397        /// The reasoning text.
398        #[serde(default)]
399        text: Option<String>,
400    },
401    /// A refusal part.
402    Refusal {
403        /// The refusal message.
404        #[serde(default)]
405        refusal: Option<String>,
406    },
407    /// Any content part type not covered above.
408    #[serde(other)]
409    Other,
410}
411
412crate::impl_from_str!(ResponseStreamEvent);
413
414#[cfg(test)]
415mod tests {
416    //! Offline deserialization tests using event payloads copied from the
417    //! DeepSeek streaming documentation example.
418
419    use std::str::FromStr;
420
421    use super::*;
422    use crate::responses::{ResponseObject, ResponseOutputItem};
423
424    /// From the streaming example at
425    /// <https://api-docs.deepseek.com/api/create-response>.
426    #[test]
427    fn parses_documented_stream_events() {
428        let created = ResponseStreamEvent::from_str(
429            r#"{"type": "response.created", "sequence_number": 0, "response": {"id": "resp_1", "object": "response", "created_at": 0, "status": "in_progress", "model": "deepseek-v4-flash", "output": []}}"#,
430        ).unwrap();
431        assert!(matches!(
432            created,
433            ResponseStreamEvent::ResponseCreated { .. }
434        ));
435
436        let delta = ResponseStreamEvent::from_str(
437            r#"{"type": "response.reasoning_text.delta", "sequence_number": 4, "item_id": "rs_1", "output_index": 0, "content_index": 0, "delta": "The user"}"#,
438        ).unwrap();
439        match delta {
440            ResponseStreamEvent::ReasoningTextDelta { delta, item_id, .. } => {
441                assert_eq!(delta, "The user");
442                assert_eq!(item_id.as_deref(), Some("rs_1"));
443            }
444            other => panic!("unexpected event: {other:?}"),
445        }
446
447        let text_delta = ResponseStreamEvent::from_str(
448            r#"{"type": "response.output_text.delta", "sequence_number": 11, "item_id": "msg_1", "output_index": 1, "content_index": 0, "delta": "Hello"}"#,
449        ).unwrap();
450        assert!(matches!(
451            text_delta,
452            ResponseStreamEvent::OutputTextDelta { .. }
453        ));
454    }
455
456    #[test]
457    fn parses_completed_event_and_extracts_response() {
458        let event = ResponseStreamEvent::from_str(
459            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}}}"#,
460        ).unwrap();
461
462        let response = event.final_response().expect("terminal event");
463        assert_eq!(response.object, ResponseObject::Response);
464        assert!(response.is_completed());
465        assert_eq!(response.output_text(), "Hi!");
466        assert!(matches!(
467            response.output.first(),
468            Some(ResponseOutputItem::Message(_))
469        ));
470    }
471
472    #[test]
473    fn unknown_event_type_is_preserved() {
474        let event = ResponseStreamEvent::from_str(
475            r#"{"type": "response.some_future_event", "sequence_number": 3}"#,
476        )
477        .unwrap();
478        assert!(matches!(event, ResponseStreamEvent::Other));
479    }
480}