mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
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:
1 parent
19bd018300
commit
794a0240f9
7 files changed
+534
-89
No files matched your search
+50
-18
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user