Skip to main content

gproxy_protocol/protocol/openai/realtime/
mod.rs

1//! OpenAI Realtime API wire models (GA: WebSocket `GET /v1/realtime` and
2//! WebRTC `POST /v1/realtime/calls`).
3//!
4//! These modules mirror Realtime JSON wire shapes only; transport (WebSocket
5//! framing, SDP exchange) and provider conversion live outside this layer.
6
7mod client_events;
8mod items;
9mod response;
10mod server_events;
11mod session;
12
13pub use client_events::*;
14pub use items::*;
15pub use response::*;
16pub use server_events::*;
17pub use session::*;
18
19use super::common::OpenAiWireModel;
20
21pub type RealtimeWireModel = OpenAiWireModel<RealtimeClientEvent, RealtimeServerEvent>;
22
23macro_rules! realtime_string_enum {
24    ($outer:ident, $known:ident { $($variant:ident => $wire:literal),+ $(,)? }) => {
25        #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
26        #[serde(untagged)]
27        #[non_exhaustive]
28        pub enum $outer {
29            Known($known),
30            Unknown(String),
31        }
32
33        impl<'de> serde::Deserialize<'de> for $outer {
34            fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
35                crate::protocol::extensible::deserialize_extensible(d, Self::Known, Self::Unknown)
36            }
37        }
38
39        #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40        #[non_exhaustive]
41        pub enum $known {
42            $(#[serde(rename = $wire)] $variant,)+
43        }
44    };
45}
46
47pub(crate) use realtime_string_enum;
48
49#[cfg(test)]
50mod tests {
51    use serde_json::json;
52
53    use super::*;
54
55    #[test]
56    fn session_update_round_trips_ga_audio_shape() {
57        let value = json!({
58            "type": "session.update",
59            "session": {
60                "type": "realtime",
61                "model": "gpt-realtime",
62                "output_modalities": ["audio"],
63                "instructions": "be brief",
64                "audio": {
65                    "input": {
66                        "format": {"type": "audio/pcm", "rate": 24000},
67                        "turn_detection": {"type": "semantic_vad", "eagerness": "low"}
68                    },
69                    "output": {"format": {"type": "audio/pcmu"}, "voice": "marin", "speed": 1.1}
70                }
71            }
72        });
73        let parsed: RealtimeClientEvent = serde_json::from_value(value.clone()).unwrap();
74        let RealtimeClientEvent::SessionUpdate { session, .. } = &parsed else {
75            panic!("expected session.update")
76        };
77        assert!(matches!(
78            session
79                .audio
80                .as_ref()
81                .unwrap()
82                .input
83                .as_ref()
84                .unwrap()
85                .format,
86            Some(RealtimeAudioFormat::Pcm {
87                rate: Some(24000),
88                ..
89            })
90        ));
91        assert_eq!(serde_json::to_value(&parsed).unwrap(), value);
92    }
93
94    #[test]
95    fn server_events_parse_known_and_fall_back_to_unknown() {
96        let known: RealtimeServerEvent = serde_json::from_value(json!({
97            "type": "response.output_audio.delta",
98            "response_id": "resp_1",
99            "item_id": "item_1",
100            "output_index": 0,
101            "content_index": 0,
102            "delta": "b64"
103        }))
104        .unwrap();
105        let RealtimeServerEvent::Known(known) = known else {
106            panic!("expected known event")
107        };
108        assert!(matches!(
109            *known,
110            KnownRealtimeServerEvent::OutputAudioDelta { .. }
111        ));
112
113        let unknown: RealtimeServerEvent = serde_json::from_value(json!({
114            "type": "response.hologram.delta",
115            "delta": "?"
116        }))
117        .unwrap();
118        assert!(matches!(unknown, RealtimeServerEvent::Unknown(_)));
119    }
120}