Skip to main content

gproxy_protocol/protocol/openai/realtime/
server_events.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize, de};
4use serde_json::Value;
5
6use super::super::common::*;
7use super::{
8    RealtimeContentPart, RealtimeError, RealtimeItem, RealtimeRateLimit, RealtimeResponse,
9    RealtimeSessionConfig, RealtimeUsage,
10};
11
12/// Events the server sends over the realtime WebSocket. Unknown event types
13/// round-trip through [`UnknownRealtimeServerEvent`]; a known `type` whose
14/// payload fails to parse also degrades to `Unknown` so a live session never
15/// hard-fails on wire evolution.
16#[derive(Debug, Clone, PartialEq, Serialize)]
17#[serde(untagged)]
18#[non_exhaustive]
19pub enum RealtimeServerEvent {
20    Known(Box<KnownRealtimeServerEvent>),
21    Unknown(UnknownRealtimeServerEvent),
22}
23
24impl<'de> Deserialize<'de> for RealtimeServerEvent {
25    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
26        let value = Value::deserialize(deserializer)?;
27        if let Ok(event) = serde_json::from_value(value.clone()) {
28            return Ok(Self::Known(Box::new(event)));
29        }
30        serde_json::from_value(value)
31            .map(Self::Unknown)
32            .map_err(de::Error::custom)
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
37#[non_exhaustive]
38pub struct UnknownRealtimeServerEvent {
39    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
40    pub type_: Option<String>,
41    #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
42    pub extra: Extra,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(tag = "type")]
47#[allow(clippy::large_enum_variant)]
48#[non_exhaustive]
49pub enum KnownRealtimeServerEvent {
50    #[serde(rename = "error")]
51    Error {
52        error: RealtimeError,
53        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
54        extra: Extra,
55    },
56    #[serde(rename = "session.created")]
57    SessionCreated {
58        session: Box<RealtimeSessionConfig>,
59        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
60        extra: Extra,
61    },
62    #[serde(rename = "session.updated")]
63    SessionUpdated {
64        session: Box<RealtimeSessionConfig>,
65        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
66        extra: Extra,
67    },
68    #[serde(rename = "conversation.item.added")]
69    ConversationItemAdded {
70        item: RealtimeItem,
71        #[serde(skip_serializing_if = "Option::is_none")]
72        previous_item_id: Option<String>,
73        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
74        extra: Extra,
75    },
76    /// Beta-era alias of `conversation.item.added` still emitted by some peers.
77    #[serde(rename = "conversation.item.created")]
78    ConversationItemCreated {
79        item: RealtimeItem,
80        #[serde(skip_serializing_if = "Option::is_none")]
81        previous_item_id: Option<String>,
82        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
83        extra: Extra,
84    },
85    #[serde(rename = "conversation.item.done")]
86    ConversationItemDone {
87        item: RealtimeItem,
88        #[serde(skip_serializing_if = "Option::is_none")]
89        previous_item_id: Option<String>,
90        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
91        extra: Extra,
92    },
93    #[serde(rename = "conversation.item.retrieved")]
94    ConversationItemRetrieved {
95        item: RealtimeItem,
96        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
97        extra: Extra,
98    },
99    #[serde(rename = "conversation.item.truncated")]
100    ConversationItemTruncated {
101        item_id: String,
102        content_index: u32,
103        audio_end_ms: u64,
104        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
105        extra: Extra,
106    },
107    #[serde(rename = "conversation.item.deleted")]
108    ConversationItemDeleted {
109        item_id: String,
110        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
111        extra: Extra,
112    },
113    #[serde(rename = "conversation.item.input_audio_transcription.delta")]
114    InputAudioTranscriptionDelta {
115        item_id: String,
116        #[serde(skip_serializing_if = "Option::is_none")]
117        content_index: Option<u32>,
118        delta: String,
119        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
120        extra: Extra,
121    },
122    #[serde(rename = "conversation.item.input_audio_transcription.completed")]
123    InputAudioTranscriptionCompleted {
124        item_id: String,
125        #[serde(skip_serializing_if = "Option::is_none")]
126        content_index: Option<u32>,
127        transcript: String,
128        #[serde(skip_serializing_if = "Option::is_none")]
129        usage: Option<RealtimeUsage>,
130        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
131        extra: Extra,
132    },
133    #[serde(rename = "conversation.item.input_audio_transcription.failed")]
134    InputAudioTranscriptionFailed {
135        item_id: String,
136        #[serde(skip_serializing_if = "Option::is_none")]
137        content_index: Option<u32>,
138        error: RealtimeError,
139        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
140        extra: Extra,
141    },
142    #[serde(rename = "conversation.item.input_audio_transcription.segment")]
143    InputAudioTranscriptionSegment {
144        item_id: String,
145        #[serde(skip_serializing_if = "Option::is_none")]
146        id: Option<String>,
147        #[serde(skip_serializing_if = "Option::is_none")]
148        speaker: Option<String>,
149        #[serde(skip_serializing_if = "Option::is_none")]
150        text: Option<String>,
151        #[serde(skip_serializing_if = "Option::is_none")]
152        start: Option<f64>,
153        #[serde(skip_serializing_if = "Option::is_none")]
154        end: Option<f64>,
155        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
156        extra: Extra,
157    },
158    #[serde(rename = "input_audio_buffer.committed")]
159    InputAudioBufferCommitted {
160        item_id: String,
161        #[serde(skip_serializing_if = "Option::is_none")]
162        previous_item_id: Option<String>,
163        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
164        extra: Extra,
165    },
166    #[serde(rename = "input_audio_buffer.cleared")]
167    InputAudioBufferCleared {
168        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
169        extra: Extra,
170    },
171    #[serde(rename = "input_audio_buffer.speech_started")]
172    InputAudioBufferSpeechStarted {
173        item_id: String,
174        audio_start_ms: u64,
175        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
176        extra: Extra,
177    },
178    #[serde(rename = "input_audio_buffer.speech_stopped")]
179    InputAudioBufferSpeechStopped {
180        item_id: String,
181        audio_end_ms: u64,
182        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
183        extra: Extra,
184    },
185    #[serde(rename = "input_audio_buffer.timeout_triggered")]
186    InputAudioBufferTimeoutTriggered {
187        #[serde(skip_serializing_if = "Option::is_none")]
188        item_id: Option<String>,
189        #[serde(skip_serializing_if = "Option::is_none")]
190        audio_start_ms: Option<u64>,
191        #[serde(skip_serializing_if = "Option::is_none")]
192        audio_end_ms: Option<u64>,
193        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
194        extra: Extra,
195    },
196    #[serde(rename = "output_audio_buffer.started")]
197    OutputAudioBufferStarted {
198        response_id: String,
199        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
200        extra: Extra,
201    },
202    #[serde(rename = "output_audio_buffer.stopped")]
203    OutputAudioBufferStopped {
204        response_id: String,
205        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
206        extra: Extra,
207    },
208    #[serde(rename = "output_audio_buffer.cleared")]
209    OutputAudioBufferCleared {
210        response_id: String,
211        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
212        extra: Extra,
213    },
214    #[serde(rename = "rate_limits.updated")]
215    RateLimitsUpdated {
216        rate_limits: Vec<RealtimeRateLimit>,
217        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
218        extra: Extra,
219    },
220    #[serde(rename = "response.created")]
221    ResponseCreated {
222        response: Box<RealtimeResponse>,
223        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
224        extra: Extra,
225    },
226    #[serde(rename = "response.done")]
227    ResponseDone {
228        response: Box<RealtimeResponse>,
229        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
230        extra: Extra,
231    },
232    #[serde(rename = "response.output_item.added")]
233    ResponseOutputItemAdded {
234        response_id: String,
235        output_index: u32,
236        item: RealtimeItem,
237        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
238        extra: Extra,
239    },
240    #[serde(rename = "response.output_item.done")]
241    ResponseOutputItemDone {
242        response_id: String,
243        output_index: u32,
244        item: RealtimeItem,
245        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
246        extra: Extra,
247    },
248    #[serde(rename = "response.content_part.added")]
249    ResponseContentPartAdded {
250        response_id: String,
251        item_id: String,
252        output_index: u32,
253        content_index: u32,
254        part: RealtimeContentPart,
255        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
256        extra: Extra,
257    },
258    #[serde(rename = "response.content_part.done")]
259    ResponseContentPartDone {
260        response_id: String,
261        item_id: String,
262        output_index: u32,
263        content_index: u32,
264        part: RealtimeContentPart,
265        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
266        extra: Extra,
267    },
268    #[serde(rename = "response.output_text.delta")]
269    OutputTextDelta {
270        response_id: String,
271        item_id: String,
272        output_index: u32,
273        content_index: u32,
274        delta: String,
275        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
276        extra: Extra,
277    },
278    #[serde(rename = "response.output_text.done")]
279    OutputTextDone {
280        response_id: String,
281        item_id: String,
282        output_index: u32,
283        content_index: u32,
284        text: String,
285        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
286        extra: Extra,
287    },
288    #[serde(rename = "response.output_audio_transcript.delta")]
289    OutputAudioTranscriptDelta {
290        response_id: String,
291        item_id: String,
292        output_index: u32,
293        content_index: u32,
294        delta: String,
295        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
296        extra: Extra,
297    },
298    #[serde(rename = "response.output_audio_transcript.done")]
299    OutputAudioTranscriptDone {
300        response_id: String,
301        item_id: String,
302        output_index: u32,
303        content_index: u32,
304        transcript: String,
305        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
306        extra: Extra,
307    },
308    #[serde(rename = "response.output_audio.delta")]
309    OutputAudioDelta {
310        response_id: String,
311        item_id: String,
312        output_index: u32,
313        content_index: u32,
314        /// Base64-encoded audio in the session output format.
315        delta: String,
316        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
317        extra: Extra,
318    },
319    #[serde(rename = "response.output_audio.done")]
320    OutputAudioDone {
321        response_id: String,
322        item_id: String,
323        output_index: u32,
324        content_index: u32,
325        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
326        extra: Extra,
327    },
328    #[serde(rename = "response.function_call_arguments.delta")]
329    FunctionCallArgumentsDelta {
330        response_id: String,
331        item_id: String,
332        output_index: u32,
333        call_id: String,
334        delta: String,
335        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
336        extra: Extra,
337    },
338    #[serde(rename = "response.function_call_arguments.done")]
339    FunctionCallArgumentsDone {
340        response_id: String,
341        item_id: String,
342        output_index: u32,
343        call_id: String,
344        arguments: String,
345        #[serde(skip_serializing_if = "Option::is_none")]
346        name: Option<String>,
347        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
348        extra: Extra,
349    },
350    #[serde(rename = "response.mcp_call_arguments.delta")]
351    McpCallArgumentsDelta {
352        response_id: String,
353        item_id: String,
354        output_index: u32,
355        delta: String,
356        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
357        extra: Extra,
358    },
359    #[serde(rename = "response.mcp_call_arguments.done")]
360    McpCallArgumentsDone {
361        response_id: String,
362        item_id: String,
363        output_index: u32,
364        arguments: String,
365        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
366        extra: Extra,
367    },
368    #[serde(rename = "response.mcp_call.in_progress")]
369    McpCallInProgress {
370        item_id: String,
371        output_index: u32,
372        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
373        extra: Extra,
374    },
375    #[serde(rename = "response.mcp_call.completed")]
376    McpCallCompleted {
377        item_id: String,
378        output_index: u32,
379        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
380        extra: Extra,
381    },
382    #[serde(rename = "response.mcp_call.failed")]
383    McpCallFailed {
384        item_id: String,
385        output_index: u32,
386        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
387        extra: Extra,
388    },
389    #[serde(rename = "mcp_list_tools.in_progress")]
390    McpListToolsInProgress {
391        #[serde(skip_serializing_if = "Option::is_none")]
392        item_id: Option<String>,
393        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
394        extra: Extra,
395    },
396    #[serde(rename = "mcp_list_tools.completed")]
397    McpListToolsCompleted {
398        #[serde(skip_serializing_if = "Option::is_none")]
399        item_id: Option<String>,
400        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
401        extra: Extra,
402    },
403    #[serde(rename = "mcp_list_tools.failed")]
404    McpListToolsFailed {
405        #[serde(skip_serializing_if = "Option::is_none")]
406        item_id: Option<String>,
407        #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
408        extra: Extra,
409    },
410}