fix(gateway): support current Codex Realtime live routes

This commit is contained in:
ZheFox
2026-08-23 02:51:49 +08:00
parent ea4453321d
commit 2cd20da1ec
18 changed files with 3540 additions and 254 deletions
+128 -4
View File
@@ -1,6 +1,9 @@
use std::net::SocketAddr;
use axum::body::Body;
use axum::extract::Request;
use axum::http::{header, HeaderValue, Response, StatusCode};
use axum::extract::ws::WebSocketUpgrade;
use axum::extract::{ConnectInfo, Request, State};
use axum::http::{header, HeaderMap, HeaderValue, Response, StatusCode, Uri};
use axum::routing::{any, get, post};
use axum::Router;
@@ -23,6 +26,7 @@ const AI_POST_ROUTE_PATTERNS: &[&str] = &[
"/v1/responses",
"/v1/responses/compact",
"/v1/live",
"/v1/realtime/calls",
"/v1/alpha/search",
"/v1/images/generations",
"/v1/images/edits",
@@ -65,7 +69,7 @@ pub(crate) fn mount_ai_routes(mut router: Router<AppState>) -> Router<AppState>
};
}
router = router.route("/v1/live/{call_id}", get(live_websocket));
router = router.route("/v1/realtime", get(realtime_websocket));
router = router.route("/v1/realtime", get(dispatch_realtime_websocket));
for path in CLAUDE_POST_ROUTE_PATTERNS {
router = router.route(
path,
@@ -78,6 +82,61 @@ pub(crate) fn mount_ai_routes(mut router: Router<AppState>) -> Router<AppState>
router
}
async fn dispatch_realtime_websocket(
State(state): State<AppState>,
ConnectInfo(remote_addr): ConnectInfo<SocketAddr>,
ws: WebSocketUpgrade,
headers: HeaderMap,
uri: Uri,
) -> Result<Response<Body>, GatewayError> {
if realtime_query_is_codex_live(uri.query(), &headers) {
live_websocket(State(state), ConnectInfo(remote_addr), ws, headers, uri).await
} else {
realtime_websocket(State(state), ConnectInfo(remote_addr), ws, headers, uri).await
}
}
fn realtime_query_is_codex_live(query: Option<&str>, headers: &HeaderMap) -> bool {
let mut has_call_id = false;
let mut has_live_intent = false;
let mut duplicate_or_conflicting_intent = false;
for (key, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if key.eq_ignore_ascii_case("call_id") {
has_call_id = true;
} else if key.eq_ignore_ascii_case("intent") {
if has_live_intent || !value.eq_ignore_ascii_case("quicksilver") {
duplicate_or_conflicting_intent = true;
}
has_live_intent = true;
}
}
if has_live_intent {
return !duplicate_or_conflicting_intent;
}
// `call_id` is part of the ordinary OpenAI Realtime WebRTC sideband
// contract too. Without Codex's explicit v1 intent it must remain on the
// generic Realtime handler instead of being authorized as `codex:live`.
if has_call_id {
return false;
}
// Realtime v2 has no intent selector. A malformed or conflicting intent
// must not be reclassified as v2 merely because a Codex originator is
// present.
let has_model = url::form_urlencoded::parse(query.unwrap_or_default().as_bytes())
.any(|(key, value)| key.eq_ignore_ascii_case("model") && !value.trim().is_empty());
let Some(originator) = crate::headers::header_value_str(headers, "originator") else {
return false;
};
has_model
&& originator.split_whitespace().next().is_some_and(|value| {
value.eq_ignore_ascii_case("codex_cli_rs")
|| value.to_ascii_lowercase().starts_with("codex_cli_rs/")
|| value.eq_ignore_ascii_case("codex_work_desktop")
|| value.eq_ignore_ascii_case("codex_work_web")
|| value.eq_ignore_ascii_case("codex_work_mobile")
})
}
async fn claude_method_not_allowed(request: Request) -> Result<Response<Body>, GatewayError> {
let trace_id = extract_or_generate_trace_id(request.headers());
let mut response = build_local_http_error_response_with_request_path(
@@ -133,7 +192,72 @@ pub(crate) fn admin_default_body_rules_for_signature(
#[cfg(test)]
mod tests {
use super::{admin_endpoint_signature_parts, public_api_format_local_path};
use axum::http::{HeaderMap, HeaderValue};
use super::{
admin_endpoint_signature_parts, public_api_format_local_path, realtime_query_is_codex_live,
AI_POST_ROUTE_PATTERNS,
};
#[test]
fn registers_and_dispatches_realtime_live_aliases() {
assert!(AI_POST_ROUTE_PATTERNS.contains(&"/v1/realtime/calls"));
let no_headers = HeaderMap::new();
assert!(realtime_query_is_codex_live(
Some("intent=quicksilver&call_id=rtc_opaque"),
&no_headers
));
assert!(realtime_query_is_codex_live(
Some("intent=quicksilver&c%61ll_id=rtc_encoded"),
&no_headers
));
assert!(!realtime_query_is_codex_live(
Some("c%61ll_id=rtc_encoded"),
&no_headers
));
assert!(!realtime_query_is_codex_live(None, &no_headers));
assert!(!realtime_query_is_codex_live(Some("call_id="), &no_headers));
assert!(!realtime_query_is_codex_live(
Some("call_id=%20"),
&no_headers
));
assert!(realtime_query_is_codex_live(
Some("intent=quicksilver&model=gpt-realtime-1.5"),
&no_headers
));
assert!(!realtime_query_is_codex_live(
Some("model=gpt-realtime-1.5"),
&no_headers
));
assert!(!realtime_query_is_codex_live(
Some("intent=other&model=gpt-realtime-1.5"),
&no_headers
));
assert!(!realtime_query_is_codex_live(
Some("intent=quicksilver&intent=other&model=gpt-realtime-1.5"),
&no_headers
));
let mut codex_v2_headers = HeaderMap::new();
codex_v2_headers.insert("originator", HeaderValue::from_static("codex_work_desktop"));
assert!(realtime_query_is_codex_live(
Some("model=gpt-live-1-codex"),
&codex_v2_headers
));
assert!(!realtime_query_is_codex_live(
Some("call_id=rtc_ordinary&model=gpt-live-1-codex"),
&codex_v2_headers
));
assert!(!realtime_query_is_codex_live(
Some("call_id=rtc_one&call_id=rtc_two"),
&codex_v2_headers
));
let mut ordinary_headers = HeaderMap::new();
ordinary_headers.insert("originator", HeaderValue::from_static("openai-python"));
assert!(!realtime_query_is_codex_live(
Some("model=gpt-realtime-1.5"),
&ordinary_headers
));
}
#[test]
fn supports_data_api_endpoint_signatures_and_public_paths() {
+1
View File
@@ -126,6 +126,7 @@ pub(crate) const RUST_FRONTDOOR_OWNED_ROUTE_PATTERNS: &[&str] = &[
"/v1/responses",
"/v1/responses/compact",
"/v1/realtime",
"/v1/realtime/calls",
"/v1/live",
"/v1/live/{call_id}",
"/v1/alpha/search",
+237 -25
View File
@@ -7,6 +7,7 @@ use crate::ai_serving::ApiOperation;
pub(super) fn classify_ai_public_route(
method: &http::Method,
normalized_path: &str,
query: Option<&str>,
headers: &http::HeaderMap,
) -> Option<ClassifiedRoute> {
if let Some(route) = classify_antigravity_v1internal_route(method, normalized_path) {
@@ -35,10 +36,28 @@ pub(super) fn classify_ai_public_route(
"openai:rerank",
true,
))
} else if (method == http::Method::POST && normalized_path == "/v1/live")
|| (method == http::Method::POST
&& normalized_path == "/v1/realtime/calls"
&& realtime_query_has_codex_live_intent(query))
|| (method == http::Method::GET
&& ((normalized_path == "/v1/live" || normalized_path.starts_with("/v1/live/"))
|| (normalized_path == "/v1/realtime"
&& (realtime_query_has_codex_live_intent(query)
|| realtime_query_is_codex_v2(query, headers))))
&& is_websocket_upgrade_request(headers))
{
// Codex Live has an independent wire contract and permission surface;
// it must never be authorized as an OpenAI Responses request.
Some(classified("ai_public", "codex", "live", "codex:live", true))
} else if method == http::Method::GET
&& normalized_path == "/v1/realtime"
&& is_websocket_upgrade_request(headers)
{
// Ordinary OpenAI Realtime WebSockets retain their independent
// permission surface. The GA WebRTC call-creation endpoint is not
// handled by this WebSocket-only implementation; only Codex AVAS is
// accepted above when it carries the explicit quicksilver intent.
Some(classified(
"ai_public",
"openai",
@@ -46,14 +65,6 @@ pub(super) fn classify_ai_public_route(
"openai:realtime",
true,
))
} else if (method == http::Method::POST && normalized_path == "/v1/live")
|| (method == http::Method::GET
&& (normalized_path == "/v1/live" || normalized_path.starts_with("/v1/live/"))
&& is_websocket_upgrade_request(headers))
{
// Codex Live has an independent wire contract and permission surface;
// it must never be authorized as an OpenAI Responses request.
Some(classified("ai_public", "codex", "live", "codex:live", true))
} else if (method == http::Method::POST
|| (method == http::Method::GET
&& normalized_path == "/v1/responses"
@@ -207,6 +218,56 @@ pub(super) fn classify_ai_public_route(
}
}
/// Codex's standalone realtime WebSocket is distinguished from the ordinary
/// OpenAI Realtime API by its `intent=quicksilver` selector. Keep this check
/// deliberately narrow: `call_id` is also used by ordinary OpenAI Realtime
/// sideband sockets, and neither it nor a model query may select the
/// `codex:live` permission surface on its own.
fn realtime_query_has_codex_live_intent(query: Option<&str>) -> bool {
let mut seen = false;
for (key, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if key.eq_ignore_ascii_case("intent") {
if seen || !value.eq_ignore_ascii_case("quicksilver") {
return false;
}
seen = true;
}
}
seen
}
/// Codex realtime v2 intentionally omits the v1 `intent=quicksilver` query
/// marker. Its default headers still carry the stable Codex originator, so
/// use that identity plus a model query to select the Codex Live permission
/// surface without stealing ordinary OpenAI Realtime sockets.
fn realtime_query_is_codex_v2(query: Option<&str>, headers: &http::HeaderMap) -> bool {
let mut has_model = false;
for (key, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
// V2 has no intent marker. If one is present, let the normal
// Realtime/V1 classifier handle it instead of silently treating a
// conflicting request as Codex V2.
if key.eq_ignore_ascii_case("intent") || key.eq_ignore_ascii_case("call_id") {
return false;
}
if key.eq_ignore_ascii_case("model") && !value.trim().is_empty() {
has_model = true;
}
}
if !has_model {
return false;
}
let Some(originator) = crate::headers::header_value_str(headers, "originator") else {
return false;
};
originator.split_whitespace().next().is_some_and(|value| {
value.eq_ignore_ascii_case("codex_cli_rs")
|| value.to_ascii_lowercase().starts_with("codex_cli_rs/")
|| value.eq_ignore_ascii_case("codex_work_desktop")
|| value.eq_ignore_ascii_case("codex_work_web")
|| value.eq_ignore_ascii_case("codex_work_mobile")
})
}
fn claude_request_auth_channel(headers: &http::HeaderMap) -> &'static str {
if crate::headers::header_value_str(headers, "x-api-key").is_some()
|| crate::headers::header_value_str(headers, "api-key").is_some()
@@ -296,7 +357,7 @@ mod tests {
headers.insert(CONNECTION, HeaderValue::from_static("keep-alive, Upgrade"));
headers.insert(UPGRADE, HeaderValue::from_static("websocket"));
let route = classify_ai_public_route(&Method::GET, "/v1/responses", &headers)
let route = classify_ai_public_route(&Method::GET, "/v1/responses", None, &headers)
.expect("Responses WebSocket should be an AI public route");
assert_eq!(route.route_class, "ai_public");
assert_eq!(route.route_family, "openai");
@@ -307,7 +368,8 @@ mod tests {
#[test]
fn does_not_classify_plain_get_as_responses_websocket() {
assert!(
classify_ai_public_route(&Method::GET, "/v1/responses", &HeaderMap::new()).is_none()
classify_ai_public_route(&Method::GET, "/v1/responses", None, &HeaderMap::new())
.is_none()
);
}
@@ -317,42 +379,192 @@ mod tests {
headers.insert(CONNECTION, HeaderValue::from_static("keep-alive, Upgrade"));
headers.insert(UPGRADE, HeaderValue::from_static("websocket"));
let route = classify_ai_public_route(&Method::GET, "/v1/realtime", &headers)
.expect("Realtime WebSocket should be an AI public route");
let route = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("model=gpt-realtime-2.1"),
&headers,
)
.expect("Realtime WebSocket should be an AI public route");
assert_eq!(route.route_class, "ai_public");
assert_eq!(route.route_family, "openai");
assert_eq!(route.route_kind, "realtime");
assert_eq!(route.auth_endpoint_signature, "openai:realtime");
assert!(route.execution_runtime_candidate);
assert!(
classify_ai_public_route(&Method::GET, "/v1/realtime", &HeaderMap::new()).is_none()
);
assert!(classify_ai_public_route(&Method::POST, "/v1/realtime", &headers).is_none());
let mut codex_v2_headers = headers.clone();
codex_v2_headers.insert("originator", HeaderValue::from_static("codex_work_desktop"));
let codex_v2 = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("model=gpt-live-1-codex"),
&codex_v2_headers,
)
.expect("Codex realtime v2 should use the Live route");
assert_eq!(codex_v2.route_family, "codex");
assert_eq!(codex_v2.auth_endpoint_signature, "codex:live");
let mut codex_cli_headers = headers.clone();
codex_cli_headers.insert("originator", HeaderValue::from_static("codex_cli_rs"));
let codex_cli_v2 = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("model=gpt-live-1-codex"),
&codex_cli_headers,
)
.expect("Codex CLI realtime v2 should use the Live route");
assert_eq!(codex_cli_v2.auth_endpoint_signature, "codex:live");
let mut ordinary_headers = headers.clone();
ordinary_headers.insert("originator", HeaderValue::from_static("openai-python"));
let ordinary = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("model=gpt-realtime-2.1"),
&ordinary_headers,
)
.expect("ordinary Realtime should remain available");
assert_eq!(ordinary.auth_endpoint_signature, "openai:realtime");
assert!(classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("model=gpt-realtime-2.1"),
&HeaderMap::new(),
)
.is_none());
assert!(classify_ai_public_route(&Method::POST, "/v1/realtime", None, &headers).is_none());
}
#[test]
fn classifies_live_http_and_websocket_routes_as_codex_live() {
let post = classify_ai_public_route(&Method::POST, "/v1/live", &HeaderMap::new())
.expect("Live WebRTC call creation should be an AI public route");
assert_eq!(post.route_family, "codex");
assert_eq!(post.route_kind, "live");
assert_eq!(post.auth_endpoint_signature, "codex:live");
let legacy_post =
classify_ai_public_route(&Method::POST, "/v1/live", None, &HeaderMap::new())
.expect("legacy Live call creation should be an AI public route");
assert_eq!(legacy_post.route_family, "codex");
assert_eq!(legacy_post.route_kind, "live");
assert_eq!(legacy_post.auth_endpoint_signature, "codex:live");
let avas_post = classify_ai_public_route(
&Method::POST,
"/v1/realtime/calls",
Some("intent=quicksilver&architecture=avas"),
&HeaderMap::new(),
)
.expect("Codex AVAS call creation should be an AI public route");
assert_eq!(avas_post.route_family, "codex");
assert_eq!(avas_post.route_kind, "live");
assert_eq!(avas_post.auth_endpoint_signature, "codex:live");
// The same endpoint is part of the ordinary OpenAI Realtime API, but
// Aether's OpenAI Realtime implementation currently supports direct
// WebSockets only. A request without Codex's explicit AVAS intent must
// neither be captured by the Codex Live planner nor be advertised as a
// supported ordinary call-create request.
for query in [None, Some("model=gpt-realtime"), Some("architecture=avas")] {
assert!(classify_ai_public_route(
&Method::POST,
"/v1/realtime/calls",
query,
&HeaderMap::new(),
)
.is_none());
}
for query in [
Some("intent=other&architecture=avas"),
Some("intent=quicksilver&intent=other&architecture=avas"),
] {
assert!(classify_ai_public_route(
&Method::POST,
"/v1/realtime/calls",
query,
&HeaderMap::new(),
)
.is_none());
}
let mut headers = HeaderMap::new();
headers.insert(CONNECTION, HeaderValue::from_static("Upgrade"));
headers.insert(UPGRADE, HeaderValue::from_static("websocket"));
for path in ["/v1/live", "/v1/live/rtc_opaque"] {
let route = classify_ai_public_route(&Method::GET, path, &headers)
let route = classify_ai_public_route(&Method::GET, path, None, &headers)
.expect("Live WebSocket should be an AI public route");
assert_eq!(route.route_family, "codex");
assert_eq!(route.route_kind, "live");
assert_eq!(route.auth_endpoint_signature, "codex:live");
}
assert!(
classify_ai_public_route(&Method::GET, "/v1/live/rtc_opaque", &HeaderMap::new())
.is_none()
assert!(classify_ai_public_route(
&Method::GET,
"/v1/live/rtc_opaque",
None,
&HeaderMap::new(),
)
.is_none());
let sideband = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("intent=quicksilver&call_id=rtc_opaque"),
&headers,
)
.expect("Realtime sideband WebSocket should be a Codex Live route");
assert_eq!(sideband.route_family, "codex");
assert_eq!(sideband.route_kind, "live");
assert_eq!(sideband.auth_endpoint_signature, "codex:live");
for query in [None, Some("model=gpt-realtime")] {
let route = classify_ai_public_route(&Method::GET, "/v1/realtime", query, &headers)
.expect("Realtime WebSocket without a call_id key should remain OpenAI Realtime");
assert_eq!(route.auth_endpoint_signature, "openai:realtime");
}
let codex_direct = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("intent=quicksilver&model=gpt-realtime-1.5"),
&headers,
)
.expect("Codex direct realtime WebSocket should use the Live route");
assert_eq!(codex_direct.route_family, "codex");
assert_eq!(codex_direct.route_kind, "live");
assert_eq!(codex_direct.auth_endpoint_signature, "codex:live");
for query in [
Some("intent=other&model=gpt-realtime-1.5"),
Some("intent=quicksilver&intent=other&model=gpt-realtime-1.5"),
] {
let route = classify_ai_public_route(&Method::GET, "/v1/realtime", query, &headers)
.expect("non-Codex realtime intent should remain OpenAI Realtime");
assert_eq!(route.auth_endpoint_signature, "openai:realtime");
}
// `call_id` is shared by ordinary OpenAI Realtime WebRTC sideband
// sockets. It must not select Codex Live without Codex's explicit
// `intent=quicksilver` signal.
for query in [
Some("call_id=rtc_ordinary"),
Some("c%61ll_id=rtc_encoded"),
Some("call_id="),
Some("call_id=%20"),
Some("call_id=rtc_one&call_id=rtc_two"),
Some("call_id=rtc_ordinary&model=gpt-realtime-1.5"),
] {
let route = classify_ai_public_route(&Method::GET, "/v1/realtime", query, &headers)
.expect("ordinary Realtime sideband should remain routable");
assert_eq!(route.route_family, "openai");
assert_eq!(route.route_kind, "realtime");
assert_eq!(route.auth_endpoint_signature, "openai:realtime");
}
let codex_sideband_with_encoded_call_id = classify_ai_public_route(
&Method::GET,
"/v1/realtime",
Some("intent=quicksilver&c%61ll_id=rtc_encoded"),
&headers,
)
.expect("encoded call_id must not hide Codex's explicit Live intent");
assert_eq!(
codex_sideband_with_encoded_call_id.auth_endpoint_signature,
"codex:live"
);
}
}
+1 -1
View File
@@ -212,7 +212,7 @@ pub(crate) fn classify_control_route(
.or_else(|| oauth::classify_oauth_route(method, &normalized_path))
.or_else(|| admin::classify_admin_route(method, &normalized_path))
.or_else(|| internal::classify_internal_route(method, &normalized_path))
.or_else(|| ai::classify_ai_public_route(method, &normalized_path, headers))?;
.or_else(|| ai::classify_ai_public_route(method, &normalized_path, uri.query(), headers))?;
let mut decision = classified.into_decision(normalized_path);
if let Some(signature) = decision.auth_endpoint_signature.as_deref() {
@@ -42,6 +42,7 @@ pub(crate) fn frontdoor_self_loop_public_ai_path(path: &str) -> bool {
| "/v1/responses"
| "/v1/responses/compact"
| "/v1/realtime"
| "/v1/realtime/calls"
| "/v1/live"
| "/v1/alpha/search"
| "/v1beta/files"
@@ -154,6 +155,7 @@ mod tests {
#[test]
fn realtime_is_protected_from_frontdoor_self_loops() {
assert!(frontdoor_self_loop_public_ai_path("/v1/realtime"));
assert!(frontdoor_self_loop_public_ai_path("/v1/realtime/calls"));
assert!(gateway_frontdoor_self_loop_guard_matches_with_port(
8084,
"ws://127.0.0.1:8084/v1/realtime?model=gpt-realtime"
File diff suppressed because it is too large Load Diff
@@ -22,10 +22,16 @@ use crate::execution_runtime::execute_execution_runtime_sync_plan_with_report_co
use crate::handlers::proxy::websocket::responses::ResponsesWebSocketTurnAdmission;
use crate::{AppState, GatewayError};
use super::audit::mark_live_call_create_report_context;
use super::audit::{
mark_live_call_create_report_context, LiveCallCreateAuditGuard,
LIVE_CALL_CANDIDATE_UNAVAILABLE_MESSAGE,
};
use super::live_usage_accounting_is_safe;
use super::planner::{live_call_url, plan_live_candidate, LiveAuthMode, LivePoolLeaseGuard};
use super::protocol::{build_live_multipart, extract_call_id_from_location, parse_live_multipart};
use super::protocol::{
build_live_multipart, extract_call_id_from_location, parse_live_multipart, validate_model,
validate_realtime_call_create_query, LiveRouteDialect,
};
use super::registry::{LiveCallBinding, LiveCallRegistry};
const MAX_LIVE_HTTP_BODY_BYTES: usize = 1024 * 1024;
@@ -37,36 +43,98 @@ pub(crate) async fn maybe_handle_live_http(
body: Option<&Bytes>,
remote_addr: &SocketAddr,
) -> Result<Option<Response<Body>>, GatewayError> {
if parts.method != http::Method::POST || request_context.request_path != "/v1/live" {
let Some(dialect) = LiveRouteDialect::from_call_create_path(&request_context.request_path)
else {
return Ok(None);
};
if parts.method != http::Method::POST {
return Ok(None);
}
// `/v1/realtime/calls` is shared with the ordinary OpenAI Realtime API.
// The control-plane route classifier is the authority that distinguishes
// Codex AVAS (`intent=quicksilver`) from a normal Realtime call. Do not
// let this path-only specialized hook steal an `openai:realtime` request
// after classification has deliberately kept it on the generic proxy.
if dialect == LiveRouteDialect::Realtime
&& !request_context
.control_decision
.as_ref()
.and_then(|decision| decision.auth_endpoint_signature.as_deref())
.is_some_and(|format| format.eq_ignore_ascii_case("codex:live"))
{
return Ok(None);
}
Box::pin(handle_live_http(
state,
request_context,
parts,
body,
remote_addr,
dialect,
))
.await
}
async fn handle_live_http(
state: &AppState,
request_context: &GatewayPublicRequestContext,
parts: &http::request::Parts,
body: Option<&Bytes>,
remote_addr: &SocketAddr,
dialect: LiveRouteDialect,
) -> Result<Option<Response<Body>>, GatewayError> {
let mut call_audit = LiveCallCreateAuditGuard::new(
state,
request_context.control_decision.as_ref(),
request_context.trace_id.as_str(),
request_context.request_path.as_str(),
);
if dialect == LiveRouteDialect::Realtime {
if let Err(error) = validate_realtime_call_create_query(parts.uri.query()) {
return audited_local_live_error(
&mut call_audit,
request_context,
error.status_code(),
error.client_message(),
error.code(),
);
}
}
let Some(control_decision) = request_context.control_decision.as_ref() else {
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::NOT_FOUND,
"Codex Live route is unavailable",
)?));
"route_unavailable",
);
};
if !live_usage_accounting_is_safe(control_decision) {
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::NOT_IMPLEMENTED,
"Codex Live is unavailable for finite-balance keys until Frameless usage settlement is supported",
)?));
"finite_balance_unsupported",
);
}
let Some(body) = body else {
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_REQUEST,
"Codex Live requires a multipart WebRTC offer",
)?));
"request_body_missing",
);
};
if body.len() > MAX_LIVE_HTTP_BODY_BYTES {
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::PAYLOAD_TOO_LARGE,
"Codex Live WebRTC offer exceeds the 1 MiB limit",
)?));
"request_body_too_large",
);
}
let content_type = parts
.headers
@@ -76,11 +144,13 @@ pub(crate) async fn maybe_handle_live_http(
let offer = match parse_live_multipart(content_type, body.as_ref()) {
Ok(offer) => offer,
Err(error) => {
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
error.status_code(),
error.client_message(),
)?))
"multipart_parse_failed",
)
}
};
let Some(client_model) = offer
@@ -88,29 +158,86 @@ pub(crate) async fn maybe_handle_live_http(
.get("model")
.and_then(serde_json::Value::as_str)
else {
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_REQUEST,
"Codex Live session.model must be a non-empty model identifier",
)?));
"model_missing",
);
};
if validate_model(client_model).is_err() {
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_REQUEST,
"Codex Live session.model must be a valid model identifier",
"model_invalid",
);
}
call_audit.set_validated_client_model(client_model);
let Some(mut candidate) = plan_live_candidate(
let planning = match plan_live_candidate(
state,
request_context.trace_id.as_str(),
control_decision,
&parts.headers,
remote_addr,
client_model,
dialect,
None,
)
.await?
else {
return Ok(Some(local_live_error(
.await
{
Ok(planning) => planning,
Err(error) => {
call_audit.fail(gateway_error_status(&error), "planning_failed");
return Err(error);
}
};
call_audit.set_runtime_miss(planning.runtime_miss.clone());
let runtime_miss = planning.runtime_miss;
let Some(mut candidate) = planning.candidate else {
let auth_context = control_decision.auth_context.as_ref();
warn!(
event_name = "codex_live_call_candidate_unavailable",
log_type = "ops",
trace_id = %request_context.trace_id,
transport = "webrtc",
mode = "call_create",
status_code = StatusCode::SERVICE_UNAVAILABLE.as_u16(),
client_model,
user_id = auth_context
.map(|auth| auth.user_id.as_str())
.unwrap_or("-"),
api_key_id = auth_context
.map(|auth| auth.api_key_id.as_str())
.unwrap_or("-"),
runtime_miss_reason = runtime_miss
.as_ref()
.map(|diagnostic| diagnostic.reason.as_str())
.unwrap_or("unknown"),
candidate_count = runtime_miss
.as_ref()
.and_then(|diagnostic| diagnostic.candidate_count)
.unwrap_or(0),
skipped_candidate_count = runtime_miss
.as_ref()
.and_then(|diagnostic| diagnostic.skipped_candidate_count)
.unwrap_or(0),
skip_reasons = runtime_miss
.as_ref()
.and_then(|diagnostic| diagnostic.skip_reasons_summary())
.unwrap_or_default(),
"Codex Live call creation has no eligible provider mapping"
);
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_GATEWAY,
"No eligible Codex Live provider mapping is available",
)?));
StatusCode::SERVICE_UNAVAILABLE,
LIVE_CALL_CANDIDATE_UNAVAILABLE_MESSAGE,
"candidate_unavailable",
);
};
let lease = LivePoolLeaseGuard::new(state, &candidate);
let binding = LiveCallBinding::from_candidate(&candidate);
@@ -122,20 +249,32 @@ pub(crate) async fn maybe_handle_live_http(
"model".to_string(),
serde_json::Value::String(candidate.provider_model.clone()),
);
let upstream_url = match live_call_url(&candidate) {
let upstream_url = match live_call_url(&candidate, dialect) {
Ok(url) => url,
Err(error) => {
lease.release().await;
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
error.status_code(),
error.client_message(),
)?));
"upstream_url_invalid",
);
}
};
let (provider_content_type, provider_body_base64) =
build_live_call_provider_body(candidate.auth_mode, offer.sdp.as_str(), &provider_session)?;
let (provider_content_type, provider_body_base64) = match build_live_call_provider_body(
candidate.auth_mode,
offer.sdp.as_str(),
&provider_session,
) {
Ok(body) => body,
Err(error) => {
lease.release().await;
call_audit.fail(gateway_error_status(&error), "provider_body_build_failed");
return Err(error);
}
};
// The standard plan builder requires a JSON body marker even when the exact wire body is
// carried as bytes. Keep only the mapped model here: retaining the SDP/session projection in
// the decision would unnecessarily widen the surface for future logging or report changes.
@@ -159,41 +298,84 @@ pub(crate) async fn maybe_handle_live_http(
.to_string(),
);
let Some(mut attempt) =
build_standard_sync_plan_from_decision(parts, &provider_body_marker, candidate.execution)?
else {
lease.release().await;
return Ok(Some(local_live_error(
request_context,
StatusCode::BAD_GATEWAY,
"Codex Live provider request could not be built",
)?));
let mut attempt = match build_standard_sync_plan_from_decision(
parts,
&provider_body_marker,
candidate.execution,
) {
Ok(Some(attempt)) => attempt,
Ok(None) => {
lease.release().await;
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_GATEWAY,
"Codex Live provider request could not be built",
"provider_plan_unavailable",
);
}
Err(error) => {
lease.release().await;
call_audit.fail(gateway_error_status(&error), "provider_plan_build_failed");
return Err(error);
}
};
// The synchronous SDP exchange has an ordinary request lifecycle, but it
// does not contain the media leg's token/cost usage. Keep the existing row
// while making that boundary explicit and non-billable.
mark_live_call_create_report_context(&mut attempt.report_context);
if let Some(rejection) = execution_plan_balance_capacity_rejection(
call_audit.bind_attempt(&attempt);
let balance_rejection = match execution_plan_balance_capacity_rejection(
state,
control_decision,
&attempt.plan,
attempt.report_context.as_ref(),
)
.await?
.await
{
Ok(rejection) => rejection,
Err(error) => {
lease.release().await;
call_audit.fail(
gateway_error_status(&error),
"balance_capacity_check_failed",
);
return Err(error);
}
};
if let Some(rejection) = balance_rejection {
lease.release().await;
return Ok(Some(build_local_auth_rejection_response(
let response = match build_local_auth_rejection_response(
request_context.trace_id.as_str(),
Some(control_decision),
&rejection,
)?));
) {
Ok(response) => response,
Err(error) => {
call_audit.fail(
gateway_error_status(&error),
"downstream_response_build_failed",
);
return Err(error);
}
};
call_audit.fail(response.status(), "balance_capacity_rejected");
return Ok(Some(response));
}
let admission = ResponsesWebSocketTurnAdmission::acquire(
let admission = match ResponsesWebSocketTurnAdmission::acquire(
state,
&attempt.plan,
request_context.trace_id.as_str(),
)
.await?;
.await
{
Ok(admission) => admission,
Err(error) => {
lease.release().await;
call_audit.fail(gateway_error_status(&error), "admission_failed");
return Err(error);
}
};
let result = execute_execution_runtime_sync_plan_with_report_context(
state,
Some(request_context.trace_id.as_str()),
@@ -208,9 +390,21 @@ pub(crate) async fn maybe_handle_live_http(
admission.release().await;
let pool_lease_healthy = lease.is_healthy();
lease.release().await;
let result = result?;
let result = match result {
Ok(result) => result,
Err(error) => {
call_audit.fail(gateway_error_status(&error), "upstream_execute_failed");
return Err(error);
}
};
if !(200..300).contains(&result.status_code) {
let response_body = execution_result_body(&result)?;
let response_body = match execution_result_body(&result) {
Ok(body) => body,
Err(error) => {
call_audit.fail(StatusCode::BAD_GATEWAY, "upstream_error_body_unavailable");
return Err(error);
}
};
let downstream_headers =
sanitized_live_response_headers(&result.headers, response_body.preserves_wire_encoding);
warn!(
@@ -224,13 +418,24 @@ pub(crate) async fn maybe_handle_live_http(
elapsed_ms = result.telemetry.as_ref().and_then(|value| value.elapsed_ms),
"Codex Live call creation failed upstream"
);
return Ok(Some(build_client_response_from_parts(
let response = match build_client_response_from_parts(
result.status_code,
&downstream_headers,
Body::from(response_body.bytes),
request_context.trace_id.as_str(),
Some(control_decision),
)?));
) {
Ok(response) => response,
Err(error) => {
call_audit.fail(
gateway_error_status(&error),
"downstream_response_build_failed",
);
return Err(error);
}
};
call_audit.fail(response.status(), "upstream_rejected");
return Ok(Some(response));
}
if !pool_lease_healthy {
warn_live_call_orphaned(
@@ -240,11 +445,13 @@ pub(crate) async fn maybe_handle_live_http(
"pool_lease_lost",
None,
);
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::SERVICE_UNAVAILABLE,
"Codex Live provider lease expired during call creation",
)?));
"pool_lease_lost",
);
}
let response_body = match execution_result_body(&result) {
Ok(body) => body,
@@ -256,6 +463,7 @@ pub(crate) async fn maybe_handle_live_http(
"response_body_unavailable",
None,
);
call_audit.fail(StatusCode::BAD_GATEWAY, "response_body_unavailable");
return Err(error);
}
};
@@ -267,11 +475,13 @@ pub(crate) async fn maybe_handle_live_http(
"location_missing",
None,
);
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_GATEWAY,
"Codex Live upstream response did not include a call location",
)?));
"location_missing",
);
};
let call_id = match extract_call_id_from_location(location) {
Ok(call_id) => call_id,
@@ -283,11 +493,13 @@ pub(crate) async fn maybe_handle_live_http(
"location_invalid",
Some(error.code()),
);
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::BAD_GATEWAY,
error.client_message(),
)?));
"location_invalid",
);
}
};
let Some(auth_context) = control_decision.auth_context.as_ref() else {
@@ -298,11 +510,13 @@ pub(crate) async fn maybe_handle_live_http(
"auth_context_missing",
None,
);
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::UNAUTHORIZED,
"Codex Live requires an authenticated gateway API key",
)?));
"auth_context_missing",
);
};
let registry = LiveCallRegistry::new(std::sync::Arc::clone(&state.runtime_state));
if let Err(error) = registry
@@ -321,11 +535,13 @@ pub(crate) async fn maybe_handle_live_http(
"binding_failed",
Some(error.kind()),
);
return Ok(Some(local_live_error(
return audited_local_live_error(
&mut call_audit,
request_context,
StatusCode::SERVICE_UNAVAILABLE,
"Codex Live sideband binding is temporarily unavailable",
)?));
"binding_failed",
);
}
info!(
event_name = "codex_live_call_created",
@@ -340,10 +556,10 @@ pub(crate) async fn maybe_handle_live_http(
usage_unavailable = true,
"Codex Live created a bound WebRTC call"
);
let downstream_location = format!("/v1/live/{call_id}");
let downstream_location = dialect.downstream_location(call_id.as_str());
let downstream_headers =
sanitized_live_response_headers(&result.headers, response_body.preserves_wire_encoding);
Ok(Some(build_client_response_from_parts_with_mutator(
let response = match build_client_response_from_parts_with_mutator(
result.status_code,
&downstream_headers,
Body::from(response_body.bytes),
@@ -357,7 +573,58 @@ pub(crate) async fn maybe_handle_live_http(
);
Ok(())
},
)?))
) {
Ok(response) => response,
Err(error) => {
warn_live_call_orphaned(
request_context,
&attempt.plan,
&result,
"downstream_response_build_failed",
None,
);
call_audit.fail(
gateway_error_status(&error),
"downstream_response_build_failed",
);
return Err(error);
}
};
call_audit.complete(response.status().as_u16(), "call_created");
Ok(Some(response))
}
fn gateway_error_status(error: &GatewayError) -> StatusCode {
match error {
GatewayError::UpstreamUnavailable { .. } | GatewayError::ControlUnavailable { .. } => {
StatusCode::BAD_GATEWAY
}
GatewayError::LocalExecutionPlanningTimeout { .. } => StatusCode::GATEWAY_TIMEOUT,
GatewayError::AdmissionTimeout { .. } => StatusCode::TOO_MANY_REQUESTS,
GatewayError::Client { status, .. } => *status,
GatewayError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn audited_local_live_error(
audit: &mut LiveCallCreateAuditGuard,
request_context: &GatewayPublicRequestContext,
status: StatusCode,
message: &str,
termination: &'static str,
) -> Result<Option<Response<Body>>, GatewayError> {
let response = match local_live_error(request_context, status, message) {
Ok(response) => response,
Err(error) => {
audit.fail(
gateway_error_status(&error),
"downstream_response_build_failed",
);
return Err(error);
}
};
audit.fail(response.status(), termination);
Ok(Some(response))
}
fn local_live_error(
@@ -368,7 +635,7 @@ fn local_live_error(
build_local_http_error_response_with_request_path(
request_context.trace_id.as_str(),
request_context.control_decision.as_ref(),
Some("/v1/live"),
Some(request_context.request_path.as_str()),
status,
message,
)
@@ -497,13 +764,57 @@ fn execution_result_body(result: &ExecutionResult) -> Result<LiveResponseBody, G
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use aether_contracts::{ExecutionPlan, ExecutionResult, ResponseBody};
use aether_data::repository::usage::InMemoryUsageReadRepository;
use aether_data_contracts::repository::usage::UsageReadRepository;
use axum::body::to_bytes;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
use crate::control::{GatewayControlAuthContext, GatewayControlDecision};
use super::*;
#[derive(Clone, Default)]
struct SharedLogBuffer(Arc<Mutex<Vec<u8>>>);
struct SharedLogWriter(Arc<Mutex<Vec<u8>>>);
impl SharedLogBuffer {
fn lines(&self) -> Vec<serde_json::Value> {
String::from_utf8(self.0.lock().expect("log buffer should lock").clone())
.expect("logs should be valid UTF-8")
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).expect("log line should be valid JSON"))
.collect()
}
}
impl std::io::Write for SharedLogWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.expect("log buffer should lock")
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for SharedLogBuffer {
type Writer = SharedLogWriter;
fn make_writer(&'a self) -> Self::Writer {
SharedLogWriter(Arc::clone(&self.0))
}
}
#[test]
fn preserved_wire_bytes_win_over_the_json_projection() {
let result = ExecutionResult {
@@ -680,6 +991,112 @@ mod tests {
);
}
#[test]
fn live_http_handler_future_stays_stack_bounded() {
let state = AppState::new().expect("gateway state should build");
let request_context = GatewayPublicRequestContext {
trace_id: "trace-live-future-size".to_string(),
request_method: http::Method::POST,
request_path: "/v1/live".to_string(),
request_query_string: None,
request_content_type: None,
host_header: None,
control_decision: None,
};
let (parts, _) = http::Request::builder()
.method(http::Method::POST)
.uri("/v1/live")
.body(())
.expect("request should build")
.into_parts();
let remote_addr = "127.0.0.1:65002"
.parse()
.expect("remote address should parse");
let future = maybe_handle_live_http(&state, &request_context, &parts, None, &remote_addr);
let future_size = std::mem::size_of_val(&future);
assert!(
future_size <= 4 * 1024,
"Live HTTP handler future grew to {future_size} bytes"
);
}
#[tokio::test]
async fn ordinary_openai_realtime_call_is_not_intercepted_by_codex_live() {
let decision = GatewayControlDecision::synthetic(
"/v1/realtime/calls",
Some("ai_public".to_string()),
Some("openai".to_string()),
Some("realtime".to_string()),
Some("openai:realtime".to_string()),
);
let request_context = GatewayPublicRequestContext {
trace_id: "trace-ordinary-realtime-call".to_string(),
request_method: http::Method::POST,
request_path: "/v1/realtime/calls".to_string(),
request_query_string: None,
request_content_type: Some("application/sdp".to_string()),
host_header: None,
control_decision: Some(decision),
};
let (parts, _) = http::Request::builder()
.method(http::Method::POST)
.uri("/v1/realtime/calls")
.body(())
.expect("request should build")
.into_parts();
let response = maybe_handle_live_http(
&AppState::new().expect("gateway state should build"),
&request_context,
&parts,
None,
&"127.0.0.1:65003".parse().unwrap(),
)
.await
.expect("ordinary Realtime hook check should succeed");
assert!(response.is_none());
}
#[tokio::test]
async fn codex_realtime_call_remains_on_the_live_handler() {
let decision = GatewayControlDecision::synthetic(
"/v1/realtime/calls",
Some("ai_public".to_string()),
Some("codex".to_string()),
Some("live".to_string()),
Some("codex:live".to_string()),
);
let request_context = GatewayPublicRequestContext {
trace_id: "trace-codex-realtime-call".to_string(),
request_method: http::Method::POST,
request_path: "/v1/realtime/calls".to_string(),
request_query_string: Some("intent=quicksilver&architecture=avas".to_string()),
request_content_type: Some("multipart/form-data".to_string()),
host_header: None,
control_decision: Some(decision),
};
let (parts, _) = http::Request::builder()
.method(http::Method::POST)
.uri("/v1/realtime/calls?intent=quicksilver&architecture=avas")
.body(())
.expect("request should build")
.into_parts();
let response = maybe_handle_live_http(
&AppState::new().expect("gateway state should build"),
&request_context,
&parts,
None,
&"127.0.0.1:65004".parse().unwrap(),
)
.await
.expect("Codex Realtime hook check should succeed")
.expect("Codex Realtime call must stay on the Live handler");
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn finite_balance_post_live_fails_before_parsing_or_upstream_execution() {
let mut decision = GatewayControlDecision::synthetic(
@@ -733,4 +1150,163 @@ mod tests {
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert!(String::from_utf8_lossy(body.as_ref()).contains("finite-balance"));
}
#[tokio::test]
async fn post_live_without_a_candidate_returns_service_unavailable_and_clears_diagnostic() {
let log_buffer = SharedLogBuffer::default();
let subscriber = tracing_subscriber::registry().with(
tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_current_span(false)
.with_span_list(false)
.with_writer(log_buffer.clone())
.with_filter(LevelFilter::WARN),
);
let dispatch = tracing::Dispatch::new(subscriber);
let _guard = tracing::dispatcher::set_default(&dispatch);
let mut decision = GatewayControlDecision::synthetic(
"/v1/live",
Some("ai_public".to_string()),
Some("codex".to_string()),
Some("live".to_string()),
Some("codex:live".to_string()),
);
decision.auth_context = Some(GatewayControlAuthContext {
user_id: "user-live-unmapped".to_string(),
api_key_id: "key-live-unmapped".to_string(),
username: Some("unmapped".to_string()),
api_key_name: Some("unmapped".to_string()),
balance_remaining: None,
access_allowed: true,
user_rate_limit: None,
api_key_rate_limit: None,
api_key_is_standalone: true,
admin_bypass_limits: false,
local_rejection: None,
allowed_models: None,
ip_rules: None,
});
let request_context = GatewayPublicRequestContext {
trace_id: "trace-live-unmapped".to_string(),
request_method: http::Method::POST,
request_path: "/v1/live".to_string(),
request_query_string: None,
request_content_type: Some("multipart/form-data".to_string()),
host_header: None,
control_decision: Some(decision),
};
let (content_type, body) = build_live_multipart(
"v=0\r\no=unmapped-live-offer",
&json!({"model": "gpt-live-unmapped"}),
);
let (parts, _) = http::Request::builder()
.method(http::Method::POST)
.uri("/v1/live")
.header(http::header::CONTENT_TYPE, content_type)
.body(())
.unwrap()
.into_parts();
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
let state = AppState::new()
.expect("gateway state should build")
.with_usage_data_repository_for_tests(Arc::clone(&usage_repository))
.with_usage_runtime_for_tests(crate::usage::UsageRuntimeConfig {
enabled: true,
..crate::usage::UsageRuntimeConfig::default()
});
state.set_local_execution_runtime_miss_diagnostic(
request_context.trace_id.as_str(),
crate::LocalExecutionRuntimeMissDiagnostic {
reason: "test_sentinel".to_string(),
..Default::default()
},
);
let response = maybe_handle_live_http(
&state,
&request_context,
&parts,
Some(&Bytes::from(body)),
&"127.0.0.1:65001".parse().unwrap(),
)
.await
.unwrap()
.expect("Live HTTP route must reject an unmapped model locally");
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(
response
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
Some("application/json")
);
assert_eq!(
response
.headers()
.get(crate::constants::TRACE_ID_HEADER)
.and_then(|value| value.to_str().ok()),
Some(request_context.trace_id.as_str())
);
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert!(String::from_utf8_lossy(body.as_ref())
.contains("No eligible Codex Live provider mapping is available"));
assert!(state
.take_local_execution_runtime_miss_diagnostic(request_context.trace_id.as_str())
.is_none());
let usage = tokio::time::timeout(std::time::Duration::from_secs(5), async {
loop {
if let Some(usage) = usage_repository
.find_by_request_id(request_context.trace_id.as_str())
.await
.expect("Live preflight usage read should succeed")
{
break usage;
}
tokio::task::yield_now().await;
}
})
.await
.expect("Live preflight rejection should persist a usage row before timeout");
assert_eq!(usage.status, "failed");
assert_eq!(usage.billing_status, "void");
assert_eq!(usage.status_code, Some(503));
assert_eq!(usage.request_type.as_deref(), Some("live"));
assert_eq!(usage.api_format.as_deref(), Some("codex:live"));
assert_eq!(usage.model, "gpt-live-unmapped");
assert!(!usage.is_stream);
assert!(!usage.is_websocket());
assert_eq!(usage.websocket_transport(), None);
assert!(!usage.usage_available());
assert!(!usage.usage_pricing_available());
assert_eq!(usage.input_tokens, 0);
assert_eq!(usage.output_tokens, 0);
assert_eq!(usage.total_tokens, 0);
assert_eq!(usage.total_cost_usd, 0.0);
assert_eq!(usage.actual_total_cost_usd, 0.0);
assert!(usage.request_headers.is_none());
assert!(usage.request_body.is_none());
assert!(usage.provider_request_headers.is_none());
assert!(usage.provider_request_body.is_none());
assert!(!serde_json::to_string(&usage)
.expect("usage should serialize")
.contains("unmapped-live-offer"));
let logs = log_buffer.lines();
let unavailable = logs
.iter()
.find(|entry| entry["event_name"] == "codex_live_call_candidate_unavailable")
.expect("candidate miss should emit a dedicated structured log");
assert_eq!(unavailable["status_code"], 503);
assert_eq!(unavailable["client_model"], "gpt-live-unmapped");
assert_eq!(unavailable["user_id"], "user-live-unmapped");
assert_eq!(unavailable["api_key_id"], "key-live-unmapped");
assert_eq!(unavailable["transport"], "webrtc");
assert_eq!(unavailable["mode"], "call_create");
assert!(!serde_json::to_string(&logs)
.expect("logs should serialize")
.contains("unmapped-live-offer"));
}
}
@@ -59,13 +59,25 @@ pub(crate) async fn live_websocket(
{
AuthenticatedAiWebSocketUpgradePreparation::Rejected(response) => Ok(response),
AuthenticatedAiWebSocketUpgradePreparation::Ready(prepared) => {
let live =
match session::prepare_live_websocket(prepared.state(), prepared.context()).await {
Ok(live) => live,
Err(rejection) => {
return prepared.rejection_response(rejection.status(), rejection.message())
let live = match session::prepare_live_websocket(prepared.state(), prepared.context())
.await
{
Ok(live) => live,
Err(rejection) => {
if !rejection.audit_persisted() {
audit::record_live_websocket_preflight_failure(
prepared.state(),
&prepared.context().decision,
prepared.context().trace_id.as_str(),
prepared.context().uri.path(),
prepared.context().uri.query(),
rejection.status(),
rejection.termination(),
);
}
};
return prepared.rejection_response(rejection.status(), rejection.message());
}
};
Ok(prepared.into_response_with(
ws,
LIVE_WEBSOCKET_SESSION_LIMITS,
@@ -24,11 +24,13 @@ use crate::ai_serving::{
use crate::control::GatewayControlDecision;
use crate::headers::request_origin_from_headers_and_remote_addr;
use crate::privacy::RedactionSessionSlot;
use crate::state::LocalExecutionRuntimeMissDiagnostic;
use crate::{AppState, GatewayError};
use super::protocol::{validate_model, LiveProtocolError};
use super::protocol::{validate_model, LiveProtocolError, LiveRouteDialect};
pub(super) const LIVE_ALPHA_HEADER_VALUE: &str = "quicksilver=v2";
pub(super) const LEGACY_LIVE_ALPHA_HEADER_VALUE: &str = "quicksilver=v2";
pub(super) const REALTIME_LIVE_ALPHA_HEADER_VALUE: &str = "quicksilver=v1";
const CHATGPT_ACCOUNT_ID_HEADER: &str = "chatgpt-account-id";
const CHATGPT_FEDRAMP_HEADER: &str = "x-openai-fedramp";
const CHATGPT_SESSION_ID_HEADER: &str = "x-session-id";
@@ -56,6 +58,50 @@ pub(super) struct PlannedLiveCandidate {
pub(super) routing_fingerprint: String,
}
#[derive(Debug)]
pub(super) struct LiveCandidatePlanningOutcome {
pub(super) candidate: Option<PlannedLiveCandidate>,
pub(super) runtime_miss: Option<LocalExecutionRuntimeMissDiagnostic>,
}
/// Owns the request-scoped runtime-miss entry while Live planning is in
/// progress. The ordinary HTTP proxy consumes this entry after planning, but
/// Live has three specialized call sites and pinned sideband planning can be
/// cancelled when its attachment lease is lost. Clearing from `Drop` makes
/// that cancellation path safe as well as ordinary errors and early returns.
struct LiveRuntimeMissDiagnosticGuard<'a> {
state: &'a AppState,
trace_id: &'a str,
armed: bool,
}
impl<'a> LiveRuntimeMissDiagnosticGuard<'a> {
fn new(state: &'a AppState, trace_id: &'a str) -> Self {
Self {
state,
trace_id,
armed: true,
}
}
fn take(mut self) -> Option<LocalExecutionRuntimeMissDiagnostic> {
let diagnostic = self
.state
.take_local_execution_runtime_miss_diagnostic(self.trace_id);
self.armed = false;
diagnostic
}
}
impl Drop for LiveRuntimeMissDiagnosticGuard<'_> {
fn drop(&mut self) {
if self.armed {
self.state
.clear_local_execution_runtime_miss_diagnostic(self.trace_id);
}
}
}
/// Cancellation-safe owner for the scheduler's distributed pool-key lease.
/// Live does not enter the ordinary HTTP/Responses attempt lifecycle, so it
/// must hold and release the lease explicitly for the call or socket lifetime.
@@ -148,6 +194,35 @@ pub(super) async fn plan_live_candidate(
headers: &HeaderMap,
remote_addr: &SocketAddr,
client_model: &str,
dialect: LiveRouteDialect,
pinned_candidate: Option<&ResponsesWebSocketPinnedCandidate>,
) -> Result<LiveCandidatePlanningOutcome, GatewayError> {
let diagnostic_guard = LiveRuntimeMissDiagnosticGuard::new(state, trace_id);
let candidate = plan_live_candidate_inner(
state,
trace_id,
decision,
headers,
remote_addr,
client_model,
dialect,
pinned_candidate,
)
.await?;
Ok(LiveCandidatePlanningOutcome {
candidate,
runtime_miss: diagnostic_guard.take(),
})
}
async fn plan_live_candidate_inner(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
headers: &HeaderMap,
remote_addr: &SocketAddr,
client_model: &str,
dialect: LiveRouteDialect,
pinned_candidate: Option<&ResponsesWebSocketPinnedCandidate>,
) -> Result<Option<PlannedLiveCandidate>, GatewayError> {
if validate_model(client_model).is_err() || client_model.len() > MAX_LIVE_MODEL_BYTES {
@@ -247,7 +322,7 @@ pub(super) async fn plan_live_candidate(
.await;
return Ok(None);
};
apply_live_headers(&mut execution.provider_request_headers, trace_id);
apply_live_headers(&mut execution.provider_request_headers, trace_id, dialect);
let routing_fingerprint =
match live_routing_fingerprint(&execution, effective_auth_type.as_str(), auth_mode) {
Ok(fingerprint) => fingerprint,
@@ -272,21 +347,92 @@ pub(super) async fn plan_live_candidate(
pub(super) fn direct_live_websocket_url(
candidate: &PlannedLiveCandidate,
) -> Result<String, LiveProtocolError> {
direct_live_websocket_url_for_dialect(candidate, LiveRouteDialect::LegacyLive)
}
pub(super) fn direct_live_websocket_url_for_dialect(
candidate: &PlannedLiveCandidate,
dialect: LiveRouteDialect,
) -> Result<String, LiveProtocolError> {
if candidate.auth_mode == LiveAuthMode::ChatGptOauth {
return Err(LiveProtocolError::OauthDirectWebSocketUnsupported);
}
replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["live"],
Some(("model", candidate.provider_model.as_str())),
)
match dialect {
LiveRouteDialect::LegacyLive => replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["live"],
Some(("model", candidate.provider_model.as_str())),
),
LiveRouteDialect::Realtime => {
// The endpoint is planned using the canonical codex:live `/live`
// URL. Direct Codex WebSocket clients use `/realtime` instead,
// with the quicksilver intent and mapped provider model in the
// query string. Keep endpoint-owned query parameters while
// replacing any duplicate transport selectors.
let raw = replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["realtime"],
None,
)?;
let mut url =
Url::parse(raw.as_str()).map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?;
replace_url_query_pair(&mut url, "intent", "quicksilver");
replace_url_query_pair(&mut url, "model", candidate.provider_model.as_str());
Ok(url.to_string())
}
LiveRouteDialect::RealtimeV2 => {
// Realtime v2 is the current Codex default. It intentionally
// omits the v1 quicksilver intent marker; remove any stale or
// duplicate endpoint-owned intent before adding the mapped model.
let raw = replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["realtime"],
None,
)?;
let mut url =
Url::parse(raw.as_str()).map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?;
remove_url_query_pair(&mut url, "intent");
replace_url_query_pair(&mut url, "model", candidate.provider_model.as_str());
Ok(url.to_string())
}
}
}
pub(super) fn live_call_url(candidate: &PlannedLiveCandidate) -> Result<String, LiveProtocolError> {
pub(super) fn live_call_url(
candidate: &PlannedLiveCandidate,
dialect: LiveRouteDialect,
) -> Result<String, LiveProtocolError> {
if dialect == LiveRouteDialect::RealtimeV2 {
// Codex pins AVAS/WebRTC call creation to Realtime v1. Realtime v2 is
// a direct WebSocket transport and has no call-create exchange.
return Err(LiveProtocolError::InvalidCallLocation);
}
match candidate.auth_mode {
LiveAuthMode::ApiKey => {
replace_live_suffix(candidate.execution.upstream_url.as_deref(), &["live"], None)
let raw = replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
match dialect {
LiveRouteDialect::LegacyLive => &["live"],
LiveRouteDialect::Realtime => &["realtime", "calls"],
LiveRouteDialect::RealtimeV2 => unreachable!("handled above"),
},
None,
)?;
if dialect == LiveRouteDialect::LegacyLive {
return Ok(raw);
}
// Current Codex creates AVAS calls with these two transport
// selectors. The scheduler intentionally plans against a synthetic
// body and fixed URI, so reconstruct them here rather than trusting
// arbitrary downstream query parameters. Replacement is
// case-insensitive and preserves endpoint-owned query parameters.
let mut url =
Url::parse(raw.as_str()).map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?;
replace_url_query_pair(&mut url, "intent", "quicksilver");
replace_url_query_pair(&mut url, "architecture", "avas");
Ok(url.to_string())
}
LiveAuthMode::ChatGptOauth => {
let source =
@@ -309,20 +455,77 @@ pub(super) fn live_call_url(candidate: &PlannedLiveCandidate) -> Result<String,
pub(super) fn live_sideband_url(
candidate: &PlannedLiveCandidate,
call_id: &str,
dialect: LiveRouteDialect,
) -> Result<String, LiveProtocolError> {
super::protocol::validate_call_id(call_id)?;
match candidate.auth_mode {
LiveAuthMode::ApiKey => replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["live", call_id],
None,
),
LiveAuthMode::ApiKey => match dialect {
LiveRouteDialect::LegacyLive => replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["live", call_id],
None,
),
LiveRouteDialect::Realtime => {
// AVAS sideband sockets use the same Codex quicksilver
// discriminator as the call-creation and direct websocket
// paths. Keep endpoint-owned query parameters, but replace
// any stale/duplicate transport selectors so the upstream
// receives exactly one `intent=quicksilver` and `call_id`.
let raw = replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["realtime"],
None,
)?;
let mut url =
Url::parse(raw.as_str()).map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?;
replace_url_query_pair(&mut url, "intent", "quicksilver");
replace_url_query_pair(&mut url, "call_id", call_id);
Ok(url.to_string())
}
LiveRouteDialect::RealtimeV2 => {
// V2 sideband joins (if used by a future client) carry only
// the call id; unlike V1 they must not include quicksilver.
let raw = replace_live_suffix(
candidate.execution.upstream_url.as_deref(),
&["realtime"],
None,
)?;
let mut url =
Url::parse(raw.as_str()).map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?;
remove_url_query_pair(&mut url, "intent");
replace_url_query_pair(&mut url, "call_id", call_id);
Ok(url.to_string())
}
},
// The Codex ChatGPT call creation endpoint returns an OpenAI Realtime
// call ID. Current Codex connects its sideband to this API origin even
// when call creation used the ChatGPT OAuth backend.
LiveAuthMode::ChatGptOauth => {
validated_official_chatgpt_url(candidate.execution.upstream_url.as_deref())?;
Ok(format!("https://api.openai.com/v1/live/{call_id}"))
let mut url = Url::parse("https://api.openai.com/v1/live")
.map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?;
// The sideband intentionally crosses from the official ChatGPT
// backend to the OpenAI API origin. Do not copy backend query
// parameters across that boundary; only the transport selectors
// constructed below are valid on the sideband origin.
match dialect {
LiveRouteDialect::LegacyLive => {
url.path_segments_mut()
.map_err(|_| LiveProtocolError::InvalidUpstreamUrl)?
.push(call_id);
}
LiveRouteDialect::Realtime => {
url.set_path("/v1/realtime");
replace_url_query_pair(&mut url, "intent", "quicksilver");
replace_url_query_pair(&mut url, "call_id", call_id);
}
LiveRouteDialect::RealtimeV2 => {
url.set_path("/v1/realtime");
remove_url_query_pair(&mut url, "intent");
replace_url_query_pair(&mut url, "call_id", call_id);
}
}
Ok(url.to_string())
}
}
}
@@ -330,8 +533,18 @@ pub(super) fn live_sideband_url(
pub(super) fn apply_live_headers(
headers: &mut std::collections::BTreeMap<String, String>,
seed: &str,
dialect: LiveRouteDialect,
) {
replace_header(headers, "openai-alpha", LIVE_ALPHA_HEADER_VALUE);
let alpha = match dialect {
LiveRouteDialect::LegacyLive => LEGACY_LIVE_ALPHA_HEADER_VALUE,
LiveRouteDialect::Realtime => REALTIME_LIVE_ALPHA_HEADER_VALUE,
LiveRouteDialect::RealtimeV2 => "",
};
if dialect == LiveRouteDialect::RealtimeV2 {
headers.retain(|name, _| !name.eq_ignore_ascii_case("openai-alpha"));
} else {
replace_header(headers, "openai-alpha", alpha);
}
let session_id = find_header(headers, "x-session-id")
.or_else(|| find_header(headers, "thread-id"))
.or_else(|| find_header(headers, "session-id"))
@@ -489,6 +702,11 @@ fn replace_live_suffix(
}
fn replace_url_query_pair(url: &mut Url, name: &str, value: &str) {
remove_url_query_pair(url, name);
url.query_pairs_mut().append_pair(name, value);
}
fn remove_url_query_pair(url: &mut Url, name: &str) {
let retained = url
.query_pairs()
.filter(|(candidate, _)| !candidate.eq_ignore_ascii_case(name))
@@ -499,7 +717,6 @@ fn replace_url_query_pair(url: &mut Url, name: &str, value: &str) {
for (key, value) in retained {
query.append_pair(key.as_str(), value.as_str());
}
query.append_pair(name, value);
}
fn canonical_live_route_query(url: &Url) -> String {
@@ -637,6 +854,55 @@ mod tests {
use super::*;
#[test]
fn live_runtime_miss_guard_returns_and_removes_the_diagnostic() {
let state = AppState::new().expect("gateway state should build");
let trace_id = "trace-live-planner-diagnostic";
let expected = LocalExecutionRuntimeMissDiagnostic {
reason: "no_eligible_candidate".to_string(),
candidate_count: Some(2),
..Default::default()
};
state.set_local_execution_runtime_miss_diagnostic(trace_id, expected.clone());
let diagnostic = LiveRuntimeMissDiagnosticGuard::new(&state, trace_id).take();
assert_eq!(diagnostic, Some(expected));
assert!(state
.take_local_execution_runtime_miss_diagnostic(trace_id)
.is_none());
}
#[tokio::test]
async fn cancelling_live_planning_clears_the_runtime_miss_diagnostic() {
let state = AppState::new().expect("gateway state should build");
let trace_id = "trace-live-planner-cancelled";
state.set_local_execution_runtime_miss_diagnostic(
trace_id,
LocalExecutionRuntimeMissDiagnostic {
reason: "planning_started".to_string(),
..Default::default()
},
);
let task_state = state.clone();
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
let task = tokio::spawn(async move {
let _guard = LiveRuntimeMissDiagnosticGuard::new(&task_state, trace_id);
let _ = ready_tx.send(());
std::future::pending::<()>().await;
});
ready_rx
.await
.expect("planning task should install its diagnostic guard");
task.abort();
let _ = task.await;
assert!(state
.take_local_execution_runtime_miss_diagnostic(trace_id)
.is_none());
}
fn candidate(url: &str, auth_mode: LiveAuthMode) -> PlannedLiveCandidate {
let execution: AiExecutionDecision = serde_json::from_value(json!({
"action": "stream",
@@ -794,13 +1060,166 @@ mod tests {
);
assert!(!direct.as_str().contains("global-model"));
assert_eq!(
live_call_url(&candidate).unwrap(),
live_call_url(&candidate, LiveRouteDialect::LegacyLive).unwrap(),
"https://api.example.test/v1/live?api-version=2026-08-01&model=stale&MODEL=duplicate"
);
assert_eq!(
live_sideband_url(&candidate, "rtc_abc-123").unwrap(),
live_sideband_url(&candidate, "rtc_abc-123", LiveRouteDialect::LegacyLive).unwrap(),
"https://api.example.test/v1/live/rtc_abc-123?api-version=2026-08-01&model=stale&MODEL=duplicate"
);
assert_eq!(
live_call_url(&candidate, LiveRouteDialect::Realtime).unwrap(),
"https://api.example.test/v1/realtime/calls?api-version=2026-08-01&model=stale&MODEL=duplicate&intent=quicksilver&architecture=avas"
);
assert_eq!(
live_sideband_url(&candidate, "rtc_abc-123", LiveRouteDialect::Realtime).unwrap(),
"https://api.example.test/v1/realtime?api-version=2026-08-01&model=stale&MODEL=duplicate&intent=quicksilver&call_id=rtc_abc-123"
);
}
#[test]
fn derives_direct_realtime_websocket_url_with_mapped_model_and_intent() {
let mut candidate = candidate(
"https://api.example.test/v1/live?api-version=2026-08-01&intent=stale&INTENT=duplicate&model=stale&MODEL=duplicate&deployment=primary",
LiveAuthMode::ApiKey,
);
candidate.provider_model = "upstream/model + future".to_string();
let direct = Url::parse(
direct_live_websocket_url_for_dialect(&candidate, LiveRouteDialect::Realtime)
.expect("direct Realtime URL should build")
.as_str(),
)
.expect("direct Realtime URL should parse");
assert_eq!(direct.path(), "/v1/realtime");
assert_eq!(
direct.query_pairs().collect::<Vec<_>>(),
vec![
("api-version".into(), "2026-08-01".into()),
("deployment".into(), "primary".into()),
("intent".into(), "quicksilver".into()),
("model".into(), "upstream/model + future".into()),
]
);
assert_eq!(
direct
.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("intent"))
.count(),
1
);
assert_eq!(
direct
.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("model"))
.count(),
1
);
}
#[test]
fn derives_direct_realtime_v2_websocket_url_without_intent() {
let mut candidate = candidate(
"https://api.example.test/v1/live?api-version=2026-08-01&intent=stale&INTENT=duplicate&model=stale&MODEL=duplicate&deployment=primary",
LiveAuthMode::ApiKey,
);
candidate.provider_model = "upstream/model + future".to_string();
let direct = Url::parse(
direct_live_websocket_url_for_dialect(&candidate, LiveRouteDialect::RealtimeV2)
.expect("direct Realtime v2 URL should build")
.as_str(),
)
.expect("direct Realtime v2 URL should parse");
assert_eq!(direct.path(), "/v1/realtime");
assert_eq!(
direct.query_pairs().collect::<Vec<_>>(),
vec![
("api-version".into(), "2026-08-01".into()),
("deployment".into(), "primary".into()),
("model".into(), "upstream/model + future".into()),
]
);
assert_eq!(
direct
.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("intent"))
.count(),
0
);
assert_eq!(
direct
.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("model"))
.count(),
1
);
}
#[test]
fn realtime_api_key_call_url_replaces_duplicate_transport_selectors() {
let candidate = candidate(
"https://api.example.test/v1/live?intent=stale&INTENT=duplicate&architecture=stale&ARCHITECTURE=duplicate&deployment=primary",
LiveAuthMode::ApiKey,
);
let url = Url::parse(
live_call_url(&candidate, LiveRouteDialect::Realtime)
.expect("Realtime API-key call URL should build")
.as_str(),
)
.expect("Realtime API-key call URL should parse");
assert_eq!(
url.query_pairs().collect::<Vec<_>>(),
vec![
("deployment".into(), "primary".into()),
("intent".into(), "quicksilver".into()),
("architecture".into(), "avas".into()),
]
);
assert_eq!(
url.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("intent"))
.count(),
1
);
assert_eq!(
url.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("architecture"))
.count(),
1
);
}
#[test]
fn realtime_sideband_url_replaces_duplicate_transport_selectors() {
let candidate = candidate(
"https://api.example.test/v1/live?trace=1&intent=stale&INTENT=duplicate&call_id=old&CALL_ID=duplicate",
LiveAuthMode::ApiKey,
);
let url = Url::parse(
live_sideband_url(&candidate, "rtc_current", LiveRouteDialect::Realtime)
.expect("Realtime sideband URL should build")
.as_str(),
)
.expect("Realtime sideband URL should parse");
assert_eq!(
url.query_pairs().collect::<Vec<_>>(),
vec![
("trace".into(), "1".into()),
("intent".into(), "quicksilver".into()),
("call_id".into(), "rtc_current".into()),
]
);
assert_eq!(
url.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("intent"))
.count(),
1
);
assert_eq!(
url.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("call_id"))
.count(),
1
);
}
#[test]
@@ -809,7 +1228,12 @@ mod tests {
"https://chatgpt.com/backend-api/codex/live?api-version=2026-08-01&intent=stale&INTENT=duplicate&architecture=stale&ARCHITECTURE=duplicate",
LiveAuthMode::ChatGptOauth,
);
let call = Url::parse(live_call_url(&candidate).unwrap().as_str()).unwrap();
let call = Url::parse(
live_call_url(&candidate, LiveRouteDialect::Realtime)
.unwrap()
.as_str(),
)
.unwrap();
assert_eq!(call.path(), "/backend-api/codex/realtime/calls");
assert_eq!(
call.query_pairs().collect::<Vec<_>>(),
@@ -832,15 +1256,55 @@ mod tests {
1
);
assert_eq!(
live_sideband_url(&candidate, "rtc_call_1").unwrap(),
live_sideband_url(&candidate, "rtc_call_1", LiveRouteDialect::LegacyLive).unwrap(),
"https://api.openai.com/v1/live/rtc_call_1"
);
assert_eq!(
live_sideband_url(&candidate, "rtc_call_1", LiveRouteDialect::Realtime).unwrap(),
"https://api.openai.com/v1/realtime?intent=quicksilver&call_id=rtc_call_1"
);
let oauth_sideband = Url::parse(
live_sideband_url(&candidate, "rtc_call_1", LiveRouteDialect::Realtime)
.unwrap()
.as_str(),
)
.unwrap();
assert_eq!(
oauth_sideband
.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("intent"))
.count(),
1
);
assert_eq!(
oauth_sideband
.query_pairs()
.filter(|(name, _)| name.eq_ignore_ascii_case("call_id"))
.count(),
1
);
assert_eq!(
direct_live_websocket_url(&candidate),
Err(LiveProtocolError::OauthDirectWebSocketUnsupported)
);
}
#[test]
fn chatgpt_oauth_sideband_does_not_copy_cross_origin_query() {
let candidate = candidate(
"https://chatgpt.com/backend-api/codex/live?api-version=2026-08-01&deployment=backend-only&access_token=must-not-cross-origin&X-Api-Key=must-not-cross-origin",
LiveAuthMode::ChatGptOauth,
);
assert_eq!(
live_sideband_url(&candidate, "rtc_call_1", LiveRouteDialect::LegacyLive).unwrap(),
"https://api.openai.com/v1/live/rtc_call_1"
);
assert_eq!(
live_sideband_url(&candidate, "rtc_call_1", LiveRouteDialect::Realtime).unwrap(),
"https://api.openai.com/v1/realtime?intent=quicksilver&call_id=rtc_call_1"
);
}
#[test]
fn chatgpt_oauth_live_fails_closed_for_custom_backend_origins() {
let candidate = candidate(
@@ -848,11 +1312,11 @@ mod tests {
LiveAuthMode::ChatGptOauth,
);
assert_eq!(
live_call_url(&candidate),
live_call_url(&candidate, LiveRouteDialect::Realtime),
Err(LiveProtocolError::OauthUpstreamUnsupported)
);
assert_eq!(
live_sideband_url(&candidate, "rtc_custom_backend"),
live_sideband_url(&candidate, "rtc_custom_backend", LiveRouteDialect::Realtime),
Err(LiveProtocolError::OauthUpstreamUnsupported)
);
assert_eq!(
@@ -973,15 +1437,15 @@ mod tests {
}
#[test]
fn live_headers_force_quicksilver_and_preserve_a_stable_session_identity() {
fn live_headers_force_the_route_dialect_and_preserve_a_stable_session_identity() {
let mut headers = BTreeMap::from([
("OpenAI-Alpha".to_string(), "wrong".to_string()),
("thread-id".to_string(), "thread-stable".to_string()),
]);
apply_live_headers(&mut headers, "trace-fallback");
apply_live_headers(&mut headers, "trace-fallback", LiveRouteDialect::Realtime);
assert_eq!(
headers.get("openai-alpha").map(String::as_str),
Some(LIVE_ALPHA_HEADER_VALUE)
Some(REALTIME_LIVE_ALPHA_HEADER_VALUE)
);
assert_eq!(
headers.get("x-session-id").map(String::as_str),
@@ -999,7 +1463,17 @@ mod tests {
("Session-Id".to_string(), "legacy-stable".to_string()),
("chatgpt-account-id".to_string(), "account-1".to_string()),
]);
apply_live_headers(&mut legacy_session_header, "trace-fallback");
apply_live_headers(
&mut legacy_session_header,
"trace-fallback",
LiveRouteDialect::LegacyLive,
);
assert_eq!(
legacy_session_header
.get("openai-alpha")
.map(String::as_str),
Some(LEGACY_LIVE_ALPHA_HEADER_VALUE)
);
assert_eq!(
legacy_session_header
.get("x-session-id")
@@ -1012,6 +1486,23 @@ mod tests {
.map(String::as_str),
Some("account-1")
);
let mut v2_headers = BTreeMap::from([
("OpenAI-Alpha".to_string(), "stale".to_string()),
("thread-id".to_string(), "v2-thread".to_string()),
]);
apply_live_headers(
&mut v2_headers,
"trace-fallback",
LiveRouteDialect::RealtimeV2,
);
assert!(v2_headers
.keys()
.all(|name| !name.eq_ignore_ascii_case("openai-alpha")));
assert_eq!(
v2_headers.get("x-session-id").map(String::as_str),
Some("v2-thread")
);
}
#[test]
@@ -1027,11 +1518,11 @@ mod tests {
LiveAuthMode::ApiKey,
);
assert_eq!(
live_call_url(&wrong_suffix),
live_call_url(&wrong_suffix, LiveRouteDialect::LegacyLive),
Err(LiveProtocolError::InvalidUpstreamUrl)
);
assert_eq!(
live_sideband_url(&wrong_suffix, "rtc/escape"),
live_sideband_url(&wrong_suffix, "rtc/escape", LiveRouteDialect::LegacyLive),
Err(LiveProtocolError::InvalidCallId)
);
@@ -1044,7 +1535,7 @@ mod tests {
Err(LiveProtocolError::InvalidUpstreamUrl)
);
assert_eq!(
live_call_url(&fragment),
live_call_url(&fragment, LiveRouteDialect::LegacyLive),
Err(LiveProtocolError::InvalidUpstreamUrl)
);
@@ -4,7 +4,7 @@
//! only bounded identifiers, multipart framing and the first `session.update`
//! check; it intentionally does not copy the evolving Codex event schema.
use axum::http::StatusCode;
use axum::http::{HeaderMap, StatusCode};
use serde_json::Value;
const MAX_MODEL_BYTES: usize = 256;
@@ -15,6 +15,41 @@ const MAX_SDP_BYTES: usize = 512 * 1024;
const MAX_SESSION_BYTES: usize = 256 * 1024;
const MAX_PART_HEADERS_BYTES: usize = 8 * 1024;
pub(super) const LEGACY_LIVE_CALL_PATH: &str = "/v1/live";
pub(super) const REALTIME_CALLS_PATH: &str = "/v1/realtime/calls";
pub(super) const REALTIME_SIDEBAND_PATH: &str = "/v1/realtime";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum LiveRouteDialect {
LegacyLive,
Realtime,
/// OpenAI Realtime v2, which is the current Codex default. Unlike the
/// legacy Codex realtime dialect it deliberately omits the
/// `intent=quicksilver` query selector and `openai-alpha` header.
RealtimeV2,
}
impl LiveRouteDialect {
pub(super) fn from_call_create_path(path: &str) -> Option<Self> {
match path {
LEGACY_LIVE_CALL_PATH => Some(Self::LegacyLive),
REALTIME_CALLS_PATH => Some(Self::Realtime),
_ => None,
}
}
pub(super) fn downstream_location(self, call_id: &str) -> String {
match self {
Self::LegacyLive => format!("{LEGACY_LIVE_CALL_PATH}/{call_id}"),
Self::Realtime => format!("{REALTIME_CALLS_PATH}/{call_id}"),
// V2 does not create WebRTC calls through this route today (Codex
// pins AVAS call creation to V1). Keep a valid sideband location
// for defensive callers rather than synthesising a new path.
Self::RealtimeV2 => format!("{REALTIME_SIDEBAND_PATH}?call_id={call_id}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub(super) enum LiveProtocolError {
#[error("missing Live upstream URL")]
@@ -27,6 +62,10 @@ pub(super) enum LiveProtocolError {
OauthUpstreamUnsupported,
#[error("invalid Live model query")]
InvalidModelQuery,
#[error("invalid Live websocket intent")]
InvalidLiveIntent,
#[error("invalid Live WebRTC architecture")]
InvalidLiveArchitecture,
#[error("invalid Live model")]
InvalidModel,
#[error("invalid Live call ID")]
@@ -92,6 +131,8 @@ impl LiveProtocolError {
Self::OauthDirectWebSocketUnsupported => "codex_live_oauth_direct_unsupported",
Self::OauthUpstreamUnsupported => "codex_live_oauth_upstream_unsupported",
Self::InvalidModelQuery => "codex_live_model_query_invalid",
Self::InvalidLiveIntent => "codex_live_intent_invalid",
Self::InvalidLiveArchitecture => "codex_live_architecture_invalid",
Self::InvalidModel => "codex_live_model_invalid",
Self::InvalidCallId => "codex_live_call_id_invalid",
Self::UnsupportedMediaType => "codex_live_media_type_unsupported",
@@ -129,6 +170,12 @@ impl LiveProtocolError {
Self::InvalidModelQuery => {
"Codex Live WebSocket requires exactly one model query parameter"
}
Self::InvalidLiveIntent => {
"Codex Live WebSocket requires intent=quicksilver"
}
Self::InvalidLiveArchitecture => {
"Codex Live WebRTC call creation requires architecture=avas"
}
Self::InvalidModel => {
"Codex Live model must be a non-empty identifier no longer than 256 bytes"
}
@@ -205,6 +252,42 @@ pub(super) fn validate_call_id(call_id: &str) -> Result<(), LiveProtocolError> {
Ok(())
}
/// Validate the current Codex AVAS WebRTC call-create selector.
///
/// The shared `/v1/realtime/calls` route is selected as Codex Live by its
/// unique quicksilver intent. Require the accompanying architecture here so a
/// malformed client request is rejected instead of being silently rewritten
/// into a different upstream transport contract. Unknown non-sensitive query
/// fields remain opaque for forward compatibility.
pub(super) fn validate_realtime_call_create_query(
query: Option<&str>,
) -> Result<(), LiveProtocolError> {
let mut intent_seen = false;
let mut architecture_seen = false;
for (name, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if name.eq_ignore_ascii_case("intent") {
if intent_seen || !value.eq_ignore_ascii_case("quicksilver") {
return Err(LiveProtocolError::InvalidLiveIntent);
}
intent_seen = true;
} else if name.eq_ignore_ascii_case("architecture") {
if architecture_seen || !value.eq_ignore_ascii_case("avas") {
return Err(LiveProtocolError::InvalidLiveArchitecture);
}
architecture_seen = true;
} else if live_query_parameter_is_sensitive(name.as_ref()) {
return Err(LiveProtocolError::InvalidModelQuery);
}
}
if !intent_seen {
return Err(LiveProtocolError::InvalidLiveIntent);
}
if !architecture_seen {
return Err(LiveProtocolError::InvalidLiveArchitecture);
}
Ok(())
}
fn is_live_call_id_segment(call_id: &str) -> bool {
if validate_call_id(call_id).is_err() {
return false;
@@ -245,6 +328,89 @@ pub(super) fn direct_model_from_query(query: Option<&str>) -> Result<String, Liv
model.ok_or(LiveProtocolError::InvalidModelQuery)
}
/// Parse the standalone Codex realtime WebSocket query shape.
///
/// Current Codex clients use `/v1/realtime?intent=quicksilver&model=...` for
/// the direct WebSocket transport. The same path is also used by OpenAI's
/// ordinary Realtime API, so the intent marker is part of the trust boundary:
/// callers must not be able to select the Codex Live planner merely by
/// choosing a model name.
pub(super) fn direct_realtime_model_from_query(
query: Option<&str>,
) -> Result<String, LiveProtocolError> {
let mut intent_seen = false;
for (name, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if name.eq_ignore_ascii_case("intent") {
if intent_seen || !value.eq_ignore_ascii_case("quicksilver") {
return Err(LiveProtocolError::InvalidLiveIntent);
}
intent_seen = true;
}
}
if !intent_seen {
return Err(LiveProtocolError::InvalidLiveIntent);
}
direct_model_from_query(query)
}
/// Parse the Codex Realtime v2 direct WebSocket query shape.
///
/// Realtime v2 intentionally has no `intent` marker. The caller must use the
/// Codex originator header as the additional trust-boundary discriminator
/// before invoking this parser; this function only validates the query itself.
pub(super) fn direct_realtime_v2_model_from_query(
query: Option<&str>,
) -> Result<String, LiveProtocolError> {
for (name, _) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if name.eq_ignore_ascii_case("intent") {
return Err(LiveProtocolError::InvalidLiveIntent);
}
// A call_id identifies a WebRTC sideband socket, not a standalone
// v2 conversation. Reject it here as well as at the session branch
// so this parser cannot accidentally be reused as a direct route.
if name.eq_ignore_ascii_case("call_id") {
return Err(LiveProtocolError::InvalidModelQuery);
}
}
direct_model_from_query(query)
}
/// Return whether the request carries a first-party Codex originator.
///
/// The default Codex CLI sends `codex_cli_rs` (optionally with a version),
/// while Desktop/Web/Mobile use their stable `codex_work_*` values. Keep the
/// allowlist narrow so a normal OpenAI Realtime v2 socket is not routed into
/// the Codex Live planner merely because it has a `model` query parameter.
pub(super) fn is_codex_realtime_originator(headers: &HeaderMap) -> bool {
let Some(originator) = headers
.get("originator")
.and_then(|value| value.to_str().ok())
else {
return false;
};
originator.split_whitespace().next().is_some_and(|value| {
value.eq_ignore_ascii_case("codex_cli_rs")
|| value.to_ascii_lowercase().starts_with("codex_cli_rs/")
|| value.eq_ignore_ascii_case("codex_work_desktop")
|| value.eq_ignore_ascii_case("codex_work_web")
|| value.eq_ignore_ascii_case("codex_work_mobile")
})
}
/// Query + header discriminator for a Codex Realtime v2 direct socket.
pub(super) fn realtime_v2_request_is_codex(query: Option<&str>, headers: &HeaderMap) -> bool {
let mut has_model = false;
for (name, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if name.eq_ignore_ascii_case("intent") || name.eq_ignore_ascii_case("call_id") {
return false;
}
if name.eq_ignore_ascii_case("model") && !value.trim().is_empty() {
has_model = true;
}
}
has_model && is_codex_realtime_originator(headers)
}
fn live_query_parameter_is_sensitive(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
@@ -275,6 +441,38 @@ pub(super) fn call_id_from_path(path: &str) -> Result<String, LiveProtocolError>
Ok(call_id.to_string())
}
pub(super) fn realtime_sideband_query_has_call_id(query: Option<&str>) -> bool {
url::form_urlencoded::parse(query.unwrap_or_default().as_bytes())
.any(|(name, _)| name.eq_ignore_ascii_case("call_id"))
}
pub(super) fn sideband_call_from_request(
path: &str,
query: Option<&str>,
) -> Result<(LiveRouteDialect, String), LiveProtocolError> {
if path != REALTIME_SIDEBAND_PATH {
return call_id_from_path(path).map(|call_id| (LiveRouteDialect::LegacyLive, call_id));
}
let mut call_id = None;
for (name, value) in url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()) {
if name.eq_ignore_ascii_case("call_id") {
if call_id.is_some() {
return Err(LiveProtocolError::InvalidCallId);
}
validate_call_id(value.as_ref())?;
call_id = Some(value.into_owned());
continue;
}
if live_query_parameter_is_sensitive(name.as_ref()) {
return Err(LiveProtocolError::InvalidCallId);
}
}
call_id
.map(|call_id| (LiveRouteDialect::Realtime, call_id))
.ok_or(LiveProtocolError::InvalidCallId)
}
pub(super) fn validate_initial_session_update(raw: &str) -> Result<(), LiveProtocolError> {
if raw.len() > MAX_SESSION_BYTES {
return Err(LiveProtocolError::SessionTooLarge);
@@ -376,19 +574,37 @@ pub(super) fn extract_call_id_from_location(location: &str) -> Result<String, Li
return Err(LiveProtocolError::InvalidCallLocation);
}
let path = if let Ok(url) = url::Url::parse(location) {
if url.fragment().is_some() {
return Err(LiveProtocolError::InvalidCallLocation);
}
url.path().to_string()
} else {
if !location.starts_with('/') || location.contains('#') {
return Err(LiveProtocolError::InvalidCallLocation);
}
location
.split_once('?')
.map_or(location, |(path, _)| path)
.to_string()
};
let call_id = path
.trim_end_matches('/')
.rsplit('/')
.next()
.filter(|value| !value.is_empty())
let segments = path
.split('/')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>();
let (call_id, resource) = segments
.split_last()
.ok_or(LiveProtocolError::InvalidCallLocation)?;
let valid_resource = matches!(
resource,
["v1", "live"]
| ["v1", "realtime", "calls"]
| ["v1", "realtime", "calls", "calls"]
| ["backend-api", "codex", "realtime", "calls"]
| ["backend-api", "codex", "realtime", "calls", "calls"]
);
if !valid_resource {
return Err(LiveProtocolError::InvalidCallLocation);
}
if !is_live_call_id_segment(call_id) {
return Err(LiveProtocolError::InvalidCallLocation);
}
@@ -593,6 +809,136 @@ mod tests {
);
}
#[test]
fn direct_realtime_query_requires_quicksilver_and_preserves_model_validation() {
assert_eq!(
direct_realtime_model_from_query(Some(
"intent=quicksilver&model=gpt-realtime%2Ffuture&client=codex",
))
.unwrap(),
"gpt-realtime/future"
);
assert_eq!(
direct_realtime_model_from_query(Some("INTENT=QUICKSILVER&model=gpt-live")).unwrap(),
"gpt-live"
);
for query in [
None,
Some("model=gpt-live"),
Some("intent=other&model=gpt-live"),
Some("intent=quicksilver&intent=quicksilver&model=gpt-live"),
Some("intent=quicksilver&intent=other&model=gpt-live"),
] {
assert_eq!(
direct_realtime_model_from_query(query),
Err(LiveProtocolError::InvalidLiveIntent),
"query should not select Codex Live: {query:?}"
);
}
assert_eq!(
direct_realtime_model_from_query(Some("intent=quicksilver&model=a&MODEL=b")),
Err(LiveProtocolError::InvalidModelQuery)
);
assert_eq!(
direct_realtime_model_from_query(Some(
"intent=quicksilver&model=gpt-live&token=secret"
)),
Err(LiveProtocolError::InvalidModelQuery)
);
}
#[test]
fn realtime_call_create_requires_unique_quicksilver_avas_selectors() {
assert_eq!(
validate_realtime_call_create_query(Some(
"intent=quicksilver&architecture=avas&future_hint=opaque"
)),
Ok(())
);
assert_eq!(
validate_realtime_call_create_query(Some("architecture=avas")),
Err(LiveProtocolError::InvalidLiveIntent)
);
for query in [
"intent=other&architecture=avas",
"intent=quicksilver&intent=quicksilver&architecture=avas",
] {
assert_eq!(
validate_realtime_call_create_query(Some(query)),
Err(LiveProtocolError::InvalidLiveIntent)
);
}
for query in [
"intent=quicksilver",
"intent=quicksilver&architecture=other",
"intent=quicksilver&architecture=avas&ARCHITECTURE=avas",
] {
assert_eq!(
validate_realtime_call_create_query(Some(query)),
Err(LiveProtocolError::InvalidLiveArchitecture)
);
}
assert_eq!(
validate_realtime_call_create_query(Some(
"intent=quicksilver&architecture=avas&access_token=secret"
)),
Err(LiveProtocolError::InvalidModelQuery)
);
}
#[test]
fn direct_realtime_v2_query_omits_intent_and_rejects_sideband_selectors() {
assert_eq!(
direct_realtime_v2_model_from_query(Some("model=gpt-realtime%2Ffuture&client=codex"))
.unwrap(),
"gpt-realtime/future"
);
for query in [
None,
Some("intent=quicksilver&model=gpt-live"),
Some("intent=other&model=gpt-live"),
Some("model=gpt-live&call_id=rtc_1"),
Some("model=a&MODEL=b"),
Some("model=gpt-live&token=secret"),
] {
assert!(
direct_realtime_v2_model_from_query(query).is_err(),
"v2 query should be rejected: {query:?}"
);
}
}
#[test]
fn realtime_v2_request_requires_a_codex_originator() {
let mut codex = HeaderMap::new();
codex.insert("originator", "codex_work_desktop".parse().unwrap());
assert!(realtime_v2_request_is_codex(
Some("model=gpt-realtime-1.5"),
&codex
));
codex.insert("originator", "codex_cli_rs/0.145.2".parse().unwrap());
assert!(realtime_v2_request_is_codex(
Some("model=gpt-realtime-1.5"),
&codex
));
for (query, originator) in [
("model=gpt-realtime-1.5", "openai-python"),
(
"intent=quicksilver&model=gpt-realtime-1.5",
"codex_work_desktop",
),
("model=gpt-realtime-1.5&call_id=rtc_1", "codex_work_desktop"),
("model=gpt-realtime-1.5", ""),
] {
let mut headers = HeaderMap::new();
if !originator.is_empty() {
headers.insert("originator", originator.parse().unwrap());
}
assert!(!realtime_v2_request_is_codex(Some(query), &headers));
}
}
#[test]
fn direct_protocol_starts_with_session_update_not_response_create() {
let opaque = r#"{"type":"session.update","session":{"future_capability":{"version":2}},"future_event_field":[1,2,3]}"#;
@@ -733,6 +1079,9 @@ mod tests {
for location in [
"https://api.openai.com/v1/live/rtc_abc-123",
"/v1/live/550e8400-e29b-41d4-a716-446655440000",
"/v1/realtime/calls/rtc_current",
"https://chatgpt.com/backend-api/codex/realtime/calls/rtc_backend",
"/v1/realtime/calls/calls/rtc_forwarded",
] {
assert!(extract_call_id_from_location(location).is_ok());
}
@@ -740,7 +1089,13 @@ mod tests {
extract_call_id_from_location("/v1/live/rtc%2Fescape"),
Err(LiveProtocolError::InvalidCallLocation)
);
for location in ["/v1/live", "/v1/live/not-a-call-id"] {
for location in [
"/v1/live",
"/v1/live/not-a-call-id",
"/unrelated/rtc_opaque",
"?call_id=rtc_query_only",
"/v1/realtime/calls/rtc_valid#fragment",
] {
assert_eq!(
extract_call_id_from_location(location),
Err(LiveProtocolError::InvalidCallLocation)
@@ -754,6 +1109,52 @@ mod tests {
}
}
#[test]
fn recognizes_call_create_and_sideband_route_dialects() {
assert_eq!(
LiveRouteDialect::from_call_create_path("/v1/live"),
Some(LiveRouteDialect::LegacyLive)
);
assert_eq!(
LiveRouteDialect::from_call_create_path("/v1/realtime/calls"),
Some(LiveRouteDialect::Realtime)
);
assert_eq!(
LiveRouteDialect::from_call_create_path("/v1/realtime"),
None
);
assert_eq!(
sideband_call_from_request("/v1/live/rtc_legacy", None),
Ok((LiveRouteDialect::LegacyLive, "rtc_legacy".to_string()))
);
assert_eq!(
sideband_call_from_request(
"/v1/realtime",
Some("intent=quicksilver&call_id=rtc_current")
),
Ok((LiveRouteDialect::Realtime, "rtc_current".to_string()))
);
}
#[test]
fn realtime_sideband_query_rejects_ambiguous_or_sensitive_call_ids() {
assert!(realtime_sideband_query_has_call_id(Some("call_id=")));
assert!(realtime_sideband_query_has_call_id(Some(
"CALL_ID=rtc_one&call_id=rtc_two"
)));
for query in [
"call_id=",
"call_id=rtc_one&call_id=rtc_two",
"call_id=rtc_valid&token=secret",
"model=gpt-realtime",
] {
assert_eq!(
sideband_call_from_request("/v1/realtime", Some(query)),
Err(LiveProtocolError::InvalidCallId)
);
}
}
#[test]
fn opaque_event_discriminator_does_not_project_unknown_fields() {
let raw = r#"{"type":"delegation.created","unknown":{"nested":[1,2,3]}}"#;
@@ -26,18 +26,21 @@ use crate::handlers::proxy::websocket::transport::{
upstream_message_to_client, websocket_relay_frame_queue, UpstreamWebSocketErrorCodes,
WebSocketRelayPumpControl, WebSocketRelayQueueError, WebSocketWriteError,
};
use crate::{AppState, GatewayError};
use crate::{AppState, GatewayError, LocalExecutionRuntimeMissDiagnostic};
use super::audit::{
LiveAuditTransport, LiveSessionAudit, LiveSessionDisposition, LiveSessionTerminal,
};
use super::live_usage_accounting_is_safe;
use super::planner::{
build_live_stream_admission_attempt, direct_live_websocket_url, live_sideband_url,
plan_live_candidate, LivePoolLeaseGuard, PlannedLiveCandidate,
build_live_stream_admission_attempt, direct_live_websocket_url_for_dialect, live_sideband_url,
plan_live_candidate, LiveCandidatePlanningOutcome, LivePoolLeaseGuard, PlannedLiveCandidate,
};
use super::protocol::{
call_id_from_path, direct_model_from_query, event_type, validate_initial_session_update,
direct_model_from_query, direct_realtime_model_from_query, direct_realtime_v2_model_from_query,
event_type, realtime_sideband_query_has_call_id, realtime_v2_request_is_codex,
sideband_call_from_request, validate_initial_session_update, LiveRouteDialect,
LEGACY_LIVE_CALL_PATH, REALTIME_SIDEBAND_PATH,
};
use super::registry::{
LiveCallBinding, LiveCallLookup, LiveCallRegistry, LiveCallRegistryError, LiveSidebandLease,
@@ -158,7 +161,9 @@ pub(super) struct PreparedLiveSideband {
pub(super) struct LiveWebSocketPreflightRejection {
status: StatusCode,
termination: &'static str,
message: &'static str,
audit_persisted: bool,
}
impl LiveWebSocketPreflightRejection {
@@ -169,6 +174,14 @@ impl LiveWebSocketPreflightRejection {
pub(super) const fn message(&self) -> &'static str {
self.message
}
pub(super) const fn termination(&self) -> &'static str {
self.termination
}
pub(super) const fn audit_persisted(&self) -> bool {
self.audit_persisted
}
}
pub(super) async fn prepare_live_websocket(
@@ -193,7 +206,7 @@ pub(super) async fn prepare_live_websocket(
"Codex Live is unavailable for finite-balance keys until Frameless usage settlement is supported",
));
}
if context.uri.path() == "/v1/live" {
if context.uri.path() == LEGACY_LIVE_CALL_PATH {
let client_model = match direct_model_from_query(context.uri.query()) {
Ok(model) => model,
Err(error) => {
@@ -206,22 +219,70 @@ pub(super) async fn prepare_live_websocket(
));
}
};
return prepare_direct_live_websocket(state, context, client_model.as_str())
return prepare_direct_live_websocket(
state,
context,
client_model.as_str(),
LiveRouteDialect::LegacyLive,
)
.await
.map(PreparedLiveWebSocket::Direct);
}
// Codex Realtime V1 uses the OpenAI `/v1/realtime` path with
// `intent=quicksilver&model=...`; the current default V2 omits `intent`
// and is identified by the first-party Codex originator header. A
// call_id on that path is a WebRTC sideband attachment and must continue
// through the registry branch below.
if context.uri.path() == REALTIME_SIDEBAND_PATH
&& !realtime_sideband_query_has_call_id(context.uri.query())
{
let (client_model, dialect) = match direct_realtime_model_from_query(context.uri.query()) {
Ok(model) => (model, LiveRouteDialect::Realtime),
Err(_v1_error)
if realtime_v2_request_is_codex(context.uri.query(), &context.headers) =>
{
match direct_realtime_v2_model_from_query(context.uri.query()) {
Ok(model) => (model, LiveRouteDialect::RealtimeV2),
Err(error) => {
return Err(preflight_rejection(
context,
"direct",
error.status_code(),
error.code(),
error.client_message(),
));
}
}
}
Err(error) => {
// Preserve the V1 error (notably InvalidLiveIntent) for
// ordinary Realtime callers and malformed Codex V1 queries.
return Err(preflight_rejection(
context,
"direct",
error.status_code(),
error.code(),
error.client_message(),
));
}
};
return prepare_direct_live_websocket(state, context, client_model.as_str(), dialect)
.await
.map(PreparedLiveWebSocket::Direct);
}
let call_id = match call_id_from_path(context.uri.path()) {
Ok(call_id) => call_id,
Err(error) => {
return Err(preflight_rejection(
context,
"sideband",
error.status_code(),
error.code(),
error.client_message(),
));
}
};
let (dialect, call_id) =
match sideband_call_from_request(context.uri.path(), context.uri.query()) {
Ok(sideband) => sideband,
Err(error) => {
return Err(preflight_rejection(
context,
"sideband",
error.status_code(),
error.code(),
error.client_message(),
));
}
};
let Some(auth) = context.decision.auth_context.as_ref() else {
return Err(preflight_rejection(
context,
@@ -299,7 +360,7 @@ pub(super) async fn prepare_live_websocket(
));
}
};
prepare_sideband_live_websocket(state, context, call_id, binding)
prepare_sideband_live_websocket(state, context, call_id, binding, dialect)
.await
.map(PreparedLiveWebSocket::Sideband)
}
@@ -308,6 +369,7 @@ async fn prepare_direct_live_websocket(
state: &AppState,
context: &WebSocketRequestContext,
client_model: &str,
dialect: LiveRouteDialect,
) -> Result<PreparedLiveRelay, LiveWebSocketPreflightRejection> {
let started_at = Instant::now();
let candidate = match plan_live_candidate(
@@ -317,12 +379,20 @@ async fn prepare_direct_live_websocket(
&context.headers,
&context.remote_addr,
client_model,
dialect,
None,
)
.await
{
Ok(Some(candidate)) => candidate,
Ok(None) => {
Ok(LiveCandidatePlanningOutcome {
candidate: Some(candidate),
..
}) => candidate,
Ok(LiveCandidatePlanningOutcome {
candidate: None,
runtime_miss,
}) => {
log_live_candidate_unavailable(context, "direct", client_model, runtime_miss.as_ref());
return Err(preflight_rejection(
context,
"direct",
@@ -353,7 +423,7 @@ async fn prepare_direct_live_websocket(
}
};
let pool_lease = LivePoolLeaseGuard::new(state, &candidate);
let upstream_url = match direct_live_websocket_url(&candidate) {
let upstream_url = match direct_live_websocket_url_for_dialect(&candidate, dialect) {
Ok(url) => url,
Err(error) => {
pool_lease.release().await;
@@ -488,6 +558,7 @@ async fn prepare_sideband_live_websocket(
context: &WebSocketRequestContext,
call_id: String,
binding: LiveCallBinding,
dialect: LiveRouteDialect,
) -> Result<PreparedLiveSideband, LiveWebSocketPreflightRejection> {
let started_at = Instant::now();
let Some(auth) = context.decision.auth_context.as_ref() else {
@@ -562,6 +633,7 @@ async fn prepare_sideband_live_websocket(
&context.headers,
&context.remote_addr,
binding.client_model(),
dialect,
Some(binding.pinned_candidate()),
),
)
@@ -577,8 +649,14 @@ async fn prepare_sideband_live_websocket(
sideband_loss_message(loss),
));
}
Ok(Ok(Some(candidate))) if binding.matches_candidate(&candidate) => candidate,
Ok(Ok(Some(candidate))) => {
Ok(Ok(LiveCandidatePlanningOutcome {
candidate: Some(candidate),
..
})) if binding.matches_candidate(&candidate) => candidate,
Ok(Ok(LiveCandidatePlanningOutcome {
candidate: Some(candidate),
..
})) => {
crate::orchestration::release_pool_key_lease_from_report_context(
state,
candidate.execution.report_context.as_ref(),
@@ -593,7 +671,16 @@ async fn prepare_sideband_live_websocket(
"Codex Live call provider binding is no longer valid",
));
}
Ok(Ok(None)) => {
Ok(Ok(LiveCandidatePlanningOutcome {
candidate: None,
runtime_miss,
})) => {
log_live_candidate_unavailable(
context,
"sideband",
binding.client_model(),
runtime_miss.as_ref(),
);
release_sideband_lease(&mut sideband_lease, context).await;
return Err(preflight_rejection(
context,
@@ -625,7 +712,7 @@ async fn prepare_sideband_live_websocket(
}
};
let pool_lease = LivePoolLeaseGuard::new(state, &candidate);
let upstream_url = match live_sideband_url(&candidate, call_id.as_str()) {
let upstream_url = match live_sideband_url(&candidate, call_id.as_str(), dialect) {
Ok(url) => url,
Err(error) => {
release_sideband_lease(&mut sideband_lease, context).await;
@@ -1058,6 +1145,7 @@ async fn audited_preflight_rejection(
started_at: Instant,
audit: Option<LiveSessionAudit>,
) -> LiveWebSocketPreflightRejection {
let audit_persisted = audit.is_some();
if let Some(audit) = audit {
audit
.finish(
@@ -1066,7 +1154,9 @@ async fn audited_preflight_rejection(
)
.await;
}
preflight_rejection(context, mode, status, termination, message)
let mut rejection = preflight_rejection(context, mode, status, termination, message);
rejection.audit_persisted = audit_persisted;
rejection
}
fn preflight_rejection(
@@ -1088,7 +1178,43 @@ fn preflight_rejection(
termination,
"Codex Live WebSocket preflight rejected the HTTP upgrade"
);
LiveWebSocketPreflightRejection { status, message }
LiveWebSocketPreflightRejection {
status,
termination,
message,
audit_persisted: false,
}
}
fn log_live_candidate_unavailable(
context: &WebSocketRequestContext,
mode: &'static str,
client_model: &str,
runtime_miss: Option<&LocalExecutionRuntimeMissDiagnostic>,
) {
warn!(
target: LIVE_LOG_TARGET,
event_name = "codex_live_candidate_unavailable",
log_type = "ops",
transport = WEBSOCKET_LOG_TRANSPORT,
websocket = true,
trace_id = %context.trace_id,
mode,
client_model,
runtime_miss_reason = runtime_miss
.map(|diagnostic| diagnostic.reason.as_str())
.unwrap_or("unknown"),
candidate_count = runtime_miss
.and_then(|diagnostic| diagnostic.candidate_count)
.unwrap_or(0),
skipped_candidate_count = runtime_miss
.and_then(|diagnostic| diagnostic.skipped_candidate_count)
.unwrap_or(0),
skip_reasons = runtime_miss
.and_then(|diagnostic| diagnostic.skip_reasons_summary())
.unwrap_or_default(),
"Codex Live request has no eligible provider mapping"
);
}
fn gateway_error_kind(error: &GatewayError) -> &'static str {
+247 -34
View File
@@ -1708,14 +1708,22 @@ async fn receive_realtime_message(socket: &mut wreq::ws::WebSocket) -> WreqWsMes
}
#[test]
fn gateway_creates_bound_codex_live_oauth_calls_with_opaque_session_fields() {
fn gateway_creates_bound_codex_live_oauth_calls_with_legacy_responses_mapping() {
super::run_frontdoor_async_test(
"codex-live-oauth-frontdoor",
run_codex_live_oauth_frontdoor_scenario(),
run_codex_live_oauth_frontdoor_scenario(CodexLiveWebRtcTestDialect::LegacyLive),
);
}
async fn run_codex_live_oauth_frontdoor_scenario() {
#[test]
fn gateway_creates_bound_codex_realtime_oauth_calls_with_legacy_responses_mapping() {
super::run_frontdoor_async_test(
"codex-realtime-oauth-frontdoor",
run_codex_live_oauth_frontdoor_scenario(CodexLiveWebRtcTestDialect::Realtime),
);
}
async fn run_codex_live_oauth_frontdoor_scenario(dialect: CodexLiveWebRtcTestDialect) {
const PROVIDER_ID: &str = "provider-codex-live";
const ENDPOINT_ID: &str = "endpoint-provider-codex-live";
const UPSTREAM_KEY_ID: &str = "key-provider-codex-live";
@@ -1724,6 +1732,15 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
const CALL_ID: &str = "rtc_frontdoor_live";
let mut row = sample_codex_live_candidate_row(PROVIDER_ID, CLIENT_MODEL, PROVIDER_MODEL);
// Existing Codex associations predate the dedicated Live format. The
// provider-aware compatibility rule must carry their Responses-scoped
// source mapping through the complete scheduler, not only the repository
// fast path.
if let Some(mappings) = row.model_provider_model_mappings.as_mut() {
for mapping in mappings {
mapping.api_formats = Some(vec!["openai:responses".to_string()]);
}
}
row.key_allowed_models = Some(vec![PROVIDER_MODEL.to_string()]);
let candidate_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -1755,6 +1772,8 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
vec![endpoint],
vec![upstream_key],
));
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
let captured_plan = Arc::new(Mutex::new(None::<aether_contracts::ExecutionPlan>));
let captured_plan_for_runtime = Arc::clone(&captured_plan);
@@ -1804,13 +1823,19 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let state = build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(
crate::data::GatewayDataState::with_minimal_candidate_selection_and_auth_for_tests(
candidate_repository,
crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_request_candidates_and_usage_for_tests(
auth_repository,
)
.attach_provider_catalog_repository_for_tests(provider_catalog_repository)
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
);
candidate_repository,
provider_catalog_repository,
request_candidate_repository,
Arc::clone(&usage_repository),
DEVELOPMENT_ENCRYPTION_KEY,
),
)
.with_usage_runtime_for_tests(crate::usage::UsageRuntimeConfig {
enabled: true,
..crate::usage::UsageRuntimeConfig::default()
});
let gateway = build_router_with_state(state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
@@ -1829,7 +1854,10 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
session
);
let response = reqwest::Client::new()
.post(format!("{gateway_url}/v1/live"))
.post(format!(
"{gateway_url}{}",
dialect.call_create_request_target()
))
.header("authorization", "Bearer sk-codex-live")
.header(
"content-type",
@@ -1842,7 +1870,7 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
.await
.expect("Codex Live call creation should complete");
assert_eq!(response.status(), StatusCode::CREATED);
let downstream_location = format!("/v1/live/{CALL_ID}");
let downstream_location = dialect.call_location(CALL_ID);
assert_eq!(
response
.headers()
@@ -1896,7 +1924,7 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
assert_eq!(provider_body["session"]["instructions"], "Keep this opaque");
assert_eq!(
plan.headers.get("openai-alpha").map(String::as_str),
Some("quicksilver=v2")
Some(dialect.alpha_header())
);
assert_eq!(
plan.headers.get("originator").map(String::as_str),
@@ -1918,6 +1946,49 @@ async fn run_codex_live_oauth_frontdoor_scenario() {
assert_eq!(plan.headers.get("thread-id"), Some(converged_session));
uuid::Uuid::parse_str(converged_session).expect("converged session ID must be a UUID");
let call_create_usage = tokio::time::timeout(std::time::Duration::from_secs(5), async {
loop {
if let Some(usage) = usage_repository
.find_by_request_id(plan.request_id.as_str())
.await
.expect("Live call-create usage read should succeed")
{
break usage;
}
tokio::task::yield_now().await;
}
})
.await
.expect("Live call-create usage should be persisted before timeout");
assert_eq!(call_create_usage.status, "completed");
assert_eq!(call_create_usage.billing_status, "void");
assert_eq!(call_create_usage.status_code, Some(201));
assert_eq!(call_create_usage.request_type.as_deref(), Some("live"));
assert_eq!(call_create_usage.api_format.as_deref(), Some("codex:live"));
assert_eq!(call_create_usage.model, CLIENT_MODEL);
assert!(!call_create_usage.is_stream);
assert!(!call_create_usage.is_websocket());
assert_eq!(call_create_usage.websocket_transport(), None);
assert!(!call_create_usage.usage_available());
assert!(!call_create_usage.usage_pricing_available());
assert_eq!(call_create_usage.total_tokens, 0);
assert_eq!(call_create_usage.total_cost_usd, 0.0);
assert_eq!(call_create_usage.actual_total_cost_usd, 0.0);
assert!(call_create_usage.request_headers.is_none());
assert!(call_create_usage.request_body.is_none());
assert!(call_create_usage.provider_request_headers.is_none());
assert!(call_create_usage.provider_request_body.is_none());
let serialized_call_create_usage =
serde_json::to_string(&call_create_usage).expect("Live call-create usage should serialize");
for sentinel in [
offer_sdp,
"Keep this opaque",
"sk-codex-live",
"oauth-upstream-secret",
] {
assert!(!serialized_call_create_usage.contains(sentinel));
}
gateway_handle.abort();
execution_runtime_handle.abort();
}
@@ -1927,20 +1998,78 @@ struct ObservedCodexLiveWebSocket {
request_target: String,
authorization: Option<String>,
alpha: Option<String>,
originator: Option<String>,
session_id: Option<String>,
initial_event: serde_json::Value,
event_after_turn_done: serde_json::Value,
}
#[derive(Debug, Clone, Copy)]
enum CodexLiveDirectTestDialect {
LegacyLive,
RealtimeV1,
RealtimeV2,
}
impl CodexLiveDirectTestDialect {
fn public_path(self) -> &'static str {
match self {
Self::LegacyLive => "/v1/live",
Self::RealtimeV1 | Self::RealtimeV2 => "/v1/realtime",
}
}
fn query_prefix(self) -> &'static str {
match self {
Self::RealtimeV1 => "intent=quicksilver&",
Self::LegacyLive | Self::RealtimeV2 => "",
}
}
fn expected_upstream_target(self, provider_model: &str) -> String {
match self {
Self::LegacyLive => format!("/v1/live?model={provider_model}"),
Self::RealtimeV1 => {
format!("/v1/realtime?intent=quicksilver&model={provider_model}")
}
Self::RealtimeV2 => format!("/v1/realtime?model={provider_model}"),
}
}
fn expected_alpha(self) -> Option<&'static str> {
match self {
Self::LegacyLive => Some("quicksilver=v2"),
Self::RealtimeV1 => Some("quicksilver=v1"),
Self::RealtimeV2 => None,
}
}
}
#[test]
fn gateway_relays_codex_live_api_key_websocket_opaquely() {
super::run_frontdoor_async_test(
"codex-live-api-key-websocket-frontdoor",
run_codex_live_api_key_websocket_frontdoor_scenario(),
run_codex_live_api_key_websocket_frontdoor_scenario(CodexLiveDirectTestDialect::LegacyLive),
);
}
async fn run_codex_live_api_key_websocket_frontdoor_scenario() {
#[test]
fn gateway_relays_codex_realtime_v2_api_key_websocket_opaquely() {
super::run_frontdoor_async_test(
"codex-realtime-v2-api-key-websocket-frontdoor",
run_codex_live_api_key_websocket_frontdoor_scenario(CodexLiveDirectTestDialect::RealtimeV2),
);
}
#[test]
fn gateway_relays_codex_realtime_v1_api_key_websocket_opaquely() {
super::run_frontdoor_async_test(
"codex-realtime-v1-api-key-websocket-frontdoor",
run_codex_live_api_key_websocket_frontdoor_scenario(CodexLiveDirectTestDialect::RealtimeV1),
);
}
async fn run_codex_live_api_key_websocket_frontdoor_scenario(dialect: CodexLiveDirectTestDialect) {
const PROVIDER_ID: &str = "provider-codex-live-api-key";
const ENDPOINT_ID: &str = "endpoint-provider-codex-live-api-key";
const UPSTREAM_KEY_ID: &str = "key-provider-codex-live-api-key";
@@ -1951,6 +2080,7 @@ async fn run_codex_live_api_key_websocket_frontdoor_scenario() {
let upstream_state = Arc::new(Mutex::new(Some(observed_tx)));
let upstream = Router::new()
.route("/v1/live", get(mock_codex_live_websocket))
.route("/v1/realtime", get(mock_codex_live_websocket))
.with_state(upstream_state);
let (upstream_url, upstream_handle) = start_server(upstream).await;
@@ -2014,10 +2144,18 @@ async fn run_codex_live_api_key_websocket_frontdoor_scenario() {
http::HeaderName::from_static("openai-alpha"),
http::HeaderValue::from_static("client-value-must-be-replaced"),
);
if matches!(dialect, CodexLiveDirectTestDialect::RealtimeV2) {
handshake_headers.insert(
http::HeaderName::from_static("originator"),
http::HeaderValue::from_static("codex_work_desktop"),
);
}
let invalid_model_response = wreq::Client::new()
.websocket(format!(
"{}/v1/live?model={CLIENT_MODEL}&model=second-model",
gateway_url.replacen("http://", "ws://", 1)
"{}{}?{}model={CLIENT_MODEL}&model=second-model",
gateway_url.replacen("http://", "ws://", 1),
dialect.public_path(),
dialect.query_prefix(),
))
.headers(handshake_headers.clone())
.send()
@@ -2026,8 +2164,10 @@ async fn run_codex_live_api_key_websocket_frontdoor_scenario() {
assert_eq!(invalid_model_response.status(), StatusCode::BAD_REQUEST);
let websocket_url = format!(
"{}/v1/live?foo=bar&model={CLIENT_MODEL}&trace=1",
gateway_url.replacen("http://", "ws://", 1)
"{}{}?{}foo=bar&model={CLIENT_MODEL}&trace=1",
gateway_url.replacen("http://", "ws://", 1),
dialect.public_path(),
dialect.query_prefix(),
);
let response = wreq::Client::new()
.websocket(websocket_url)
@@ -2091,13 +2231,17 @@ async fn run_codex_live_api_key_websocket_frontdoor_scenario() {
.expect("mock upstream observation channel should remain open");
assert_eq!(
observed.request_target,
format!("/v1/live?model={PROVIDER_MODEL}")
dialect.expected_upstream_target(PROVIDER_MODEL)
);
assert_eq!(
observed.authorization.as_deref(),
Some("Bearer oauth-upstream-secret")
);
assert_eq!(observed.alpha.as_deref(), Some("quicksilver=v2"));
assert_eq!(observed.alpha.as_deref(), dialect.expected_alpha());
assert_eq!(
observed.originator.as_deref(),
matches!(dialect, CodexLiveDirectTestDialect::RealtimeV2).then_some("codex_work_desktop")
);
assert_eq!(observed.session_id.as_deref(), Some("stable-live-session"));
let mut expected_initial_event = initial_event;
expected_initial_event["session"]["model"] = json!(PROVIDER_MODEL);
@@ -2119,15 +2263,63 @@ struct ObservedCodexLiveSideband {
session_update: serde_json::Value,
}
#[derive(Debug, Clone, Copy)]
enum CodexLiveWebRtcTestDialect {
LegacyLive,
Realtime,
}
impl CodexLiveWebRtcTestDialect {
fn call_create_path(self) -> &'static str {
match self {
Self::LegacyLive => "/v1/live",
Self::Realtime => "/v1/realtime/calls",
}
}
fn call_location(self, call_id: &str) -> String {
format!("{}/{call_id}", self.call_create_path())
}
fn call_create_request_target(self) -> &'static str {
match self {
Self::LegacyLive => "/v1/live",
Self::Realtime => "/v1/realtime/calls?intent=quicksilver&architecture=avas",
}
}
fn alpha_header(self) -> &'static str {
match self {
Self::LegacyLive => "quicksilver=v2",
Self::Realtime => "quicksilver=v1",
}
}
fn sideband_path(self, call_id: &str) -> String {
match self {
Self::LegacyLive => format!("/v1/live/{call_id}"),
Self::Realtime => format!("/v1/realtime?intent=quicksilver&call_id={call_id}"),
}
}
}
#[test]
fn gateway_creates_and_relays_bound_codex_live_api_key_sideband() {
super::run_frontdoor_async_test(
"codex-live-api-key-sideband-frontdoor",
run_codex_live_api_key_sideband_frontdoor_scenario(),
run_codex_live_api_key_sideband_frontdoor_scenario(CodexLiveWebRtcTestDialect::LegacyLive),
);
}
async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
#[test]
fn gateway_creates_and_relays_bound_codex_live_api_key_realtime_calls_sideband() {
super::run_frontdoor_async_test(
"codex-live-api-key-realtime-calls-sideband-frontdoor",
run_codex_live_api_key_sideband_frontdoor_scenario(CodexLiveWebRtcTestDialect::Realtime),
);
}
async fn run_codex_live_api_key_sideband_frontdoor_scenario(dialect: CodexLiveWebRtcTestDialect) {
const PROVIDER_ID: &str = "provider-codex-live-sideband";
const ENDPOINT_ID: &str = "endpoint-provider-codex-live-sideband";
const UPSTREAM_KEY_ID: &str = "key-provider-codex-live-sideband";
@@ -2142,12 +2334,13 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
"/v1/live/{call_id}",
get(mock_codex_live_sideband_websocket),
)
.route("/v1/realtime", get(mock_codex_live_sideband_websocket))
.with_state(upstream_state);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let captured_plan = Arc::new(Mutex::new(None::<aether_contracts::ExecutionPlan>));
let captured_plan_for_runtime = Arc::clone(&captured_plan);
let upstream_location = format!("{upstream_url}/v1/live/{CALL_ID}");
let upstream_location = format!("{upstream_url}{}", dialect.call_location(CALL_ID));
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |request: Request| {
@@ -2237,9 +2430,11 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
let gateway = build_router_with_state(state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let sideband_path = dialect.sideband_path(CALL_ID);
let sideband_url = format!(
"{}/v1/live/{CALL_ID}",
gateway_url.replacen("http://", "ws://", 1)
"{}{}",
gateway_url.replacen("http://", "ws://", 1),
sideband_path
);
let mut sideband_headers = HeaderMap::new();
sideband_headers.insert(
@@ -2273,7 +2468,10 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
session
);
let response = reqwest::Client::new()
.post(format!("{gateway_url}/v1/live"))
.post(format!(
"{gateway_url}{}",
dialect.call_create_request_target()
))
.header("authorization", "Bearer sk-codex-live-sideband")
.header(
"content-type",
@@ -2291,7 +2489,7 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
.headers()
.get(http::header::LOCATION)
.and_then(|value| value.to_str().ok()),
Some(format!("/v1/live/{CALL_ID}").as_str())
Some(dialect.call_location(CALL_ID).as_str())
);
assert_eq!(
response
@@ -2315,8 +2513,17 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
.clone()
.expect("Live API-key call should reach execution runtime");
let plan_url = url::Url::parse(plan.url.as_str()).expect("Live API-key URL should parse");
assert_eq!(plan_url.path(), "/v1/live");
assert!(plan_url.query().is_none());
assert_eq!(plan_url.path(), dialect.call_create_path());
match dialect {
CodexLiveWebRtcTestDialect::LegacyLive => assert!(plan_url.query().is_none()),
CodexLiveWebRtcTestDialect::Realtime => assert_eq!(
plan_url.query_pairs().collect::<HashMap<_, _>>(),
HashMap::from([
("intent".into(), "quicksilver".into()),
("architecture".into(), "avas".into()),
])
),
}
assert_eq!(plan.method, "POST");
assert!(!plan.stream);
assert!(plan
@@ -2344,7 +2551,7 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
);
assert_eq!(
plan.headers.get("openai-alpha").map(String::as_str),
Some("quicksilver=v2")
Some(dialect.alpha_header())
);
assert_eq!(
plan.headers.get("x-session-id").map(String::as_str),
@@ -2380,8 +2587,9 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
);
let conflicting_response = wreq::Client::new()
.websocket(format!(
"{}/v1/live/{CALL_ID}",
gateway_url.replacen("http://", "ws://", 1)
"{}{}",
gateway_url.replacen("http://", "ws://", 1),
dialect.sideband_path(CALL_ID)
))
.headers(sideband_headers)
.send()
@@ -2414,12 +2622,12 @@ async fn run_codex_live_api_key_sideband_frontdoor_scenario() {
.await
.expect("mock sideband should observe the opaque command before timeout")
.expect("mock sideband observation channel should remain open");
assert_eq!(observed.request_target, format!("/v1/live/{CALL_ID}"));
assert_eq!(observed.request_target, sideband_path);
assert_eq!(
observed.authorization.as_deref(),
Some("Bearer oauth-upstream-secret")
);
assert_eq!(observed.alpha.as_deref(), Some("quicksilver=v2"));
assert_eq!(observed.alpha.as_deref(), Some(dialect.alpha_header()));
assert_eq!(
observed.session_id.as_deref(),
Some("stable-live-sideband-session")
@@ -2501,6 +2709,10 @@ async fn mock_codex_live_websocket(
.get("openai-alpha")
.and_then(|value| value.to_str().ok())
.map(str::to_string);
let originator = headers
.get("originator")
.and_then(|value| value.to_str().ok())
.map(str::to_string);
let session_id = headers
.get("x-session-id")
.and_then(|value| value.to_str().ok())
@@ -2535,6 +2747,7 @@ async fn mock_codex_live_websocket(
request_target,
authorization,
alpha,
originator,
session_id,
initial_event,
event_after_turn_done,
@@ -127,8 +127,9 @@ pub fn resolve_execution_runtime_stream_plan_kind_with_client_surface(
if route_family == Some("codex")
&& route_kind == Some("live")
&& ((*method == Method::GET && (path == "/v1/live" || path.starts_with("/v1/live/")))
|| (*method == Method::POST && path == "/v1/live"))
&& ((*method == Method::GET
&& (path == "/v1/live" || path.starts_with("/v1/live/") || path == "/v1/realtime"))
|| (*method == Method::POST && matches!(path, "/v1/live" | "/v1/realtime/calls")))
{
return Some(CODEX_LIVE_STREAM_PLAN_KIND);
}
@@ -471,14 +472,42 @@ pub fn sanitize_request_path_and_query(path: &str, query: Option<&str>) -> Optio
let sanitized_path = sanitize_request_path(path)?;
let sanitized_query = query
.and_then(sanitize_request_query_string)
.or_else(|| embedded_query.and_then(sanitize_request_query_string));
.and_then(|query| sanitize_request_query_string_for_path(path, query))
.or_else(|| {
embedded_query.and_then(|query| sanitize_request_query_string_for_path(path, query))
});
Some(match sanitized_query {
Some(query) => format!("{sanitized_path}?{query}"),
None => sanitized_path,
})
}
fn sanitize_request_query_string_for_path(path: &str, query: &str) -> Option<String> {
if path != "/v1/realtime" {
return sanitize_request_query_string(query);
}
let query = query.trim().trim_start_matches('?').trim();
if query.is_empty() {
return None;
}
let mut serializer = form_urlencoded::Serializer::new(String::new());
let mut redacted_call_id = false;
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
if key.eq_ignore_ascii_case("call_id") {
if !value.trim().is_empty() && !redacted_call_id {
serializer.append_pair("call_id", "{call_id}");
redacted_call_id = true;
}
} else if request_query_key_is_safe_to_trace(key.as_ref()) {
serializer.append_pair(key.as_ref(), value.as_ref());
}
}
let sanitized = serializer.finish();
(!sanitized.is_empty()).then_some(sanitized)
}
fn request_query_key_is_safe_to_trace(key: &str) -> bool {
matches!(
key.to_ascii_lowercase().as_str(),
@@ -699,8 +728,10 @@ mod tests {
fn resolves_codex_live_call_create_and_websocket_routes() {
for (method, path) in [
(Method::POST, "/v1/live"),
(Method::POST, "/v1/realtime/calls"),
(Method::GET, "/v1/live"),
(Method::GET, "/v1/live/rtc_opaque"),
(Method::GET, "/v1/realtime"),
] {
assert_eq!(
resolve_execution_runtime_stream_plan_kind(
@@ -973,6 +1004,22 @@ mod tests {
.as_deref(),
Some("/v1/realtime?model=gpt-realtime-2.1")
);
assert_eq!(
sanitize_request_path_and_query(
"/v1/realtime?intent=quicksilver&call_id=rtc_secret_opaque&model=gpt-realtime-1.5&api_key=secret",
None
)
.as_deref(),
Some("/v1/realtime?call_id=%7Bcall_id%7D&model=gpt-realtime-1.5")
);
assert_eq!(
sanitize_request_path_and_query(
"/v1/realtime?c%61ll_id=rtc_secret_encoded&call_id=rtc_duplicate",
None
)
.as_deref(),
Some("/v1/realtime?call_id=%7Bcall_id%7D")
);
}
#[test]
@@ -978,7 +978,7 @@ const PROVIDER_MODEL_MAPPING_API_FORMAT_MATCH_SQL: &str = r#"(
OR (
LOWER(BTRIM(p.provider_type)) = 'codex'
AND LOWER($4) = 'codex:live'
AND LOWER(BTRIM(fmt.value)) = 'openai:responses'
AND LOWER(BTRIM(fmt.value)) IN ('openai:responses', '/v1/responses')
)
)"#;
@@ -1514,7 +1514,8 @@ mod tests {
};
use crate::{PostgresPoolConfig, PostgresPoolFactory};
use aether_data_contracts::repository::candidate_selection::{
StoredPoolKeyCandidateOrder, StoredProviderModelMapping,
provider_model_mapping_api_format_covers, StoredPoolKeyCandidateOrder,
StoredProviderModelMapping,
};
#[tokio::test]
@@ -1606,10 +1607,24 @@ mod tests {
assert!(!sql.contains(PROVIDER_MODEL_MAPPING_API_FORMAT_MATCH_MARKER));
assert!(compatibility.contains("LOWER(BTRIM(p.provider_type)) = 'codex'"));
assert!(compatibility.contains("LOWER($4) = 'codex:live'"));
assert!(compatibility.contains("LOWER(BTRIM(fmt.value)) = 'openai:responses'"));
for legacy_responses_alias in ["openai:responses", "/v1/responses"] {
assert!(provider_model_mapping_api_format_covers(
"codex",
legacy_responses_alias,
"codex:live"
));
assert!(compatibility.contains(&format!("'{legacy_responses_alias}'")));
}
assert!(!LIST_FOR_EXACT_API_FORMAT_SQL.contains(compatibility));
assert!(!LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL.contains(compatibility));
assert!(!LIST_POOL_KEYS_FOR_GROUP_SQL.contains(compatibility));
for permission_sql in [
LIST_FOR_EXACT_API_FORMAT_SQL,
LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL,
LIST_POOL_KEYS_FOR_GROUP_SQL,
] {
assert!(!permission_sql.contains("'/v1/responses'"));
}
}
#[test]
@@ -254,6 +254,12 @@ mod tests {
),
"/v1/realtime?model=gpt-realtime-2.1"
);
assert_eq!(
sanitize_access_log_path(
"/v1/realtime?intent=quicksilver&call_id=rtc_secret_opaque&model=gpt-realtime-1.5&api_key=secret"
),
"/v1/realtime?call_id=%7Bcall_id%7D&model=gpt-realtime-1.5"
);
}
#[tokio::test(flavor = "current_thread")]
@@ -214,6 +214,71 @@ mod tests {
assert_eq!(candidates[0].selected_provider_model_name, "gpt-5-canary-1");
}
#[test]
fn codex_live_enumeration_reuses_responses_mapping_without_widening_provider_scope() {
let mut codex = sample_row("codex-live");
codex.provider_name = "Codex".to_string();
codex.provider_type = "codex".to_string();
codex.endpoint_api_format = "codex:live".to_string();
codex.endpoint_api_family = Some("codex".to_string());
codex.endpoint_kind = Some("live".to_string());
codex.key_auth_type = "oauth".to_string();
codex.key_api_formats = Some(vec!["codex:live".to_string()]);
codex.key_allowed_models = Some(vec!["gpt-future-live".to_string()]);
codex.global_model_name = "live-future-alias".to_string();
codex.model_provider_model_name = "gpt-future-live".to_string();
codex.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-future-live".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: Some(vec![codex.endpoint_id.clone()]),
operations: None,
}]);
let constraints = SchedulerAuthConstraints {
allowed_providers: Some(vec!["codex".to_string()]),
allowed_api_formats: Some(vec!["codex:live".to_string()]),
allowed_models: Some(vec!["live-future-alias".to_string()]),
};
let candidates =
super::enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
rows: vec![codex.clone()],
normalized_api_format: "codex:live",
request_operation: None,
requested_model_name: "live-future-alias",
resolved_global_model_name: "live-future-alias",
require_streaming: true,
required_capabilities: None,
auth_constraints: Some(&constraints),
})
.expect("Codex Live candidate enumeration should build");
assert_eq!(candidates.len(), 1);
assert_eq!(
candidates[0].selected_provider_model_name,
"gpt-future-live"
);
for provider_type in ["openai", "custom"] {
let mut non_codex = codex.clone();
non_codex.provider_type = provider_type.to_string();
let candidates = super::enumerate_minimal_candidate_selection(
EnumerateMinimalCandidateSelectionInput {
rows: vec![non_codex],
normalized_api_format: "codex:live",
request_operation: None,
requested_model_name: "live-future-alias",
resolved_global_model_name: "live-future-alias",
require_streaming: true,
required_capabilities: None,
auth_constraints: None,
},
)
.expect("non-Codex Live candidate enumeration should build");
assert!(candidates.is_empty());
}
}
#[test]
fn enumeration_preserves_effective_streaming_capability() {
let mut row = sample_row("1");
+55 -8
View File
@@ -2,7 +2,8 @@ use std::borrow::Cow;
use std::collections::BTreeSet;
use aether_data_contracts::repository::candidate_selection::{
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
provider_model_mapping_api_format_covers, StoredMinimalCandidateSelectionRow,
StoredProviderModelMapping,
};
use aether_data_contracts::DataLayerError;
use regex::RegexBuilder;
@@ -339,9 +340,9 @@ fn mapping_scope_matches(
request_operation: Option<&str>,
) -> bool {
let api_format_matches_scope = mapping.api_formats.as_ref().is_none_or(|api_formats| {
api_formats
.iter()
.any(|value| api_format_scope_covers(value, api_format))
api_formats.iter().any(|value| {
provider_model_mapping_api_format_covers(&row.provider_type, value, api_format)
})
});
if !api_format_matches_scope {
return false;
@@ -498,10 +499,6 @@ fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format(left) == normalize_api_format(right)
}
fn api_format_scope_covers(allowed: &str, requested: &str) -> bool {
aether_ai_formats::api_format_permission_covers(allowed, requested)
}
fn requested_model_name_candidates(
requested_model_name: &str,
enable_model_directives: bool,
@@ -690,6 +687,56 @@ mod tests {
));
}
#[test]
fn codex_live_reuses_only_codex_responses_model_mappings() {
let mut row = sample_row("live-client-alias", "gpt-realtime-future");
row.provider_type = "codex".to_string();
row.endpoint_id = "endpoint-live".to_string();
row.endpoint_api_format = "codex:live".to_string();
row.key_auth_type = "oauth".to_string();
row.key_api_formats = Some(vec!["codex:live".to_string()]);
row.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-realtime-future".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: Some(vec!["endpoint-live".to_string()]),
operations: None,
}]);
assert!(row_supports_requested_model(
&row,
"live-client-alias",
"codex:live"
));
assert_eq!(
resolve_provider_model_name(&row, "live-client-alias", "codex:live")
.map(|resolved| resolved.0),
Some("gpt-realtime-future".to_string())
);
let mut wrong_endpoint = row.clone();
wrong_endpoint.endpoint_id = "endpoint-other".to_string();
assert!(!row_supports_requested_model(
&wrong_endpoint,
"live-client-alias",
"codex:live"
));
for provider_type in ["openai", "custom"] {
let mut non_codex = row.clone();
non_codex.provider_type = provider_type.to_string();
assert!(!row_supports_requested_model(
&non_codex,
"live-client-alias",
"codex:live"
));
assert!(
resolve_provider_model_name(&non_codex, "live-client-alias", "codex:live")
.is_none()
);
}
}
#[test]
fn operation_scoped_mapping_overrides_generic_mapping_for_compaction() {
let mut row = sample_row("gpt-5.6-sol", "gpt-5.6-sol");
+65 -6
View File
@@ -1,13 +1,15 @@
# WebSocket transports
Aether exposes three independent WebSocket surfaces. They share transport
Aether exposes several independent WebSocket surfaces. They share transport
machinery, but not request schemas or continuation state:
| Public route | API format | Protocol |
| --- | --- | --- |
| `GET /v1/responses` | `openai:responses` | Responses WebSocket mode; every turn starts with `response.create`. |
| `GET /v1/realtime?model=...` | `openai:realtime` | OpenAI Realtime JSON events, including Base64 audio events. |
| `GET /v1/live[/{call_id}]` | `codex:live` | Codex Frameless/Live direct and WebRTC-sideband transport. |
| `GET /v1/realtime?model=...` with a first-party Codex `originator` | `codex:live` | Current Codex Realtime v2 direct WebSocket transport. |
| `POST /v1/realtime/calls`, `GET /v1/realtime?intent=quicksilver&call_id=...` | `codex:live` | Codex Realtime v1 AVAS WebRTC call creation and sideband transport. |
| `GET/POST /v1/live[/{call_id}]` | `codex:live` | Legacy Codex Frameless direct and WebRTC compatibility transport. |
Do not point one surface at an endpoint configured for another. In particular,
a Realtime or Live event is not passed through the Responses
@@ -62,13 +64,29 @@ Codex clients. It is related to the OpenAI Realtime API, but it is not the
Responses WebSocket protocol and never enters Aether's `response.create`
state machine:
- Direct WebSocket: `GET /v1/live?model=<global-model>`. The first client text
- Current WebRTC call creation: `POST /v1/realtime/calls?intent=quicksilver&architecture=avas`
with bounded `sdp` and `session` multipart parts. Aether normalizes those
selectors, applies the global-to-provider mapping, and rewrites the upstream
`Location` to `/v1/realtime/calls/<call-id>`.
- Current WebRTC sideband: `GET /v1/realtime?intent=quicksilver&call_id=<call-id>`.
The unique `intent=quicksilver` selector classifies it as Codex Live;
`call_id` alone remains on the separate OpenAI Realtime surface.
- Current direct WebSocket (Realtime v2):
`GET /v1/realtime?model=<authorized-global-model>`. V2 intentionally omits
`intent=quicksilver`; Aether uses the first-party Codex `originator` header
to distinguish it from ordinary OpenAI Realtime. The first client event is
still the opaque `session.update` frame generated by Codex; Aether forwards
it without converting it into a Responses `response.create` event.
- Realtime v1 direct WebSocket uses
`GET /v1/realtime?intent=quicksilver&model=<authorized-global-model>` and
sends `openai-alpha: quicksilver=v1` upstream. V2 does not send that header.
- Legacy direct WebSocket: `GET /v1/live?model=<global-model>`. The first client text
frame must be `session.update`; later text, binary, ping, pong, and close
frames are relayed opaquely.
- WebRTC call creation: `POST /v1/live` with bounded `sdp` and `session`
- Legacy WebRTC call creation: `POST /v1/live` with bounded `sdp` and `session`
multipart parts. Aether applies the existing global-to-provider model
mapping and rewrites the upstream `Location` to `/v1/live/<call-id>`.
- WebRTC sideband: `GET /v1/live/<call-id>`. Frameless sideband attaches to an
- Legacy WebRTC sideband: `GET /v1/live/<call-id>`. Frameless sideband attaches to an
already initialized call, so Aether neither waits for nor sends a second
`session.update` frame.
@@ -78,6 +96,45 @@ custom providers can add it in the endpoint editor. The
`responses_websocket.enabled` provider option belongs only to
`openai:responses` WebSocket mode and is not reused as the Live permission.
Codex Live also requires an authorized model mapping; adding the endpoint alone
is not enough. For WebRTC, Codex sends the selected model in the multipart
`session.model`. Aether treats that value as the downstream global model,
selects an existing mapping whose provider endpoint is `codex:live`, and
rewrites only `session.model` to the mapped upstream model. Configure Codex's
realtime model selection to an authorized global alias (the current Codex
default may be `gpt-realtime-1.5`, but that value is client-version dependent),
or create an authorized mapping for the alias the client already sends. Aether
does not invent a Live model name or add a bundled hard-coded model merely
because the endpoint is enabled. For Codex providers only, an existing model mapping scoped to
`openai:responses` (including the historical `/v1/responses` alias) can be
reused for Live. The provider endpoint and key must still explicitly allow
`codex:live`; OpenAI and custom providers do not receive this compatibility
rule.
For Codex Desktop/app-server, point both the call-creation and sideband
overrides at the same Aether origin when using a custom provider. Explicitly
setting both avoids a client-version-dependent fallback to the OpenAI origin:
```toml
model_provider = "aether"
experimental_realtime_webrtc_call_base_url = "https://<aether-host>/v1"
experimental_realtime_ws_base_url = "https://<aether-host>/v1"
experimental_realtime_ws_model = "<authorized-global-live-alias>"
[model_providers.aether]
name = "Aether"
base_url = "https://<aether-host>/v1"
wire_api = "responses"
```
The two experimental overrides are optional when the selected provider's
`base_url` already points at Aether, but if either is set they must resolve to
the same deployment so the authenticated call binding can be found. A missing
call-create request in Aether means the client did not select this provider or
failed before gateway routing; a call-create request without the matching
sideband usually means the sideband origin or credential differs. These
settings may change with newer Codex releases.
API-key and bearer providers can use direct WebSocket or WebRTC. ChatGPT OAuth
uses the official Codex backend for WebRTC call creation and the OpenAI Live
origin for its sideband; direct OAuth WebSocket and custom OAuth backend
@@ -109,7 +166,9 @@ created call that never attaches a sideband is not held against provider
concurrency after call creation.
For the public GA Realtime API's connection and session concepts, see the
[OpenAI Realtime guide](https://developers.openai.com/api/docs/guides/realtime).
[OpenAI Realtime guide](https://developers.openai.com/api/docs/guides/realtime),
[WebRTC connection guide](https://developers.openai.com/api/docs/guides/realtime-webrtc),
and [server-side controls guide](https://developers.openai.com/api/docs/guides/realtime-server-controls).
OpenAI's current WebSocket service supports named `stream_id` lanes: requests on
the same lane are FIFO, while different lanes may run concurrently. Aether's