@@ -12,7 +12,7 @@ use futures::{
1212 stream:: { SplitSink , SplitStream } ,
1313} ;
1414use openai_api_rs:: realtime:: {
15- api:: RealtimeClient ,
15+ api:: { RealtimeClient , RealtimeProtocol } ,
1616 client_event:: { self , ClientEvent } ,
1717 server_event:: { self , ServerEvent } ,
1818 types:: {
@@ -27,6 +27,7 @@ use tokio_tungstenite::{
2727 tungstenite:: { Bytes , protocol:: Message } ,
2828} ;
2929use tracing:: { debug, info, trace, warn} ;
30+ use url:: Url ;
3031use uuid:: Uuid ;
3132
3233use context_switch_core:: {
@@ -39,6 +40,7 @@ use context_switch_core::{
3940pub struct Params {
4041 pub api_key : String ,
4142 pub model : String ,
43+ pub protocol : Option < Protocol > ,
4244 pub host : Option < String > ,
4345 pub instructions : Option < String > ,
4446 pub voice : Option < RealtimeVoice > ,
@@ -53,6 +55,7 @@ impl Params {
5355 Self {
5456 api_key : api_key. into ( ) ,
5557 model : model. into ( ) ,
58+ protocol : None ,
5659 host : None ,
5760 instructions : None ,
5861 voice : None ,
@@ -63,6 +66,22 @@ impl Params {
6366 }
6467}
6568
69+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize ) ]
70+ #[ serde( rename_all = "lowercase" ) ]
71+ pub enum Protocol {
72+ OpenAI ,
73+ Azure ,
74+ }
75+
76+ impl Protocol {
77+ fn to_realtime_protocol ( self ) -> RealtimeProtocol {
78+ match self {
79+ Protocol :: OpenAI => RealtimeProtocol :: OpenAI ,
80+ Protocol :: Azure => RealtimeProtocol :: Azure ,
81+ }
82+ }
83+ }
84+
6685#[ derive( Debug ) ]
6786pub struct OpenAIDialog ;
6887
@@ -80,10 +99,12 @@ impl Service for OpenAIDialog {
8099 bail ! ( "Input and output audio formats must match for OpenAI dialog service" ) ;
81100 }
82101
102+ let protocol = resolve_protocol ( params. protocol , params. host . as_deref ( ) ) ?;
103+
83104 let host = if let Some ( host) = & params. host {
84- Host :: new_with_host ( host, & params. api_key , & params. model )
105+ Host :: new_with_host ( host, & params. api_key , & params. model , protocol )
85106 } else {
86- Host :: new ( & params. api_key , & params. model )
107+ Host :: new ( & params. api_key , & params. model , protocol )
87108 } ;
88109 info ! ( "Connecting to {host:?}" ) ;
89110 let mut client = host. connect ( ) . await ?;
@@ -107,6 +128,52 @@ impl Service for OpenAIDialog {
107128 }
108129}
109130
131+ fn resolve_protocol ( protocol : Option < Protocol > , host : Option < & str > ) -> Result < Protocol > {
132+ let protocol = match protocol {
133+ Some ( protocol) => Ok ( protocol) ,
134+ None => infer_protocol_from_host ( host) ,
135+ } ?;
136+
137+ validate_protocol_host ( protocol, host) ?;
138+ Ok ( protocol)
139+ }
140+
141+ fn validate_protocol_host ( protocol : Protocol , host : Option < & str > ) -> Result < ( ) > {
142+ match ( protocol, host) {
143+ ( Protocol :: Azure , None ) => {
144+ bail ! (
145+ "Protocol `azure` requires an Azure OpenAI `host` endpoint. Set `host` to your Azure OpenAI realtime websocket URL."
146+ )
147+ }
148+ ( Protocol :: Azure , Some ( _) ) => Ok ( ( ) ) ,
149+ ( Protocol :: OpenAI , _) => Ok ( ( ) ) ,
150+ }
151+ }
152+
153+ fn infer_protocol_from_host ( host : Option < & str > ) -> Result < Protocol > {
154+ let host = match host {
155+ Some ( host) => host,
156+ None => return Ok ( Protocol :: OpenAI ) ,
157+ } ;
158+
159+ let parsed = Url :: parse ( host) . with_context ( || format ! ( "Invalid host URL: {host}" ) ) ?;
160+
161+ let is_openai_realtime_endpoint = parsed. scheme ( ) == "wss"
162+ && parsed. host_str ( ) == Some ( "api.openai.com" )
163+ && parsed. path ( ) == "/v1/realtime" ;
164+
165+ if is_openai_realtime_endpoint {
166+ return Ok ( Protocol :: OpenAI ) ;
167+ }
168+
169+ match parsed. host_str ( ) {
170+ Some ( host) if host. ends_with ( ".openai.azure.com" ) => Ok ( Protocol :: Azure ) ,
171+ _ => bail ! (
172+ "Cannot infer protocol from host `{host}`. Set `protocol` explicitly to `openai` or `azure`, use `wss://api.openai.com/v1/realtime`, or use an Azure OpenAI host."
173+ ) ,
174+ }
175+ }
176+
110177#[ derive( Debug , Serialize , Deserialize ) ]
111178#[ serde( tag = "type" , rename_all = "camelCase" ) ]
112179pub enum ServiceInputEvent {
@@ -165,18 +232,24 @@ impl fmt::Debug for Host {
165232}
166233
167234impl Host {
168- pub fn new_with_host ( host : & str , api_key : & str , model : & str ) -> Self {
235+ pub fn new_with_host ( host : & str , api_key : & str , model : & str , protocol : Protocol ) -> Self {
169236 Host {
170- client : RealtimeClient :: new_with_endpoint ( host. into ( ) , api_key. into ( ) , model. into ( ) ) ,
237+ client : RealtimeClient :: new_with_endpoint_and_protocol (
238+ host. into ( ) ,
239+ api_key. into ( ) ,
240+ model. into ( ) ,
241+ protocol. to_realtime_protocol ( ) ,
242+ ) ,
171243 }
172244 }
173245
174- pub fn new ( api_key : & str , model : & str ) -> Self {
246+ pub fn new ( api_key : & str , model : & str , protocol : Protocol ) -> Self {
175247 Host {
176- client : RealtimeClient :: new_with_endpoint (
248+ client : RealtimeClient :: new_with_endpoint_and_protocol (
177249 "wss://api.openai.com/v1/realtime" . into ( ) ,
178250 api_key. into ( ) ,
179251 model. into ( ) ,
252+ protocol. to_realtime_protocol ( ) ,
180253 ) ,
181254 }
182255 }
@@ -188,23 +261,14 @@ impl Host {
188261 . await
189262 . map_err ( |e| anyhow ! ( e. to_string( ) ) ) ?;
190263
191- Ok ( Client :: new ( read, write, self . is_azure_host ( ) ) )
192- }
193-
194- fn is_azure_host ( & self ) -> bool {
195- self . client
196- . wss_url
197- . split ( '/' )
198- . nth ( 2 )
199- . map ( |host| host. ends_with ( ".openai.azure.com" ) )
200- . unwrap_or ( false )
264+ Ok ( Client :: new ( read, write, self . client . protocol ) )
201265 }
202266}
203267
204268pub struct Client {
205269 read : SplitStream < WebSocketStream < MaybeTlsStream < TcpStream > > > ,
206270 write : SplitSink < WebSocketStream < MaybeTlsStream < TcpStream > > , Message > ,
207- use_tagged_session_update : bool ,
271+ protocol : RealtimeProtocol ,
208272
209273 response_state : ResponseState ,
210274 inflight_prompt : Option < ( String , PromptRequest ) > ,
@@ -222,18 +286,32 @@ impl Client {
222286 fn new (
223287 read : SplitStream < WebSocketStream < MaybeTlsStream < TcpStream > > > ,
224288 write : SplitSink < WebSocketStream < MaybeTlsStream < TcpStream > > , Message > ,
225- use_tagged_session_update : bool ,
289+ protocol : RealtimeProtocol ,
226290 ) -> Self {
227291 Self {
228292 read,
229293 write,
230- use_tagged_session_update ,
294+ protocol ,
231295 response_state : ResponseState :: Idle ,
232296 inflight_prompt : None ,
233297 pending_prompts : Default :: default ( ) ,
234298 }
235299 }
236300
301+ fn session_update_payload (
302+ & self ,
303+ session : types:: RealtimeSession ,
304+ ) -> client_event:: SessionUpdatePayload {
305+ match self . protocol {
306+ RealtimeProtocol :: OpenAI => client_event:: SessionUpdatePayload :: Untagged (
307+ types:: UntaggedSession :: Realtime ( session) ,
308+ ) ,
309+ RealtimeProtocol :: Azure => {
310+ client_event:: SessionUpdatePayload :: Tagged ( types:: Session :: Realtime ( session) )
311+ }
312+ }
313+ }
314+
237315 /// Run an audio dialog.
238316 pub async fn dialog (
239317 & mut self ,
@@ -308,13 +386,7 @@ impl Client {
308386 }
309387
310388 if send_update {
311- let payload = if self . use_tagged_session_update {
312- client_event:: SessionUpdatePayload :: Tagged ( types:: Session :: Realtime ( session) )
313- } else {
314- client_event:: SessionUpdatePayload :: Untagged ( types:: UntaggedSession :: Realtime (
315- session,
316- ) )
317- } ;
389+ let payload = self . session_update_payload ( session) ;
318390
319391 self . send_client_event ( ClientEvent :: SessionUpdate ( client_event:: SessionUpdate {
320392 event_id : None ,
@@ -514,15 +586,7 @@ impl Client {
514586 ..Default :: default ( )
515587 } ;
516588
517- let payload = if self . use_tagged_session_update {
518- client_event:: SessionUpdatePayload :: Tagged ( types:: Session :: Realtime (
519- session,
520- ) )
521- } else {
522- client_event:: SessionUpdatePayload :: Untagged (
523- types:: UntaggedSession :: Realtime ( session) ,
524- )
525- } ;
589+ let payload = self . session_update_payload ( session) ;
526590
527591 let event = ClientEvent :: SessionUpdate ( client_event:: SessionUpdate {
528592 session : payload,
0 commit comments