mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
feat(gateway): add Codex Live and OpenAI Realtime
Implement preflighted Live/Realtime WebSocket transports, protocol-aware authentication, usage auditing, UI filtering, and legacy Codex permission migration.
This commit is contained in:
@@ -243,7 +243,7 @@ fn select_primary_credential(
|
||||
if signature.starts_with("claude:") {
|
||||
return select_claude_messages_credential(bundle);
|
||||
}
|
||||
if signature.starts_with("openai:") {
|
||||
if signature.starts_with("openai:") || signature.starts_with("codex:") {
|
||||
return select_openai_credential(bundle);
|
||||
}
|
||||
if signature.starts_with("aether:") {
|
||||
@@ -491,6 +491,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_codex_live_bearer_as_provider_api_key() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(
|
||||
http::header::AUTHORIZATION,
|
||||
"Bearer sk-codex-live".parse().unwrap(),
|
||||
);
|
||||
|
||||
let extracted =
|
||||
extract_request_credentials(&headers, &uri("/v1/live?model=gpt-live"), "codex:live");
|
||||
assert_eq!(
|
||||
extracted.primary,
|
||||
Some(GatewayPrimaryCredential::ProviderApiKey {
|
||||
raw: "sk-codex-live".to_string(),
|
||||
carrier: GatewayCredentialCarrier::AuthorizationBearer,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefers_claude_chat_x_api_key_over_bearer() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
|
||||
@@ -35,21 +35,25 @@ pub(super) fn classify_ai_public_route(
|
||||
"openai:rerank",
|
||||
true,
|
||||
))
|
||||
} else if method == http::Method::GET
|
||||
&& normalized_path == "/v1/realtime"
|
||||
&& is_websocket_upgrade_request(headers)
|
||||
{
|
||||
Some(classified(
|
||||
"ai_public",
|
||||
"openai",
|
||||
"realtime",
|
||||
"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 is an experimental companion transport for an existing
|
||||
// Responses mapping. It deliberately reuses the Responses permission
|
||||
// surface while its wire protocol is handled by an independent relay.
|
||||
Some(classified(
|
||||
"ai_public",
|
||||
"openai",
|
||||
"live",
|
||||
"openai:responses",
|
||||
true,
|
||||
))
|
||||
// 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"
|
||||
@@ -308,11 +312,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_live_http_and_websocket_routes_as_responses_companions() {
|
||||
fn classifies_only_websocket_upgrade_on_realtime_route() {
|
||||
let mut headers = HeaderMap::new();
|
||||
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");
|
||||
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());
|
||||
}
|
||||
|
||||
#[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, "openai:responses");
|
||||
assert_eq!(post.auth_endpoint_signature, "codex:live");
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(CONNECTION, HeaderValue::from_static("Upgrade"));
|
||||
@@ -320,8 +345,9 @@ mod tests {
|
||||
for path in ["/v1/live", "/v1/live/rtc_opaque"] {
|
||||
let route = classify_ai_public_route(&Method::GET, path, &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, "openai:responses");
|
||||
assert_eq!(route.auth_endpoint_signature, "codex:live");
|
||||
}
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -108,6 +108,32 @@ fn classifies_openai_chat_and_responses_separately_from_embedding() {
|
||||
assert_ne!(responses.route_kind.as_deref(), Some("embedding"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_openai_realtime_only_for_websocket_upgrades() {
|
||||
let websocket_headers = headers(&[
|
||||
("authorization", "Bearer sk-test"),
|
||||
("connection", "keep-alive, Upgrade"),
|
||||
("upgrade", "websocket"),
|
||||
]);
|
||||
let uri: Uri = "/v1/realtime?model=gpt-realtime"
|
||||
.parse()
|
||||
.expect("uri should parse");
|
||||
|
||||
let decision = classify_control_route(&http::Method::GET, &uri, &websocket_headers)
|
||||
.expect("Realtime WebSocket route should classify");
|
||||
assert_eq!(decision.route_family.as_deref(), Some("openai"));
|
||||
assert_eq!(decision.route_kind.as_deref(), Some("realtime"));
|
||||
assert_eq!(
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
Some("openai:realtime")
|
||||
);
|
||||
assert!(decision.is_execution_runtime_candidate());
|
||||
|
||||
let plain_headers = headers(&[("authorization", "Bearer sk-test")]);
|
||||
assert!(classify_control_route(&http::Method::GET, &uri, &plain_headers).is_none());
|
||||
assert!(classify_control_route(&http::Method::POST, &uri, &websocket_headers).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_openai_image_generation_and_edit_but_not_variation() {
|
||||
let headers = headers(&[("authorization", "Bearer sk-test")]);
|
||||
|
||||
Reference in New Issue
Block a user