gproxy_protocol/wire/openai/realtime/
rtc.rs1use 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>;
19pub type RealtimeCallSessionJson = RealtimeSessionCreateRequest;
21
22#[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#[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 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 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}