mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 18:37: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:
@@ -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'))"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user