dynamo_protocols/types/
realtime.rs1pub use async_openai::types::realtime::*;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize)]
17pub struct RealtimeClientEventInputTextAppend {
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub event_id: Option<String>,
20 pub text: String,
21}
22
23#[derive(Debug, Default, Serialize, Deserialize)]
25pub struct RealtimeClientEventInputTextCommit {
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub event_id: Option<String>,
28}
29
30#[derive(Debug, Default, Serialize, Deserialize)]
35pub struct RealtimeClientEventInputTextClear {
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub event_id: Option<String>,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
45#[serde(tag = "type")]
46pub enum RealtimeClientEventExtension {
47 #[serde(rename = "input_text.append")]
48 InputTextAppend(RealtimeClientEventInputTextAppend),
49 #[serde(rename = "input_text.commit")]
50 InputTextCommit(RealtimeClientEventInputTextCommit),
51 #[serde(rename = "input_text.clear")]
52 InputTextClear(RealtimeClientEventInputTextClear),
53}
54
55#[derive(Debug, Serialize, Deserialize)]
57#[serde(untagged)]
58pub enum DynamoRealtimeClientEvent {
59 OpenAI(RealtimeClientEvent),
60 Extension(RealtimeClientEventExtension),
61}
62
63impl From<RealtimeClientEvent> for DynamoRealtimeClientEvent {
64 fn from(event: RealtimeClientEvent) -> Self {
65 Self::OpenAI(event)
66 }
67}
68
69pub trait EventType {
85 fn event_type(&self) -> &'static str;
86}
87
88impl EventType for RealtimeClientEvent {
89 fn event_type(&self) -> &'static str {
90 match self {
94 RealtimeClientEvent::SessionUpdate(_) => "session.update",
95 RealtimeClientEvent::InputAudioBufferAppend(_) => "input_audio_buffer.append",
96 RealtimeClientEvent::InputAudioBufferCommit(_) => "input_audio_buffer.commit",
97 RealtimeClientEvent::InputAudioBufferClear(_) => "input_audio_buffer.clear",
98 RealtimeClientEvent::ConversationItemCreate(_) => "conversation.item.create",
99 RealtimeClientEvent::ConversationItemRetrieve(_) => "conversation.item.retrieve",
100 RealtimeClientEvent::ConversationItemTruncate(_) => "conversation.item.truncate",
101 RealtimeClientEvent::ConversationItemDelete(_) => "conversation.item.delete",
102 RealtimeClientEvent::ResponseCreate(_) => "response.create",
103 RealtimeClientEvent::ResponseCancel(_) => "response.cancel",
104 RealtimeClientEvent::OutputAudioBufferClear(_) => "output_audio_buffer.clear",
105 }
106 }
107}
108
109impl EventType for RealtimeClientEventExtension {
110 fn event_type(&self) -> &'static str {
111 match self {
112 RealtimeClientEventExtension::InputTextAppend(_) => "input_text.append",
113 RealtimeClientEventExtension::InputTextCommit(_) => "input_text.commit",
114 RealtimeClientEventExtension::InputTextClear(_) => "input_text.clear",
115 }
116 }
117}
118
119impl EventType for DynamoRealtimeClientEvent {
120 fn event_type(&self) -> &'static str {
121 match self {
122 DynamoRealtimeClientEvent::OpenAI(event) => event.event_type(),
123 DynamoRealtimeClientEvent::Extension(event) => event.event_type(),
124 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn session_update_accepts_null_turn_detection() {
134 let event: RealtimeClientEvent = serde_json::from_value(serde_json::json!({
135 "type": "session.update",
136 "session": {
137 "type": "transcription",
138 "audio": {
139 "input": {
140 "format": { "type": "audio/pcm", "rate": 24000 },
141 "transcription": { "model": "whisper-1" },
142 "turn_detection": null
143 }
144 }
145 }
146 }))
147 .expect("null turn_detection should be accepted");
148
149 let RealtimeClientEvent::SessionUpdate(update) = event else {
150 panic!("expected session.update");
151 };
152 let Session::RealtimeTranscriptionSession(session) = update.session else {
153 panic!("expected transcription session");
154 };
155 assert!(session.audio.input.turn_detection.is_none());
156 }
157
158 #[test]
159 fn text_events_round_trip() {
160 let values = [
161 (
162 serde_json::json!({
163 "type": "input_text.append",
164 "event_id": "append-1",
165 "text": "hello "
166 }),
167 "input_text.append",
168 ),
169 (
170 serde_json::json!({
171 "type": "input_text.commit",
172 "event_id": "commit-1"
173 }),
174 "input_text.commit",
175 ),
176 (
177 serde_json::json!({
178 "type": "input_text.clear",
179 "event_id": "clear-1"
180 }),
181 "input_text.clear",
182 ),
183 ];
184
185 for (value, event_type) in values {
186 let event: DynamoRealtimeClientEvent =
187 serde_json::from_value(value.clone()).expect("event should deserialize");
188 assert_eq!(event.event_type(), event_type);
189 assert_eq!(serde_json::to_value(event).unwrap(), value);
190 }
191 }
192
193 #[test]
194 fn text_append_requires_non_null_text() {
195 for value in [
196 serde_json::json!({"type": "input_text.append"}),
197 serde_json::json!({"type": "input_text.append", "text": null}),
198 ] {
199 assert!(serde_json::from_value::<DynamoRealtimeClientEvent>(value).is_err());
200 }
201 }
202}