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:
ZheFox
2026-08-21 04:27:34 +08:00
parent fe38dcd294
commit 2c89202001
105 changed files with 7553 additions and 947 deletions
@@ -4,10 +4,11 @@ use async_trait::async_trait;
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
use aether_data_contracts::repository::candidate_selection::{
MinimalCandidateSelectionReadRepository, StoredApiFormatCandidateRowsQuery,
StoredMinimalCandidateSelectionRow, StoredPoolKeyCandidateOrder,
StoredPoolKeyCandidateRowsByKeyIdsQuery, StoredPoolKeyCandidateRowsQuery,
StoredProviderModelMapping, StoredRequestedModelCandidateRowsQuery,
provider_model_mapping_api_format_covers, MinimalCandidateSelectionReadRepository,
StoredApiFormatCandidateRowsQuery, StoredMinimalCandidateSelectionRow,
StoredPoolKeyCandidateOrder, StoredPoolKeyCandidateRowsByKeyIdsQuery,
StoredPoolKeyCandidateRowsQuery, StoredProviderModelMapping,
StoredRequestedModelCandidateRowsQuery,
};
use aether_data_contracts::DataLayerError;
@@ -675,9 +676,9 @@ fn mapping_scope_matches(
api_format: &str,
) -> bool {
mapping.api_formats.as_ref().is_none_or(|formats| {
formats
.iter()
.any(|value| api_format_scope_covers(value, api_format))
formats.iter().any(|value| {
provider_model_mapping_api_format_covers(&row.provider_type, value, api_format)
})
}) && mapping.endpoint_ids.as_ref().is_none_or(|endpoint_ids| {
endpoint_ids
.iter()
@@ -690,7 +691,7 @@ fn push_key_auth_channel_filter(builder: &mut QueryBuilder<'_, MySql>, api_forma
builder.push(" AND LOWER(TRIM(pak.auth_type)) = 'oauth' AND ");
builder.push_bind(api_format.to_string());
builder.push(
" IN ('openai:responses', 'openai:responses:compact', 'openai:search', 'openai:image'))",
" IN ('openai:responses', 'openai:responses:compact', 'openai:search', 'openai:image', 'codex:live'))",
);
builder.push(" OR (LOWER(TRIM(p.provider_type)) = 'chatgpt_web'");
@@ -749,6 +750,7 @@ fn key_auth_channel_matches(row: &CandidateSelectionRow, api_format: &str) -> bo
| "openai:responses:compact"
| "openai:search"
| "openai:image"
| "codex:live"
)
}
"chatgpt_web" => {
@@ -1096,10 +1098,6 @@ fn api_format_matches(left: &str, right: &str) -> bool {
aether_ai_formats::api_format_alias_matches(left, right)
}
fn api_format_scope_covers(allowed: &str, requested: &str) -> bool {
aether_ai_formats::api_format_permission_covers(allowed, requested)
}
fn sql_match_aliases(api_formats: &[String]) -> Vec<String> {
api_formats
.iter()
@@ -1111,6 +1109,7 @@ fn sql_match_aliases(api_formats: &[String]) -> Vec<String> {
mod tests {
use super::{
api_format_page_query, pool_key_group_by_key_ids_query, pool_key_group_query,
provider_model_mapping_api_format_covers, push_key_auth_channel_filter,
requested_model_page_query, vertex_key_auth_channel_matches, ExactPageAccumulator,
MysqlMinimalCandidateSelectionReadRepository, REQUESTED_MODEL_RAW_SCAN_LIMIT,
};
@@ -1147,6 +1146,47 @@ mod tests {
assert!(sql.contains("LIMIT ? OFFSET ?"));
}
#[test]
fn codex_auth_sql_allows_live_for_oauth_keys() {
let mut builder = sqlx::QueryBuilder::<sqlx::MySql>::new("SELECT 1 WHERE 1 = 1");
push_key_auth_channel_filter(&mut builder, "codex:live");
let sql = builder.sql();
let codex_clause = sql
.split_once("LOWER(TRIM(p.provider_type)) = 'codex'")
.and_then(|(_, suffix)| {
suffix.split_once("LOWER(TRIM(p.provider_type)) = 'chatgpt_web'")
})
.map(|(clause, _)| clause)
.expect("Codex auth clause should exist");
assert!(codex_clause.contains("LOWER(TRIM(pak.auth_type)) = 'oauth'"));
assert!(codex_clause.contains("'codex:live'"));
}
#[test]
fn mysql_mapping_scope_keeps_legacy_responses_compatibility_codex_only() {
assert!(provider_model_mapping_api_format_covers(
"codex",
"openai:responses",
"codex:live"
));
assert!(!provider_model_mapping_api_format_covers(
"openai",
"openai:responses",
"codex:live"
));
assert!(!provider_model_mapping_api_format_covers(
"custom",
"openai:responses",
"codex:live"
));
assert!(!provider_model_mapping_api_format_covers(
"codex",
"openai:chat",
"codex:live"
));
}
#[test]
fn exact_page_accumulator_continues_after_coarse_false_positives() {
let mut accumulator = ExactPageAccumulator::new(1, 2);
@@ -100,6 +100,30 @@ mod tests {
assert!(versions.windows(2).all(|pair| pair[0] < pair[1]));
}
#[test]
fn embeds_scoped_codex_live_permission_migration() {
let migration = MIGRATOR
.iter()
.find(|migration| migration.version == 20260821000000)
.expect("Codex Live permission migration should be embedded");
let sql = migration.sql.as_ref();
for required_fragment in [
"UPDATE users",
"UPDATE user_groups",
"UPDATE api_keys",
"UPDATE provider_api_keys",
"provider.provider_type",
"openai:responses",
"codex:live",
] {
assert!(
sql.contains(required_fragment),
"Codex Live permission migration is missing {required_fragment}"
);
}
}
#[test]
fn embeds_cross_driver_schema_parity_migration() {
let migration = MIGRATOR
@@ -583,6 +583,12 @@ AND LOWER(TRIM(COALESCE(`usage`.provider_name, ''))) NOT IN ('unknown', 'unknow'
push_where(builder, has_where);
builder.push("`usage`.is_stream = ").push_bind(is_stream);
}
if let Some(is_websocket) = query.is_websocket {
push_where(builder, has_where);
builder
.push("COALESCE(JSON_UNQUOTE(JSON_EXTRACT(`usage`.request_metadata, '$.websocket_mode')), 'false') = ")
.push_bind(if is_websocket { "true" } else { "false" });
}
if query.error_only {
push_where(builder, has_where);
builder.push(
@@ -613,6 +619,7 @@ fn push_keyword_filters(
statuses: query.statuses.clone(),
exclude_status_codes: query.exclude_status_codes.clone(),
is_stream: query.is_stream,
is_websocket: query.is_websocket,
error_only: query.error_only,
limit: None,
offset: None,
@@ -808,4 +815,38 @@ mod tests {
.sql()
.contains("LOWER(COALESCE(`usage`.api_key_name, '')) LIKE ?"));
}
#[test]
fn websocket_filter_is_applied_to_list_and_keyword_queries() {
let mut list_query = QueryBuilder::<MySql>::new(USAGE_COLUMNS);
let mut has_where = false;
push_list_filters(
&mut list_query,
&UsageAuditListQuery {
is_websocket: Some(true),
..UsageAuditListQuery::default()
},
&mut has_where,
)
.expect("WebSocket list query should build");
assert!(list_query
.sql()
.contains("JSON_UNQUOTE(JSON_EXTRACT(`usage`.request_metadata, '$.websocket_mode'))"));
let mut keyword_query = QueryBuilder::<MySql>::new(USAGE_COLUMNS);
let mut has_where = false;
push_keyword_filters(
&mut keyword_query,
&UsageAuditKeywordSearchQuery {
is_websocket: Some(true),
keywords: vec!["live".to_string()],
..UsageAuditKeywordSearchQuery::default()
},
&mut has_where,
)
.expect("WebSocket keyword query should build");
assert!(keyword_query
.sql()
.contains("JSON_UNQUOTE(JSON_EXTRACT(`usage`.request_metadata, '$.websocket_mode'))"));
}
}