1+ use crate :: api_bridge:: CoreAuthProvider ;
2+ use crate :: auth:: add_auth_headers_to_header_map;
13use crate :: endpoint:: realtime_websocket:: methods_common:: conversation_handoff_append_message;
24use crate :: endpoint:: realtime_websocket:: methods_common:: conversation_item_create_message;
35use crate :: endpoint:: realtime_websocket:: methods_common:: normalized_session_mode;
46use crate :: endpoint:: realtime_websocket:: methods_common:: session_update_session;
7+ use crate :: endpoint:: realtime_websocket:: methods_common:: webrtc_intent;
58use crate :: endpoint:: realtime_websocket:: protocol:: RealtimeAudioFrame ;
69use crate :: endpoint:: realtime_websocket:: protocol:: RealtimeEvent ;
710use crate :: endpoint:: realtime_websocket:: protocol:: RealtimeEventParser ;
@@ -25,6 +28,9 @@ use opus::Application;
2528use opus:: Channels ;
2629use opus:: Decoder as OpusDecoder ;
2730use opus:: Encoder as OpusEncoder ;
31+ use reqwest:: multipart:: Form ;
32+ use reqwest:: multipart:: Part ;
33+ use serde_json:: json;
2834use std:: collections:: HashMap ;
2935use std:: sync:: Arc ;
3036use std:: sync:: Mutex as StdMutex ;
@@ -362,12 +368,13 @@ fn append_transcript_delta(entries: &mut Vec<RealtimeTranscriptEntry>, role: &st
362368}
363369
364370pub struct RealtimeWebRtcClient {
371+ auth : CoreAuthProvider ,
365372 provider : Provider ,
366373}
367374
368375impl RealtimeWebRtcClient {
369- pub fn new ( provider : Provider ) -> Self {
370- Self { provider }
376+ pub fn new ( provider : Provider , auth : CoreAuthProvider ) -> Self {
377+ Self { auth , provider }
371378 }
372379
373380 pub async fn connect (
@@ -380,9 +387,12 @@ impl RealtimeWebRtcClient {
380387 let calls_url = calls_url_from_api_url (
381388 self . provider . base_url . as_str ( ) ,
382389 self . provider . query_params . as_ref ( ) ,
390+ config. model . as_deref ( ) ,
391+ config. event_parser ,
383392 ) ?;
384393
385394 let headers = merge_request_headers (
395+ & self . auth ,
386396 & self . provider . headers ,
387397 with_session_id_header ( extra_headers, config. session_id . as_deref ( ) ) ?,
388398 default_headers,
@@ -412,12 +422,14 @@ impl RealtimeWebRtcClient {
412422}
413423
414424fn merge_request_headers (
425+ auth : & CoreAuthProvider ,
415426 provider_headers : & HeaderMap ,
416427 extra_headers : HeaderMap ,
417428 default_headers : HeaderMap ,
418429) -> HeaderMap {
419430 let mut headers = provider_headers. clone ( ) ;
420431 headers. extend ( extra_headers) ;
432+ add_auth_headers_to_header_map ( auth, & mut headers) ;
421433 for ( name, value) in & default_headers {
422434 if let http:: header:: Entry :: Vacant ( entry) = headers. entry ( name) {
423435 entry. insert ( value. clone ( ) ) ;
@@ -445,6 +457,8 @@ fn with_session_id_header(
445457fn calls_url_from_api_url (
446458 api_url : & str ,
447459 query_params : Option < & HashMap < String , String > > ,
460+ model : Option < & str > ,
461+ event_parser : RealtimeEventParser ,
448462) -> Result < Url , ApiError > {
449463 let mut url = Url :: parse ( api_url)
450464 . map_err ( |err| ApiError :: Stream ( format ! ( "failed to parse realtime api_url: {err}" ) ) ) ?;
@@ -466,10 +480,27 @@ fn calls_url_from_api_url(
466480 }
467481 }
468482
469- if let Some ( query_params) = query_params {
483+ let intent = webrtc_intent ( event_parser) ;
484+ let has_extra_query_params = query_params. is_some_and ( |query_params| {
485+ query_params
486+ . iter ( )
487+ . any ( |( key, _) | key != "intent" && !( key == "model" && model. is_some ( ) ) )
488+ } ) ;
489+ if intent. is_some ( ) || model. is_some ( ) || has_extra_query_params {
470490 let mut query = url. query_pairs_mut ( ) ;
471- for ( key, value) in query_params {
472- query. append_pair ( key, value) ;
491+ if let Some ( intent) = intent {
492+ query. append_pair ( "intent" , intent) ;
493+ }
494+ if let Some ( model) = model {
495+ query. append_pair ( "model" , model) ;
496+ }
497+ if let Some ( query_params) = query_params {
498+ for ( key, value) in query_params {
499+ if key == "intent" || ( key == "model" && model. is_some ( ) ) {
500+ continue ;
501+ }
502+ query. append_pair ( key, value) ;
503+ }
473504 }
474505 }
475506
@@ -509,7 +540,10 @@ fn normalize_realtime_calls_path(url: &mut Url) {
509540
510541 if path. ends_with ( "/v1/" ) {
511542 url. set_path ( & format ! ( "{path}realtime/calls" ) ) ;
543+ return ;
512544 }
545+
546+ url. set_path ( & format ! ( "{}/realtime/calls" , path. trim_end_matches( '/' ) ) ) ;
513547}
514548
515549async fn connect_webrtc_transport (
@@ -814,18 +848,44 @@ async fn fetch_realtime_answer(
814848 {
815849 session_json. insert ( "model" . to_string ( ) , serde_json:: Value :: String ( model) ) ;
816850 }
817- let session_payload = serde_json:: to_string ( & session_json)
818- . map_err ( |err| ApiError :: Stream ( format ! ( "failed to encode realtime session: {err}" ) ) ) ?;
819- let form = reqwest:: multipart:: Form :: new ( )
820- . text ( "sdp" , offer_sdp)
821- . text ( "session" , session_payload) ;
822- let response = client
823- . post ( calls_url)
824- . headers ( headers)
825- . multipart ( form)
826- . send ( )
827- . await
828- . map_err ( |err| ApiError :: Stream ( format ! ( "failed to create realtime WebRTC call: {err}" ) ) ) ?;
851+ let response = if calls_url. path ( ) . ends_with ( "/v1/realtime/calls" ) {
852+ let session_payload = serde_json:: to_string ( & session_json)
853+ . map_err ( |err| ApiError :: Stream ( format ! ( "failed to encode realtime session: {err}" ) ) ) ?;
854+ let form = Form :: new ( )
855+ . part (
856+ "sdp" ,
857+ Part :: text ( offer_sdp)
858+ . mime_str ( "application/sdp" )
859+ . map_err ( |err| {
860+ ApiError :: Stream ( format ! ( "failed to encode realtime SDP part: {err}" ) )
861+ } ) ?,
862+ )
863+ . part (
864+ "session" ,
865+ Part :: text ( session_payload)
866+ . mime_str ( "application/json" )
867+ . map_err ( |err| {
868+ ApiError :: Stream ( format ! ( "failed to encode realtime session part: {err}" ) )
869+ } ) ?,
870+ ) ;
871+ client
872+ . post ( calls_url)
873+ . headers ( headers)
874+ . multipart ( form)
875+ . send ( )
876+ . await
877+ } else {
878+ client
879+ . post ( calls_url)
880+ . headers ( headers)
881+ . json ( & json ! ( {
882+ "sdp" : offer_sdp,
883+ "session" : session_json,
884+ } ) )
885+ . send ( )
886+ . await
887+ }
888+ . map_err ( |err| ApiError :: Stream ( format ! ( "failed to create realtime WebRTC call: {err}" ) ) ) ?;
829889 let status = response. status ( ) ;
830890 let body = response. text ( ) . await . map_err ( |err| {
831891 ApiError :: Stream ( format ! ( "failed to read realtime WebRTC answer SDP: {err}" ) )
@@ -893,8 +953,13 @@ mod tests {
893953 #[ test]
894954 fn calls_url_from_api_url_normalizes_http_root ( ) {
895955 let query_params = HashMap :: from ( [ ( "model" . to_string ( ) , "gpt-realtime" . to_string ( ) ) ] ) ;
896- let calls_url =
897- calls_url_from_api_url ( "http://example.com" , Some ( & query_params) ) . expect ( "calls url" ) ;
956+ let calls_url = calls_url_from_api_url (
957+ "http://example.com" ,
958+ Some ( & query_params) ,
959+ Some ( "gpt-realtime" ) ,
960+ RealtimeEventParser :: RealtimeV2 ,
961+ )
962+ . expect ( "calls url" ) ;
898963
899964 assert_eq ! (
900965 calls_url. as_str( ) ,
@@ -905,16 +970,68 @@ mod tests {
905970 #[ test]
906971 fn calls_url_from_api_url_preserves_v1_realtime_path_and_query ( ) {
907972 let query_params = HashMap :: from ( [ ( "model" . to_string ( ) , "gpt-realtime" . to_string ( ) ) ] ) ;
908- let calls_url =
909- calls_url_from_api_url ( "wss://example.com/v1/realtime?foo=bar" , Some ( & query_params) )
910- . expect ( "calls url" ) ;
973+ let calls_url = calls_url_from_api_url (
974+ "wss://example.com/v1/realtime?foo=bar" ,
975+ Some ( & query_params) ,
976+ Some ( "gpt-realtime" ) ,
977+ RealtimeEventParser :: RealtimeV2 ,
978+ )
979+ . expect ( "calls url" ) ;
911980
912981 assert_eq ! (
913982 calls_url. as_str( ) ,
914983 "https://example.com/v1/realtime/calls?foo=bar&model=gpt-realtime"
915984 ) ;
916985 }
917986
987+ #[ test]
988+ fn calls_url_from_api_url_appends_quicksilver_intent_for_v1 ( ) {
989+ let calls_url = calls_url_from_api_url (
990+ "wss://example.com/v1/realtime" ,
991+ None ,
992+ Some ( "quicksilver-test-model" ) ,
993+ RealtimeEventParser :: V1 ,
994+ )
995+ . expect ( "calls url" ) ;
996+
997+ assert_eq ! (
998+ calls_url. as_str( ) ,
999+ "https://example.com/v1/realtime/calls?intent=quicksilver&model=quicksilver-test-model"
1000+ ) ;
1001+ }
1002+
1003+ #[ test]
1004+ fn calls_url_from_api_url_omits_intent_for_v2 ( ) {
1005+ let calls_url = calls_url_from_api_url (
1006+ "wss://example.com/v1/realtime" ,
1007+ None ,
1008+ Some ( "gpt-realtime" ) ,
1009+ RealtimeEventParser :: RealtimeV2 ,
1010+ )
1011+ . expect ( "calls url" ) ;
1012+
1013+ assert_eq ! (
1014+ calls_url. as_str( ) ,
1015+ "https://example.com/v1/realtime/calls?model=gpt-realtime"
1016+ ) ;
1017+ }
1018+
1019+ #[ test]
1020+ fn calls_url_from_api_url_appends_calls_path_to_chatgpt_base_url ( ) {
1021+ let calls_url = calls_url_from_api_url (
1022+ "https://chatgpt.com/backend-api/codex" ,
1023+ None ,
1024+ Some ( "gpt-realtime" ) ,
1025+ RealtimeEventParser :: RealtimeV2 ,
1026+ )
1027+ . expect ( "calls url" ) ;
1028+
1029+ assert_eq ! (
1030+ calls_url. as_str( ) ,
1031+ "https://chatgpt.com/backend-api/codex/realtime/calls?model=gpt-realtime"
1032+ ) ;
1033+ }
1034+
9181035 #[ test]
9191036 fn parse_session_updated_event ( ) {
9201037 let payload = json ! ( {
0 commit comments