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
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 {