Attach WebRTC realtime starts to sideband websocket (#17057)

Summary:
- parse the realtime call Location header and join that call over the
direct realtime WebSocket
- keep WebRTC starts alive on the existing realtime conversation path

Validation:
- just fmt
- git diff --check
- cargo check -p codex-api
- cargo check -p codex-core --tests
- local cargo tests not run; relying on PR CI
This commit is contained in:
Ahmed Ibrahim authored and GitHub committed 2026-04-08 15:25:42 -07:00
1 parent 19bd018300
commit 794a0240f9
7 files changed
+534 -89

No files matched your search

+50 -18
View File
@@ -40,7 +40,7 @@ use codex_api::MemorySummarizeInput as ApiMemorySummarizeInput;
use codex_api::MemorySummarizeOutput as ApiMemorySummarizeOutput;
use codex_api::RawMemory as ApiRawMemory;
use codex_api::RealtimeCallClient as ApiRealtimeCallClient;
use codex_api::RealtimeSessionConfig;
use codex_api::RealtimeSessionConfig as ApiRealtimeSessionConfig;
use codex_api::Reasoning;
use codex_api::RequestTelemetry;
use codex_api::ReqwestTransport;
@@ -83,6 +83,7 @@ use futures::StreamExt;
use http::HeaderMap as ApiHeaderMap;
use http::HeaderValue;
use http::StatusCode as HttpStatusCode;
use http::header::AUTHORIZATION;
use reqwest::StatusCode;
use std::time::Duration;
use std::time::Instant;
@@ -258,6 +259,37 @@ enum WebsocketStreamOutcome {
FallbackToHttp,
}
/// Result of opening a WebRTC Realtime call.
///
/// The SDP answer goes back to the client. The call id and auth headers stay on the server so the
/// ordinary Realtime WebSocket machinery can join the same in-progress call as a sideband
/// controller.
pub(crate) struct RealtimeWebrtcCallStart {
pub(crate) sdp: String,
pub(crate) call_id: String,
pub(crate) sideband_headers: ApiHeaderMap,
}
/// Reuses the API-auth material that created the WebRTC call for the sideband WebSocket join.
///
/// API-key sessions send that API bearer. ChatGPT-auth sessions send their bearer plus account id;
/// transceiver is responsible for accepting that same call-create identity on the direct
/// `api.openai.com` sideband path.
fn sideband_websocket_auth_headers(api_auth: &CoreAuthProvider) -> ApiHeaderMap {
let mut headers = ApiHeaderMap::new();
if let Some(token) = api_auth.token.as_ref()
&& let Ok(value) = HeaderValue::from_str(&format!("Bearer {token}"))
{
headers.insert(AUTHORIZATION, value);
}
if let Some(account_id) = api_auth.account_id.as_ref()
&& let Ok(value) = HeaderValue::from_str(account_id)
{
headers.insert("ChatGPT-Account-ID", value);
}
headers
}
impl ModelClient {
#[allow(clippy::too_many_arguments)]
/// Creates a new session-scoped `ModelClient`.
@@ -445,28 +477,28 @@ impl ModelClient {
.map_err(map_api_error)
}
pub async fn create_realtime_call(
pub(crate) async fn create_realtime_call_with_headers(
&self,
sdp: String,
session_config: RealtimeSessionConfig,
) -> Result<String> {
self.create_realtime_call_with_headers(sdp, session_config, ApiHeaderMap::new())
.await
}
pub async fn create_realtime_call_with_headers(
&self,
sdp: String,
session_config: RealtimeSessionConfig,
session_config: ApiRealtimeSessionConfig,
extra_headers: ApiHeaderMap,
) -> Result<String> {
) -> Result<RealtimeWebrtcCallStart> {
// Create the media call over HTTP first, then retain matching auth so realtime can attach
// the server-side control WebSocket to the call id from that HTTP response.
let client_setup = self.current_client_setup().await?;
let mut sideband_headers = extra_headers.clone();
sideband_headers.extend(sideband_websocket_auth_headers(&client_setup.api_auth));
let transport = ReqwestTransport::new(build_reqwest_client());
ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth)
.create_with_session_and_headers(sdp, session_config, extra_headers)
.await
.map(|response| response.sdp)
.map_err(map_api_error)
let response =
ApiRealtimeCallClient::new(transport, client_setup.api_provider, client_setup.api_auth)
.create_with_session_and_headers(sdp, session_config, extra_headers)
.await
.map_err(map_api_error)?;
Ok(RealtimeWebrtcCallStart {
sdp: response.sdp,
call_id: response.call_id,
sideband_headers,
})
}
/// Builds memory summaries for each provided normalized raw memory.
+39 -50
View File
@@ -152,12 +152,8 @@ struct RealtimeStart {
struct RealtimeStartOutput {
realtime_active: Arc<AtomicBool>,
connection: RealtimeStartConnection,
}
enum RealtimeStartConnection {
Websocket { events_rx: Receiver<RealtimeEvent> },
Webrtc { sdp: String },
events_rx: Receiver<RealtimeEvent>,
sdp: Option<String>,
}
#[allow(dead_code)]
@@ -199,31 +195,37 @@ impl RealtimeConversationManager {
RealtimeEventParser::V1 => RealtimeSessionKind::V1,
RealtimeEventParser::RealtimeV2 => RealtimeSessionKind::V2,
};
let realtime_active = Arc::new(AtomicBool::new(true));
if let Some(sdp) = sdp {
let sdp = model_client
let client = RealtimeWebsocketClient::new(api_provider);
let (connection, sdp) = if let Some(sdp) = sdp {
let call = model_client
.create_realtime_call_with_headers(
sdp,
session_config,
session_config.clone(),
extra_headers.unwrap_or_default(),
)
.await?;
return Ok(RealtimeStartOutput {
realtime_active,
connection: RealtimeStartConnection::Webrtc { sdp },
});
}
let client = RealtimeWebsocketClient::new(api_provider);
let connection = client
.connect(
session_config,
extra_headers.unwrap_or_default(),
default_headers(),
)
.await
.map_err(map_api_error)?;
let connection = client
.connect_webrtc_sideband(
session_config,
&call.call_id,
call.sideband_headers,
default_headers(),
)
.await
.map_err(map_api_error)?;
(connection, Some(call.sdp))
} else {
let connection = client
.connect(
session_config,
extra_headers.unwrap_or_default(),
default_headers(),
)
.await
.map_err(map_api_error)?;
(connection, None)
};
let writer = connection.writer();
let events = connection.events();
@@ -261,7 +263,8 @@ impl RealtimeConversationManager {
});
Ok(RealtimeStartOutput {
realtime_active,
connection: RealtimeStartConnection::Websocket { events_rx },
events_rx,
sdp,
})
}
@@ -511,11 +514,7 @@ async fn prepare_realtime_start(
let transport = params
.transport
.unwrap_or(ConversationStartTransport::Websocket);
let mut api_provider = if matches!(transport, ConversationStartTransport::Websocket) {
provider.to_api_provider(Some(AuthMode::ApiKey))?
} else {
provider.to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?
};
let mut api_provider = provider.to_api_provider(Some(AuthMode::ApiKey))?;
if let Some(realtime_ws_base_url) = &config.experimental_realtime_ws_base_url {
api_provider.base_url = realtime_ws_base_url.clone();
}
@@ -626,26 +625,16 @@ async fn handle_start_inner(
let RealtimeStartOutput {
realtime_active,
connection,
events_rx,
sdp,
} = start_output;
let events_rx = match connection {
RealtimeStartConnection::Websocket { events_rx } => events_rx,
RealtimeStartConnection::Webrtc { sdp } => {
sess.send_event_raw(Event {
id: sub_id.to_string(),
msg: EventMsg::RealtimeConversationSdp(RealtimeConversationSdpEvent { sdp }),
})
.await;
sess.conversation.finish_if_active(&realtime_active).await;
send_realtime_conversation_closed(
sess,
sub_id.to_string(),
RealtimeConversationEnd::TransportClosed,
)
.await;
return Ok(());
}
};
if let Some(sdp) = sdp {
sess.send_event_raw(Event {
id: sub_id.to_string(),
msg: EventMsg::RealtimeConversationSdp(RealtimeConversationSdpEvent { sdp }),
})
.await;
}
let sess_clone = Arc::clone(sess);
let sub_id = sub_id.to_string();