1use std::future::Future;
9
10use bytes::Bytes;
11use serde::Deserialize;
12use serde::Serialize;
13use url::Url;
14
15use crate::error::NoSuchModelError;
16use crate::error::ProviderError;
17use crate::json::JsonObject;
18use crate::json::JsonValue;
19use crate::shared::AudioFormat;
20use crate::shared::ModelId;
21use crate::shared::ProviderId;
22use crate::shared::base64_bytes;
23
24pub trait RealtimeModel: Send + Sync + 'static {
26 fn provider(&self) -> &ProviderId;
28
29 fn model_id(&self) -> &ModelId;
31
32 fn do_create_client_secret(
34 &self,
35 options: ClientSecretOptions,
36 ) -> impl Future<Output = Result<ClientSecret, ProviderError>> + Send;
37
38 fn websocket_config(&self, token: &str, url: &Url) -> WebSocketConfig;
40
41 fn parse_server_event(&self, raw: JsonValue)
49 -> Result<Vec<RealtimeServerEvent>, ProviderError>;
50
51 fn serialize_client_event(
53 &self,
54 event: RealtimeClientEvent,
55 ) -> impl Future<Output = Result<JsonValue, ProviderError>> + Send;
56
57 fn build_session_config(
63 &self,
64 config: &RealtimeSessionConfig,
65 ) -> Result<JsonValue, ProviderError>;
66
67 fn health_check_response(&self, raw: &JsonValue) -> Option<JsonValue> {
69 let _ = raw;
70 None
71 }
72}
73
74pub trait RealtimeFactory: Send + Sync + 'static {
76 fn provider(&self) -> &ProviderId;
78
79 fn model(&self, model_id: &str) -> Result<crate::dynamic::RealtimeModelRef, NoSuchModelError>;
85
86 fn get_token(
88 &self,
89 options: GetTokenOptions,
90 ) -> impl Future<Output = Result<ClientSecret, ProviderError>> + Send;
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
95pub struct ClientSecretOptions {
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub expires_after_seconds: Option<u64>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub session_config: Option<RealtimeSessionConfig>,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct GetTokenOptions {
107 pub model: ModelId,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub expires_after_seconds: Option<u64>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub session_config: Option<RealtimeSessionConfig>,
115}
116
117#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct ClientSecret {
120 pub token: String,
122 pub url: Url,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub expires_at: Option<u64>,
127}
128
129impl std::fmt::Debug for ClientSecret {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 f.debug_struct("ClientSecret")
132 .field("token", &"***")
133 .field("url", &self.url)
134 .field("expires_at", &self.expires_at)
135 .finish()
136 }
137}
138
139#[derive(Clone, PartialEq, Eq)]
141pub struct WebSocketConfig {
142 pub url: Url,
144 pub protocols: Vec<String>,
146}
147
148impl std::fmt::Debug for WebSocketConfig {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("WebSocketConfig")
151 .field("url", &"***")
152 .field("protocols", &"***")
153 .finish()
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
159#[serde(rename_all = "lowercase")]
160#[non_exhaustive]
161pub enum Modality {
162 Text,
164 Audio,
166}
167
168#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
170pub struct TranscriptionConfig {
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub model: Option<String>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub language: Option<String>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub prompt: Option<String>,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
184#[serde(rename_all = "kebab-case")]
185#[non_exhaustive]
186pub enum TurnDetectionKind {
187 ServerVad,
189 SemanticVad,
191 Disabled,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct TurnDetection {
198 #[serde(rename = "type")]
200 pub kind: TurnDetectionKind,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub threshold: Option<f64>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub silence_duration_ms: Option<u64>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub prefix_padding_ms: Option<u64>,
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct RealtimeToolDefinition {
215 pub name: String,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub description: Option<String>,
220 pub parameters: JsonValue,
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
226pub struct RealtimeSessionConfig {
227 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub instructions: Option<String>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub voice: Option<String>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub output_modalities: Option<Vec<Modality>>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub input_audio_format: Option<AudioFormat>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub input_audio_transcription: Option<TranscriptionConfig>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub output_audio_transcription: Option<TranscriptionConfig>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub output_audio_format: Option<AudioFormat>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub turn_detection: Option<TurnDetection>,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub tools: Vec<RealtimeToolDefinition>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub provider_options: Option<JsonObject>,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
261#[serde(rename_all = "lowercase")]
262#[non_exhaustive]
263pub enum ConversationRole {
264 User,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(tag = "type", rename_all = "kebab-case")]
271#[non_exhaustive]
272pub enum ConversationItem {
273 TextMessage {
275 role: ConversationRole,
277 text: String,
279 },
280 AudioMessage {
282 role: ConversationRole,
284 #[serde(with = "base64_bytes")]
286 audio: Bytes,
287 },
288 FunctionCallOutput {
290 call_id: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 name: Option<String>,
295 output: String,
297 },
298}
299
300#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
302pub struct ResponseCreateOptions {
303 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub modalities: Option<Vec<String>>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub instructions: Option<String>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub metadata: Option<JsonObject>,
312}
313
314#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316#[serde(tag = "type", rename_all = "kebab-case")]
317#[non_exhaustive]
318pub enum RealtimeClientEvent {
319 SessionUpdate {
321 config: Box<RealtimeSessionConfig>,
323 },
324 InputAudioAppend {
326 #[serde(with = "base64_bytes")]
328 audio: Bytes,
329 },
330 InputAudioCommit,
332 InputAudioClear,
334 ConversationItemCreate {
336 item: ConversationItem,
338 },
339 ConversationItemTruncate {
341 item_id: String,
343 content_index: u32,
345 audio_end_ms: u64,
347 },
348 ResponseCreate {
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 options: Option<ResponseCreateOptions>,
353 },
354 ResponseCancel,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
362#[serde(tag = "type", rename_all = "kebab-case")]
363#[non_exhaustive]
364pub enum RealtimeServerEvent {
365 SessionCreated {
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 session_id: Option<String>,
370 raw: JsonValue,
372 },
373 SessionUpdated {
375 raw: JsonValue,
377 },
378 SpeechStarted {
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 item_id: Option<String>,
383 raw: JsonValue,
385 },
386 SpeechStopped {
388 #[serde(default, skip_serializing_if = "Option::is_none")]
390 item_id: Option<String>,
391 raw: JsonValue,
393 },
394 AudioCommitted {
396 #[serde(default, skip_serializing_if = "Option::is_none")]
398 item_id: Option<String>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
401 previous_item_id: Option<String>,
402 raw: JsonValue,
404 },
405 ConversationItemAdded {
407 item_id: String,
409 item: JsonValue,
411 raw: JsonValue,
413 },
414 InputTranscriptionCompleted {
416 item_id: String,
418 transcript: String,
420 raw: JsonValue,
422 },
423 ResponseCreated {
425 response_id: String,
427 raw: JsonValue,
429 },
430 ResponseDone {
432 response_id: String,
434 status: String,
436 raw: JsonValue,
438 },
439 OutputItemAdded {
441 response_id: String,
443 item_id: String,
445 raw: JsonValue,
447 },
448 OutputItemDone {
450 response_id: String,
452 item_id: String,
454 raw: JsonValue,
456 },
457 ContentPartAdded {
459 response_id: String,
461 item_id: String,
463 raw: JsonValue,
465 },
466 ContentPartDone {
468 response_id: String,
470 item_id: String,
472 raw: JsonValue,
474 },
475 AudioDelta {
477 response_id: String,
479 item_id: String,
481 #[serde(with = "base64_bytes")]
483 delta: Bytes,
484 raw: JsonValue,
486 },
487 AudioDone {
489 response_id: String,
491 item_id: String,
493 raw: JsonValue,
495 },
496 AudioTranscriptDelta {
498 response_id: String,
500 item_id: String,
502 delta: String,
504 raw: JsonValue,
506 },
507 AudioTranscriptDone {
509 response_id: String,
511 item_id: String,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
515 transcript: Option<String>,
516 raw: JsonValue,
518 },
519 TextDelta {
521 response_id: String,
523 item_id: String,
525 delta: String,
527 raw: JsonValue,
529 },
530 TextDone {
532 response_id: String,
534 item_id: String,
536 #[serde(default, skip_serializing_if = "Option::is_none")]
538 text: Option<String>,
539 raw: JsonValue,
541 },
542 FunctionCallArgumentsDelta {
544 response_id: String,
546 item_id: String,
548 call_id: String,
550 delta: String,
552 raw: JsonValue,
554 },
555 FunctionCallArgumentsDone {
557 response_id: String,
559 item_id: String,
561 call_id: String,
563 name: String,
565 arguments: String,
567 raw: JsonValue,
569 },
570 Error {
572 message: String,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
576 code: Option<String>,
577 raw: JsonValue,
579 },
580 Custom {
582 raw_type: String,
584 raw: JsonValue,
586 },
587}