Skip to main content

gproxy_protocol/wire/openai/realtime/
rtc.rs

1//! WebRTC call setup: Create realtime call.md. SDP answer and Location remain
2//! raw HTTP body/headers; multipart session is a direct JSON session object.
3use super::session::RealtimeSessionCreateRequest;
4use crate::{
5    WireRequest, WireResponse,
6    connection::{HttpBody, MultipartPart},
7};
8
9#[derive(Debug, gproxy_protocol_macros::WireBuilder)]
10#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
11#[derive(gproxy_protocol_macros::DeclaredFields)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13pub struct CreateRealtimeCallMultipartForm {
14    pub sdp: MultipartPart,
15    pub session: Option<RealtimeSessionCreateRequest>,
16}
17pub type CreateRealtimeCallRequest = WireRequest<CreateRealtimeCallMultipartForm>;
18pub type CreateRealtimeCallResponse = WireResponse<HttpBody>;
19/// JSON value of the multipart session part, without an extra wrapper object.
20pub type RealtimeCallSessionJson = RealtimeSessionCreateRequest;
21
22/// Frameless WebRTC setup at `POST /v1/live`. Unlike Realtime, this session
23/// has no required `type: realtime`. Source: samples/codex/codex-rs/codex-api/
24/// src/endpoint/realtime_websocket/methods_frameless_bidi.rs::session_json.
25#[derive(
26    Debug,
27    Clone,
28    PartialEq,
29    serde::Serialize,
30    serde::Deserialize,
31    gproxy_protocol_macros::WireBuilder,
32    gproxy_protocol_macros::DeclaredFields,
33)]
34#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
35#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
36pub struct LiveSessionCreateRequest {
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub model: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub instructions: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub audio: Option<super::RealtimeAudioConfig>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub delegation: Option<LiveDelegation>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub initial_items: Option<Vec<LiveInitialMessage>>,
47    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
48    pub rest: crate::Rest,
49}
50
51#[derive(
52    Debug,
53    Clone,
54    PartialEq,
55    serde::Serialize,
56    serde::Deserialize,
57    gproxy_protocol_macros::WireBuilder,
58    gproxy_protocol_macros::DeclaredFields,
59)]
60#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62pub struct LiveDelegation {
63    #[serde(rename = "type")]
64    pub type_: String,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub ack_filler: Option<bool>,
67    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
68    pub rest: crate::Rest,
69}
70
71#[derive(
72    Debug,
73    Clone,
74    PartialEq,
75    serde::Serialize,
76    serde::Deserialize,
77    gproxy_protocol_macros::WireBuilder,
78    gproxy_protocol_macros::DeclaredFields,
79)]
80#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82pub struct LiveInitialMessage {
83    #[serde(rename = "type")]
84    pub type_: String,
85    pub role: String,
86    pub content: Vec<LiveInitialText>,
87    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
88    pub rest: crate::Rest,
89}
90
91#[derive(
92    Debug,
93    Clone,
94    PartialEq,
95    serde::Serialize,
96    serde::Deserialize,
97    gproxy_protocol_macros::WireBuilder,
98    gproxy_protocol_macros::DeclaredFields,
99)]
100#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
101#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
102pub struct LiveInitialText {
103    #[serde(rename = "type")]
104    pub type_: String,
105    pub text: String,
106    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
107    pub rest: crate::Rest,
108}
109
110#[derive(Debug, gproxy_protocol_macros::WireBuilder, gproxy_protocol_macros::DeclaredFields)]
111#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113pub struct CreateLiveCallMultipartForm {
114    pub sdp: MultipartPart,
115    pub session: LiveSessionCreateRequest,
116}
117pub type CreateLiveCallRequest = WireRequest<CreateLiveCallMultipartForm>;
118pub type CreateLiveCallResponse = WireResponse<HttpBody>;
119
120/// HTTP/WS entry paths for the OpenAI realtime family. The host still owns
121/// authentication, model routing and upgrading the connection.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
124pub enum RealtimeRoute<'a> {
125    CreateCall,
126    CreateLiveCall,
127    Connect,
128    Live { call_id: Option<&'a str> },
129    HangupCall { call_id: &'a str },
130    CreateLiveSession,
131    LiveSession { session_id: Option<&'a str> },
132    HangupLiveSession { session_id: &'a str },
133}
134impl<'a> RealtimeRoute<'a> {
135    /// `/live` serves HTTP setup and a WebSocket; the method disambiguates it.
136    pub fn from_request(method: &http::Method, path: &'a str) -> Option<Self> {
137        let route = Self::from_path(path)?;
138        match (method, route) {
139            (&http::Method::POST, Self::Live { call_id: None }) => Some(Self::CreateLiveCall),
140            (&http::Method::POST, Self::CreateCall) => Some(route),
141            (&http::Method::POST, Self::LiveSession { session_id: None }) => {
142                Some(Self::CreateLiveSession)
143            }
144            (&http::Method::POST, Self::HangupCall { .. } | Self::HangupLiveSession { .. }) => {
145                Some(route)
146            }
147            (&http::Method::GET, Self::Connect | Self::Live { .. } | Self::LiveSession { .. }) => {
148                Some(route)
149            }
150            _ => None,
151        }
152    }
153    /// Match native paths, with or without the public `/v1` mount.
154    /// Path call IDs remain percent-encoded so adapters can preserve them.
155    pub fn from_path(path: &'a str) -> Option<Self> {
156        let path = path.strip_prefix("/v1/").unwrap_or(path);
157        let path = path.trim_start_matches('/').trim_end_matches('/');
158        match path {
159            "realtime/calls" => Some(Self::CreateCall),
160            "realtime" => Some(Self::Connect),
161            "live" => Some(Self::Live { call_id: None }),
162            "live/sessions" => Some(Self::LiveSession { session_id: None }),
163            _ if path.starts_with("realtime/calls/") => segment(path, "realtime/calls/", "/hangup")
164                .map(|call_id| Self::HangupCall { call_id }),
165            _ if path.starts_with("live/sessions/") => segment(path, "live/sessions/", "/attach")
166                .map(|session_id| Self::LiveSession {
167                    session_id: Some(session_id),
168                })
169                .or_else(|| {
170                    segment(path, "live/sessions/", "/hangup")
171                        .map(|session_id| Self::HangupLiveSession { session_id })
172                }),
173            _ => path
174                .strip_prefix("live/")
175                .filter(|id| !id.is_empty() && !id.contains('/') && !matches!(*id, "." | ".."))
176                .map(|id| Self::Live { call_id: Some(id) }),
177        }
178    }
179    pub fn operation(self) -> crate::OperationKey {
180        crate::OperationKey {
181            operation: match self {
182                Self::CreateCall | Self::CreateLiveCall => crate::Operation::CreateRealtimeCall,
183                Self::HangupCall { .. } => crate::Operation::HangupRealtimeCall,
184                Self::CreateLiveSession => crate::Operation::CreateLiveSession,
185                Self::LiveSession { .. } => crate::Operation::ConnectLive,
186                Self::HangupLiveSession { .. } => crate::Operation::HangupLiveSession,
187                _ => crate::Operation::ConnectRealtime,
188            },
189            dialect: crate::Dialect::OpenAi,
190        }
191    }
192}
193
194fn segment<'a>(path: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
195    path.strip_prefix(prefix)?
196        .strip_suffix(suffix)
197        .filter(|id| !id.is_empty() && !id.contains('/') && !matches!(*id, "." | ".."))
198}