Skip to main content

gproxy_protocol/openai/create_response/
stream.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::openai::create_response::response::ResponseBody as CreateResponseBody;
5use crate::openai::create_response::types::{
6    ResponseOutputItem, ResponseOutputRefusal, ResponseOutputText, ResponseReasoningTextContent,
7    ResponseSummaryTextContent,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ResponseStreamErrorPayload {
12    #[serde(rename = "type")]
13    pub type_: String,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub code: Option<String>,
16    pub message: String,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub param: Option<String>,
19}
20
21impl ResponseStreamErrorPayload {
22    pub fn code_or_type(&self) -> &str {
23        self.code.as_deref().unwrap_or(self.type_.as_str())
24    }
25}
26
27/// Stream event union documented by Responses API.
28///
29/// Each SSE `data:` line (except `[DONE]`) deserializes to one of these variants,
30/// discriminated by the `type` field in JSON.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(tag = "type")]
33pub enum ResponseStreamEvent {
34    #[serde(rename = "response.audio.delta")]
35    AudioDelta {
36        delta: String,
37        sequence_number: u64,
38        #[serde(default, skip_serializing_if = "Option::is_none")]
39        obfuscation: Option<String>,
40    },
41    #[serde(rename = "response.audio.done")]
42    AudioDone { sequence_number: u64 },
43    #[serde(rename = "response.audio.transcript.delta")]
44    AudioTranscriptDelta {
45        delta: String,
46        sequence_number: u64,
47        #[serde(default, skip_serializing_if = "Option::is_none")]
48        obfuscation: Option<String>,
49    },
50    #[serde(rename = "response.audio.transcript.done")]
51    AudioTranscriptDone { sequence_number: u64 },
52
53    #[serde(rename = "response.code_interpreter_call_code.delta")]
54    CodeInterpreterCallCodeDelta {
55        delta: String,
56        item_id: String,
57        output_index: u64,
58        sequence_number: u64,
59        #[serde(default, skip_serializing_if = "Option::is_none")]
60        obfuscation: Option<String>,
61    },
62    #[serde(rename = "response.code_interpreter_call_code.done")]
63    CodeInterpreterCallCodeDone {
64        code: String,
65        item_id: String,
66        output_index: u64,
67        sequence_number: u64,
68    },
69    #[serde(rename = "response.code_interpreter_call.completed")]
70    CodeInterpreterCallCompleted {
71        item_id: String,
72        output_index: u64,
73        sequence_number: u64,
74    },
75    #[serde(rename = "response.code_interpreter_call.in_progress")]
76    CodeInterpreterCallInProgress {
77        item_id: String,
78        output_index: u64,
79        sequence_number: u64,
80    },
81    #[serde(rename = "response.code_interpreter_call.interpreting")]
82    CodeInterpreterCallInterpreting {
83        item_id: String,
84        output_index: u64,
85        sequence_number: u64,
86    },
87
88    #[serde(rename = "response.created")]
89    Created {
90        response: CreateResponseBody,
91        sequence_number: u64,
92    },
93    #[serde(rename = "response.queued")]
94    Queued {
95        response: CreateResponseBody,
96        sequence_number: u64,
97    },
98    #[serde(rename = "response.in_progress")]
99    InProgress {
100        response: CreateResponseBody,
101        sequence_number: u64,
102    },
103    #[serde(rename = "response.failed")]
104    Failed {
105        response: CreateResponseBody,
106        sequence_number: u64,
107    },
108    #[serde(rename = "response.incomplete")]
109    Incomplete {
110        response: CreateResponseBody,
111        sequence_number: u64,
112    },
113    #[serde(rename = "response.completed")]
114    Completed {
115        response: CreateResponseBody,
116        sequence_number: u64,
117    },
118
119    #[serde(rename = "response.output_item.added")]
120    OutputItemAdded {
121        item: ResponseOutputItem,
122        output_index: u64,
123        sequence_number: u64,
124    },
125    #[serde(rename = "response.output_item.done")]
126    OutputItemDone {
127        item: ResponseOutputItem,
128        output_index: u64,
129        sequence_number: u64,
130    },
131
132    #[serde(rename = "response.content_part.added")]
133    ContentPartAdded {
134        content_index: u64,
135        item_id: String,
136        output_index: u64,
137        part: ResponseStreamContentPart,
138        sequence_number: u64,
139    },
140    #[serde(rename = "response.content_part.done")]
141    ContentPartDone {
142        content_index: u64,
143        item_id: String,
144        output_index: u64,
145        part: ResponseStreamContentPart,
146        sequence_number: u64,
147    },
148
149    #[serde(rename = "response.output_text.annotation.added")]
150    OutputTextAnnotationAdded {
151        annotation: Value,
152        annotation_index: u64,
153        content_index: u64,
154        item_id: String,
155        output_index: u64,
156        sequence_number: u64,
157    },
158
159    #[serde(rename = "response.output_text.delta")]
160    OutputTextDelta {
161        content_index: u64,
162        delta: String,
163        item_id: String,
164        #[serde(default, skip_serializing_if = "Option::is_none")]
165        logprobs: Option<Vec<ResponseStreamTokenLogprob>>,
166        output_index: u64,
167        sequence_number: u64,
168        #[serde(default, skip_serializing_if = "Option::is_none")]
169        obfuscation: Option<String>,
170    },
171    #[serde(rename = "response.output_text.done")]
172    OutputTextDone {
173        content_index: u64,
174        item_id: String,
175        #[serde(default, skip_serializing_if = "Option::is_none")]
176        logprobs: Option<Vec<ResponseStreamTokenLogprob>>,
177        output_index: u64,
178        sequence_number: u64,
179        text: String,
180    },
181
182    #[serde(rename = "response.refusal.delta")]
183    RefusalDelta {
184        content_index: u64,
185        delta: String,
186        item_id: String,
187        output_index: u64,
188        sequence_number: u64,
189        #[serde(default, skip_serializing_if = "Option::is_none")]
190        obfuscation: Option<String>,
191    },
192    #[serde(rename = "response.refusal.done")]
193    RefusalDone {
194        content_index: u64,
195        item_id: String,
196        output_index: u64,
197        refusal: String,
198        sequence_number: u64,
199    },
200
201    #[serde(rename = "response.reasoning_text.delta")]
202    ReasoningTextDelta {
203        content_index: u64,
204        delta: String,
205        item_id: String,
206        output_index: u64,
207        sequence_number: u64,
208        #[serde(default, skip_serializing_if = "Option::is_none")]
209        obfuscation: Option<String>,
210    },
211    #[serde(rename = "response.reasoning_text.done")]
212    ReasoningTextDone {
213        content_index: u64,
214        item_id: String,
215        output_index: u64,
216        sequence_number: u64,
217        text: String,
218    },
219
220    #[serde(rename = "response.reasoning_summary_part.added")]
221    ReasoningSummaryPartAdded {
222        item_id: String,
223        output_index: u64,
224        part: ResponseSummaryTextContent,
225        sequence_number: u64,
226        summary_index: u64,
227    },
228    #[serde(rename = "response.reasoning_summary_part.done")]
229    ReasoningSummaryPartDone {
230        item_id: String,
231        output_index: u64,
232        part: ResponseSummaryTextContent,
233        sequence_number: u64,
234        summary_index: u64,
235    },
236    #[serde(rename = "response.reasoning_summary_text.delta")]
237    ReasoningSummaryTextDelta {
238        delta: String,
239        item_id: String,
240        output_index: u64,
241        sequence_number: u64,
242        summary_index: u64,
243        #[serde(default, skip_serializing_if = "Option::is_none")]
244        obfuscation: Option<String>,
245    },
246    #[serde(rename = "response.reasoning_summary_text.done")]
247    ReasoningSummaryTextDone {
248        item_id: String,
249        output_index: u64,
250        sequence_number: u64,
251        summary_index: u64,
252        text: String,
253    },
254
255    #[serde(rename = "response.function_call_arguments.delta")]
256    FunctionCallArgumentsDelta {
257        delta: String,
258        item_id: String,
259        output_index: u64,
260        sequence_number: u64,
261        #[serde(default, skip_serializing_if = "Option::is_none")]
262        obfuscation: Option<String>,
263    },
264    #[serde(rename = "response.function_call_arguments.done")]
265    FunctionCallArgumentsDone {
266        arguments: String,
267        item_id: String,
268        #[serde(default, skip_serializing_if = "Option::is_none")]
269        name: Option<String>,
270        output_index: u64,
271        sequence_number: u64,
272    },
273
274    #[serde(rename = "response.file_search_call.in_progress")]
275    FileSearchCallInProgress {
276        item_id: String,
277        output_index: u64,
278        sequence_number: u64,
279    },
280    #[serde(rename = "response.file_search_call.searching")]
281    FileSearchCallSearching {
282        item_id: String,
283        output_index: u64,
284        sequence_number: u64,
285    },
286    #[serde(rename = "response.file_search_call.completed")]
287    FileSearchCallCompleted {
288        item_id: String,
289        output_index: u64,
290        sequence_number: u64,
291    },
292
293    #[serde(rename = "response.web_search_call.in_progress")]
294    WebSearchCallInProgress {
295        item_id: String,
296        output_index: u64,
297        sequence_number: u64,
298    },
299    #[serde(rename = "response.web_search_call.searching")]
300    WebSearchCallSearching {
301        item_id: String,
302        output_index: u64,
303        sequence_number: u64,
304    },
305    #[serde(rename = "response.web_search_call.completed")]
306    WebSearchCallCompleted {
307        item_id: String,
308        output_index: u64,
309        sequence_number: u64,
310    },
311
312    #[serde(rename = "response.image_generation_call.in_progress")]
313    ImageGenerationCallInProgress {
314        item_id: String,
315        output_index: u64,
316        sequence_number: u64,
317    },
318    #[serde(rename = "response.image_generation_call.generating")]
319    ImageGenerationCallGenerating {
320        item_id: String,
321        output_index: u64,
322        sequence_number: u64,
323    },
324    #[serde(rename = "response.image_generation_call.partial_image")]
325    ImageGenerationCallPartialImage {
326        item_id: String,
327        output_index: u64,
328        partial_image_b64: String,
329        partial_image_index: u64,
330        sequence_number: u64,
331    },
332    #[serde(rename = "response.image_generation_call.completed")]
333    ImageGenerationCallCompleted {
334        item_id: String,
335        output_index: u64,
336        sequence_number: u64,
337    },
338
339    #[serde(rename = "response.mcp_call_arguments.delta")]
340    McpCallArgumentsDelta {
341        delta: String,
342        item_id: String,
343        output_index: u64,
344        sequence_number: u64,
345        #[serde(default, skip_serializing_if = "Option::is_none")]
346        obfuscation: Option<String>,
347    },
348    #[serde(rename = "response.mcp_call_arguments.done")]
349    McpCallArgumentsDone {
350        arguments: String,
351        item_id: String,
352        output_index: u64,
353        sequence_number: u64,
354    },
355    #[serde(rename = "response.mcp_call.in_progress")]
356    McpCallInProgress {
357        item_id: String,
358        output_index: u64,
359        sequence_number: u64,
360    },
361    #[serde(rename = "response.mcp_call.completed")]
362    McpCallCompleted {
363        item_id: String,
364        output_index: u64,
365        sequence_number: u64,
366    },
367    #[serde(rename = "response.mcp_call.failed")]
368    McpCallFailed {
369        item_id: String,
370        output_index: u64,
371        sequence_number: u64,
372    },
373
374    #[serde(rename = "response.mcp_list_tools.in_progress")]
375    McpListToolsInProgress {
376        item_id: String,
377        output_index: u64,
378        sequence_number: u64,
379    },
380    #[serde(rename = "response.mcp_list_tools.completed")]
381    McpListToolsCompleted {
382        item_id: String,
383        output_index: u64,
384        sequence_number: u64,
385    },
386    #[serde(rename = "response.mcp_list_tools.failed")]
387    McpListToolsFailed {
388        item_id: String,
389        output_index: u64,
390        sequence_number: u64,
391    },
392
393    #[serde(rename = "response.custom_tool_call_input.delta")]
394    CustomToolCallInputDelta {
395        delta: String,
396        item_id: String,
397        output_index: u64,
398        sequence_number: u64,
399        #[serde(default, skip_serializing_if = "Option::is_none")]
400        obfuscation: Option<String>,
401    },
402    #[serde(rename = "response.custom_tool_call_input.done")]
403    CustomToolCallInputDone {
404        input: String,
405        item_id: String,
406        output_index: u64,
407        sequence_number: u64,
408    },
409
410    #[serde(rename = "error")]
411    Error {
412        error: ResponseStreamErrorPayload,
413        sequence_number: u64,
414    },
415
416    /// Undocumented SSE frame Codex ships while the upstream is still working
417    /// (`event: keepalive\ndata: {"type":"keepalive","sequence_number":N}`).
418    /// Not part of OpenAI's public Responses streaming spec but required to
419    /// keep deserialization from failing mid-stream.
420    #[serde(rename = "keepalive")]
421    Keepalive { sequence_number: u64 },
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425#[serde(untagged)]
426pub enum ResponseStreamContentPart {
427    OutputText(ResponseOutputText),
428    Refusal(ResponseOutputRefusal),
429    ReasoningText(ResponseReasoningTextContent),
430}
431
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
433pub struct ResponseStreamTokenLogprob {
434    pub token: String,
435    pub logprob: f64,
436    #[serde(default, skip_serializing_if = "Option::is_none")]
437    pub top_logprobs: Option<Vec<ResponseStreamTopLogprob>>,
438}
439
440#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
441pub struct ResponseStreamTopLogprob {
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub token: Option<String>,
444    #[serde(default, skip_serializing_if = "Option::is_none")]
445    pub logprob: Option<f64>,
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    // Codex responses backend ships `{"type":"keepalive","sequence_number":N}`
453    // mid-stream. Not in OpenAI's public spec, but real traffic includes it,
454    // so rejecting the frame aborts the whole SSE with a 500.
455    #[test]
456    fn deserializes_codex_keepalive_frame() {
457        let event: ResponseStreamEvent =
458            serde_json::from_str(r#"{"type":"keepalive","sequence_number":5}"#)
459                .expect("keepalive must deserialize");
460        assert!(matches!(
461            event,
462            ResponseStreamEvent::Keepalive { sequence_number: 5 }
463        ));
464    }
465}