dynamo_protocols/types/realtime.rs
1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Re-exports upstream async-openai realtime event types. Per the ownership
5// rubric in `lib/protocols/CLAUDE.md`, no Dynamo-side overrides are needed
6// today: upstream covers the full event surface (`RealtimeClientEvent`,
7// `RealtimeServerEvent`, and their per-variant payloads) and no real client
8// has been observed to send a shape upstream rejects.
9
10pub use async_openai::types::realtime::*;
11
12/// Returns the `type` wire-tag string for a realtime event variant — useful
13/// for logging, error messages, and metric labels that need a stable name
14/// without reserializing the value.
15///
16/// `async-openai` ships an equivalent `crate::traits::EventType` trait, but it
17/// is gated on the `_api` feature, which pulls reqwest / tokio / secrecy /
18/// eventsource-stream into the build. `dynamo-protocols` is types-only by
19/// design (see the Cargo.toml banner), so we mirror the trait shape locally.
20/// If `_api` ever becomes affordable for this crate, swap `pub use
21/// async_openai::traits::EventType;` in here and remove the impls below; call
22/// sites need no changes.
23///
24/// Implemented today only for `RealtimeClientEvent`. The `RealtimeServerEvent`
25/// impl can be added when a consumer needs it.
26///
27/// [NOTE] Could be replaced with a serde-introspection helper (e.g. the
28/// `serde_variant` crate) that reads the wire tag from `#[serde(rename)]`
29/// at runtime; deferred until as clean up work.
30pub trait EventType {
31 fn event_type(&self) -> &'static str;
32}
33
34impl EventType for RealtimeClientEvent {
35 fn event_type(&self) -> &'static str {
36 // `RealtimeClientEvent` is not `#[non_exhaustive]`, so a future upstream
37 // variant breaks this match at compile time rather than silently
38 // returning a stale label.
39 match self {
40 RealtimeClientEvent::SessionUpdate(_) => "session.update",
41 RealtimeClientEvent::InputAudioBufferAppend(_) => "input_audio_buffer.append",
42 RealtimeClientEvent::InputAudioBufferCommit(_) => "input_audio_buffer.commit",
43 RealtimeClientEvent::InputAudioBufferClear(_) => "input_audio_buffer.clear",
44 RealtimeClientEvent::ConversationItemCreate(_) => "conversation.item.create",
45 RealtimeClientEvent::ConversationItemRetrieve(_) => "conversation.item.retrieve",
46 RealtimeClientEvent::ConversationItemTruncate(_) => "conversation.item.truncate",
47 RealtimeClientEvent::ConversationItemDelete(_) => "conversation.item.delete",
48 RealtimeClientEvent::ResponseCreate(_) => "response.create",
49 RealtimeClientEvent::ResponseCancel(_) => "response.cancel",
50 RealtimeClientEvent::OutputAudioBufferClear(_) => "output_audio_buffer.clear",
51 }
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn session_update_accepts_null_turn_detection() {
61 let event: RealtimeClientEvent = serde_json::from_value(serde_json::json!({
62 "type": "session.update",
63 "session": {
64 "type": "transcription",
65 "audio": {
66 "input": {
67 "format": { "type": "audio/pcm", "rate": 24000 },
68 "transcription": { "model": "whisper-1" },
69 "turn_detection": null
70 }
71 }
72 }
73 }))
74 .expect("null turn_detection should be accepted");
75
76 let RealtimeClientEvent::SessionUpdate(update) = event else {
77 panic!("expected session.update");
78 };
79 let Session::RealtimeTranscriptionSession(session) = update.session else {
80 panic!("expected transcription session");
81 };
82 assert!(session.audio.input.turn_detection.is_none());
83 }
84}