gproxy_protocol/openai/realtime/
mod.rs1macro_rules! extensible_string {
7 ($name:ident, $known:ident { $($variant:ident => $wire:literal),+ $(,)? }) => {
8 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9 #[serde(untagged)]
10 #[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
11 pub enum $name {
12 Known($known),
13 Unknown(String),
14 }
15
16 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17 #[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
18 pub enum $known {
19 $(#[serde(rename = $wire)] $variant),+
20 }
21 };
22}
23
24mod audio;
25mod client_events;
26mod items;
27mod response;
28mod server_events;
29mod session;
30
31pub use audio::*;
32pub use client_events::*;
33pub use items::*;
34pub use response::*;
35pub use server_events::*;
36pub use session::*;
37
38use serde::{Deserialize, Serialize};
39
40use crate::openai::common::{OpenAiModelId, OpenAiWireModel, Rest};
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
43#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
44pub struct CreateRealtimeCallRequest {
45 pub sdp: String,
46 pub session: RealtimeSession,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub model: Option<OpenAiModelId>,
49 #[serde(default, flatten)]
50 pub rest: Rest,
51}
52
53pub type CreateRealtimeCallResponse = String;
54pub type RealtimeWireModel = OpenAiWireModel<RealtimeClientEvent, RealtimeServerEvent>;
55
56#[cfg(test)]
57mod tests {
58 use serde_json::json;
59
60 use super::*;
61
62 #[test]
63 fn realtime_call_round_trip_preserves_session_extensions() {
64 let value = json!({
65 "sdp":"v=0\r\n",
66 "model":"public-route",
67 "session": {
68 "type":"realtime",
69 "model":"gpt-realtime",
70 "audio":{"input":{"future_audio":true}},
71 "future_session":1
72 },
73 "future_call":true
74 });
75 let parsed: super::CreateRealtimeCallRequest =
76 serde_json::from_value(value.clone()).unwrap();
77 assert_eq!(serde_json::to_value(parsed).unwrap(), value);
78 }
79
80 #[test]
81 fn realtime_event_unions_round_trip_known_unknown_and_evolved_payloads() {
82 let client = json!({
83 "type":"session.update",
84 "event_id":"evt_1",
85 "session":{"type":"realtime","model":"gpt-realtime"},
86 "future_client":true
87 });
88 let parsed: RealtimeClientEvent = serde_json::from_value(client.clone()).unwrap();
89 assert!(matches!(
90 &parsed,
91 RealtimeClientEvent::Known(event)
92 if matches!(event.as_ref(), KnownRealtimeClientEvent::SessionUpdate { .. })
93 ));
94 assert_eq!(serde_json::to_value(parsed).unwrap(), client);
95
96 let item = json!({
97 "type":"message",
98 "id":"item_1",
99 "role":"assistant",
100 "content":[{"type":"output_audio","audio":"AAEC","future_part":1}],
101 "future_item":true
102 });
103 let parsed: RealtimeItem = serde_json::from_value(item.clone()).unwrap();
104 assert!(matches!(
105 &parsed,
106 RealtimeItem::Known(KnownRealtimeItem::Message { .. })
107 ));
108 assert_eq!(serde_json::to_value(parsed).unwrap(), item);
109
110 let server = json!({
111 "type":"response.output_audio.delta",
112 "response_id":"resp_1",
113 "item_id":"item_1",
114 "output_index":0,
115 "content_index":0,
116 "delta":"AAEC",
117 "future_server":2
118 });
119 let parsed: RealtimeServerEvent = serde_json::from_value(server.clone()).unwrap();
120 assert!(matches!(
121 &parsed,
122 RealtimeServerEvent::Known(event)
123 if matches!(event.as_ref(), KnownRealtimeServerEvent::OutputAudioDelta(_))
124 ));
125 assert_eq!(serde_json::to_value(parsed).unwrap(), server);
126
127 for unknown in [
128 json!({"type":"response.hologram.delta","delta":"?","future":3}),
129 json!({"type":"response.output_audio.delta","delta":"missing required fields"}),
130 ] {
131 let parsed: RealtimeServerEvent = serde_json::from_value(unknown.clone()).unwrap();
132 assert!(matches!(&parsed, RealtimeServerEvent::Unknown(_)));
133 assert_eq!(serde_json::to_value(parsed).unwrap(), unknown);
134 }
135
136 let evolved_item = json!({
137 "type":"message",
138 "role":"assistant",
139 "content":[{"type":"hologram","payload":"?"}]
140 });
141 let parsed: RealtimeItem = serde_json::from_value(evolved_item.clone()).unwrap();
142 assert!(matches!(&parsed, RealtimeItem::Unknown(_)));
143 assert_eq!(serde_json::to_value(parsed).unwrap(), evolved_item);
144 }
145
146 #[test]
152 fn realtime_matches_the_documented_wire() {
153 let created = json!({
154 "type":"conversation.created",
155 "event_id":"event_9999",
156 "conversation":{"id":"conv_001","object":"realtime.conversation"}
157 });
158 let parsed: RealtimeServerEvent = serde_json::from_value(created.clone()).unwrap();
159 assert!(matches!(
160 &parsed,
161 RealtimeServerEvent::Known(event)
162 if matches!(event.as_ref(), KnownRealtimeServerEvent::ConversationCreated(_))
163 ));
164 assert_eq!(serde_json::to_value(parsed).unwrap(), created);
165
166 let dtmf = json!({
167 "type":"input_audio_buffer.dtmf_event_received",
168 "event_id":"event_1",
169 "event":"5",
170 "received_at":1_764_000_000.0
171 });
172 let parsed: RealtimeServerEvent = serde_json::from_value(dtmf.clone()).unwrap();
173 assert!(matches!(
174 &parsed,
175 RealtimeServerEvent::Known(event)
176 if matches!(
177 event.as_ref(),
178 KnownRealtimeServerEvent::InputAudioBufferDtmfEventReceived(_)
179 )
180 ));
181 assert_eq!(serde_json::to_value(parsed).unwrap(), dtmf);
182
183 for translation in [
184 json!({"type":"session.close","event_id":"evt_2"}),
185 json!({"type":"session.input_audio_buffer.append","audio":"AAEC"}),
186 ] {
187 let parsed: RealtimeClientEvent = serde_json::from_value(translation.clone()).unwrap();
188 assert!(matches!(&parsed, RealtimeClientEvent::Unknown(_)));
189 assert_eq!(serde_json::to_value(parsed).unwrap(), translation);
190 }
191 }
192}