mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +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:
@@ -1,9 +1,9 @@
|
||||
mod types;
|
||||
|
||||
pub use types::{
|
||||
MinimalCandidateSelectionReadRepository, MinimalCandidateSelectionRepository,
|
||||
StoredApiFormatCandidateRowsQuery, StoredMinimalCandidateSelectionRow,
|
||||
StoredPoolKeyCandidateOrder, StoredPoolKeyCandidateRowsByKeyIdsQuery,
|
||||
StoredPoolKeyCandidateRowsQuery, StoredProviderModelMapping,
|
||||
StoredRequestedModelCandidateRowsQuery,
|
||||
provider_model_mapping_api_format_covers, MinimalCandidateSelectionReadRepository,
|
||||
MinimalCandidateSelectionRepository, StoredApiFormatCandidateRowsQuery,
|
||||
StoredMinimalCandidateSelectionRow, StoredPoolKeyCandidateOrder,
|
||||
StoredPoolKeyCandidateRowsByKeyIdsQuery, StoredPoolKeyCandidateRowsQuery,
|
||||
StoredProviderModelMapping, StoredRequestedModelCandidateRowsQuery,
|
||||
};
|
||||
|
||||
@@ -113,6 +113,27 @@ impl StoredMinimalCandidateSelectionRow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the API-format scope on a provider-model mapping.
|
||||
///
|
||||
/// Codex Live was introduced after existing Codex model associations had
|
||||
/// already stored their source-model scope as `openai:responses`. Preserve
|
||||
/// those associations for the same Codex provider without treating the two
|
||||
/// formats as globally interchangeable. Endpoint and key permissions remain
|
||||
/// independently scoped to `codex:live`.
|
||||
pub fn provider_model_mapping_api_format_covers(
|
||||
provider_type: &str,
|
||||
mapping_api_format: &str,
|
||||
requested_api_format: &str,
|
||||
) -> bool {
|
||||
if aether_ai_formats::api_format_permission_covers(mapping_api_format, requested_api_format) {
|
||||
return true;
|
||||
}
|
||||
|
||||
provider_type.trim().eq_ignore_ascii_case("codex")
|
||||
&& aether_ai_formats::normalize_api_format_alias(requested_api_format) == "codex:live"
|
||||
&& aether_ai_formats::normalize_api_format_alias(mapping_api_format) == "openai:responses"
|
||||
}
|
||||
|
||||
fn api_format_permission_covers(allowed: &str, requested: &str) -> bool {
|
||||
aether_ai_formats::api_format_permission_covers(allowed, requested)
|
||||
}
|
||||
@@ -176,3 +197,37 @@ impl<T> MinimalCandidateSelectionRepository for T where
|
||||
T: MinimalCandidateSelectionReadRepository + Send + Sync
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::provider_model_mapping_api_format_covers;
|
||||
|
||||
#[test]
|
||||
fn legacy_responses_mapping_is_only_compatible_with_codex_live() {
|
||||
assert!(provider_model_mapping_api_format_covers(
|
||||
"codex",
|
||||
"openai:responses",
|
||||
"codex:live"
|
||||
));
|
||||
assert!(provider_model_mapping_api_format_covers(
|
||||
" CoDeX ",
|
||||
"/v1/responses",
|
||||
"codex:live"
|
||||
));
|
||||
|
||||
for provider_type in ["openai", "custom", "chatgpt_web"] {
|
||||
assert!(!provider_model_mapping_api_format_covers(
|
||||
provider_type,
|
||||
"openai:responses",
|
||||
"codex:live"
|
||||
));
|
||||
}
|
||||
for requested_api_format in ["openai:chat", "claude:messages", "openai:image"] {
|
||||
assert!(!provider_model_mapping_api_format_covers(
|
||||
"codex",
|
||||
"openai:responses",
|
||||
requested_api_format
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ pub use types::{
|
||||
UsageLeaderboardGroupBy, UsageLeaderboardQuery, UsageMonitoringErrorCountQuery,
|
||||
UsageMonitoringErrorListQuery, UsagePerformancePercentilesQuery, UsageProviderPerformanceQuery,
|
||||
UsageReadRepository, UsageRepository, UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
|
||||
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
|
||||
PROVIDER_SERVICE_TIER_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository, LIVE_SESSION_METADATA_KEY,
|
||||
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
REALTIME_SESSION_METADATA_KEY, REQUESTED_REASONING_EFFORT_METADATA_KEY,
|
||||
ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY, ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY,
|
||||
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
|
||||
@@ -254,12 +254,15 @@ pub fn provider_api_key_usage_contribution(
|
||||
request_count: 1,
|
||||
success_count: i64::from(is_success),
|
||||
error_count: i64::from(is_error),
|
||||
total_tokens: if is_in_flight {
|
||||
total_tokens: if is_in_flight || !usage.usage_available() {
|
||||
0
|
||||
} else {
|
||||
i64::try_from(usage.total_tokens).unwrap_or(i64::MAX)
|
||||
},
|
||||
total_cost_usd: if is_in_flight {
|
||||
total_cost_usd: if is_in_flight
|
||||
|| !usage.usage_available()
|
||||
|| !usage.usage_pricing_available()
|
||||
{
|
||||
0.0
|
||||
} else if usage.total_cost_usd.is_finite() {
|
||||
usage.total_cost_usd.max(0.0)
|
||||
@@ -308,8 +311,14 @@ pub fn api_key_usage_contribution(
|
||||
Some(ApiKeyUsageContribution {
|
||||
api_key_id,
|
||||
total_requests: 1,
|
||||
total_tokens: i64::try_from(usage.total_tokens).unwrap_or(i64::MAX),
|
||||
total_cost_usd: if usage.total_cost_usd.is_finite() {
|
||||
total_tokens: if usage.usage_available() {
|
||||
i64::try_from(usage.total_tokens).unwrap_or(i64::MAX)
|
||||
} else {
|
||||
0
|
||||
},
|
||||
total_cost_usd: if !usage.usage_available() || !usage.usage_pricing_available() {
|
||||
0.0
|
||||
} else if usage.total_cost_usd.is_finite() {
|
||||
usage.total_cost_usd.max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
@@ -325,3 +334,80 @@ fn newer_last_used_at(before: Option<u64>, after: Option<u64>) -> Option<u64> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{api_key_usage_contribution, provider_api_key_usage_contribution};
|
||||
use crate::repository::usage::StoredRequestUsageAudit;
|
||||
|
||||
fn authoritative_unpriced_usage() -> StoredRequestUsageAudit {
|
||||
let mut usage = StoredRequestUsageAudit::new(
|
||||
"usage-realtime".to_string(),
|
||||
"request-realtime".to_string(),
|
||||
None,
|
||||
Some("downstream-key".to_string()),
|
||||
None,
|
||||
None,
|
||||
"OpenAI".to_string(),
|
||||
"gpt-realtime".to_string(),
|
||||
None,
|
||||
Some("provider-realtime".to_string()),
|
||||
Some("endpoint-realtime".to_string()),
|
||||
Some("provider-key-realtime".to_string()),
|
||||
Some("realtime".to_string()),
|
||||
Some("openai:realtime".to_string()),
|
||||
Some("openai".to_string()),
|
||||
Some("realtime".to_string()),
|
||||
Some("openai:realtime".to_string()),
|
||||
Some("openai".to_string()),
|
||||
Some("realtime".to_string()),
|
||||
false,
|
||||
true,
|
||||
120,
|
||||
40,
|
||||
160,
|
||||
9.75,
|
||||
11.25,
|
||||
Some(200),
|
||||
None,
|
||||
None,
|
||||
Some(250),
|
||||
Some(30),
|
||||
"completed".to_string(),
|
||||
"void".to_string(),
|
||||
100,
|
||||
101,
|
||||
Some(102),
|
||||
)
|
||||
.expect("usage should build");
|
||||
usage.request_metadata = Some(json!({
|
||||
"usage_available": true,
|
||||
"usage_pricing_available": false,
|
||||
"realtime_session": {
|
||||
"input_audio_tokens": 20,
|
||||
"output_audio_tokens": 10,
|
||||
}
|
||||
}));
|
||||
usage
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoritative_unpriced_usage_contributes_tokens_but_never_cost() {
|
||||
let usage = authoritative_unpriced_usage();
|
||||
|
||||
let provider = provider_api_key_usage_contribution(&usage)
|
||||
.expect("provider key contribution should exist");
|
||||
assert_eq!(provider.request_count, 1);
|
||||
assert_eq!(provider.success_count, 1);
|
||||
assert_eq!(provider.total_tokens, 160);
|
||||
assert_eq!(provider.total_cost_usd, 0.0);
|
||||
|
||||
let downstream =
|
||||
api_key_usage_contribution(&usage).expect("API key contribution should exist");
|
||||
assert_eq!(downstream.total_requests, 1);
|
||||
assert_eq!(downstream.total_tokens, 160);
|
||||
assert_eq!(downstream.total_cost_usd, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,24 @@ pub const ROUTING_CANDIDATE_SKIP_REASON_METADATA_KEY: &str = "routing_candidate_
|
||||
pub const ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY: &str = "routing_failure_diagnostic";
|
||||
pub const WEBSOCKET_MODE_METADATA_KEY: &str = "websocket_mode";
|
||||
pub const WEBSOCKET_TRANSPORT_METADATA_KEY: &str = "websocket_transport";
|
||||
/// Whether token/cost usage is authoritative for this audit row.
|
||||
///
|
||||
/// The field is absent for legacy and normally-metered requests. An explicit
|
||||
/// `false` marks a transport/session audit whose lifecycle is known while the
|
||||
/// upstream protocol exposes no trustworthy token/cost usage. Such rows still
|
||||
/// count as requests and retain status/latency; only token and cost accounting
|
||||
/// is unavailable.
|
||||
pub const USAGE_AVAILABLE_METADATA_KEY: &str = "usage_available";
|
||||
/// Whether Aether has a compatible pricing model for the authoritative usage
|
||||
/// dimensions on this row. An explicit `false` keeps token telemetry visible
|
||||
/// while preventing those tokens from being priced with an incompatible rule.
|
||||
pub const USAGE_PRICING_AVAILABLE_METADATA_KEY: &str = "usage_pricing_available";
|
||||
/// Bounded session-level telemetry for transports that do not expose token
|
||||
/// usage (for example Codex Live direct/sideband WebSockets).
|
||||
pub const LIVE_SESSION_METADATA_KEY: &str = "live_session";
|
||||
/// Bounded lifecycle and authoritative usage facts for an OpenAI Realtime
|
||||
/// WebSocket connection. Audio payloads themselves are never stored here.
|
||||
pub const REALTIME_SESSION_METADATA_KEY: &str = "realtime_session";
|
||||
|
||||
pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Option<String> {
|
||||
let object = value.and_then(Value::as_object)?;
|
||||
@@ -546,6 +564,40 @@ impl StoredRequestUsageAudit {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn websocket_transport(&self) -> Option<&str> {
|
||||
self.request_metadata_string(WEBSOCKET_TRANSPORT_METADATA_KEY)
|
||||
}
|
||||
|
||||
/// Returns whether this row may participate in token/cost accounting.
|
||||
/// Missing metadata is treated as available for backward compatibility.
|
||||
pub fn usage_available(&self) -> bool {
|
||||
self.request_metadata_bool(USAGE_AVAILABLE_METADATA_KEY)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Returns whether token usage can be safely converted into cost. Missing
|
||||
/// metadata remains eligible for backward compatibility.
|
||||
pub fn usage_pricing_available(&self) -> bool {
|
||||
self.request_metadata_bool(USAGE_PRICING_AVAILABLE_METADATA_KEY)
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn realtime_input_audio_tokens(&self) -> Option<u64> {
|
||||
self.request_metadata_object()
|
||||
.and_then(|metadata| metadata.get(REALTIME_SESSION_METADATA_KEY))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|session| session.get("input_audio_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
}
|
||||
|
||||
pub fn realtime_output_audio_tokens(&self) -> Option<u64> {
|
||||
self.request_metadata_object()
|
||||
.and_then(|metadata| metadata.get(REALTIME_SESSION_METADATA_KEY))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|session| session.get("output_audio_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
}
|
||||
|
||||
fn billing_snapshot_resolved_number(&self, key: &str) -> Option<f64> {
|
||||
self.request_metadata_object()
|
||||
.and_then(|metadata| metadata.get("billing_snapshot"))
|
||||
@@ -953,6 +1005,7 @@ pub struct UsageAuditListQuery {
|
||||
pub statuses: Option<Vec<String>>,
|
||||
pub exclude_status_codes: Vec<u16>,
|
||||
pub is_stream: Option<bool>,
|
||||
pub is_websocket: Option<bool>,
|
||||
pub error_only: bool,
|
||||
pub limit: Option<usize>,
|
||||
pub offset: Option<usize>,
|
||||
@@ -972,6 +1025,7 @@ pub struct UsageAuditKeywordSearchQuery {
|
||||
pub statuses: Option<Vec<String>>,
|
||||
pub exclude_status_codes: Vec<u16>,
|
||||
pub is_stream: Option<bool>,
|
||||
pub is_websocket: Option<bool>,
|
||||
pub error_only: bool,
|
||||
pub keywords: Vec<String>,
|
||||
pub matched_user_ids_by_keyword: Vec<Vec<String>>,
|
||||
@@ -2401,8 +2455,9 @@ mod tests {
|
||||
extract_provider_actual_service_tier_from_response,
|
||||
extract_provider_service_tier_from_body, resolve_provider_cache_ttl_minutes,
|
||||
StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState, UsageBodyCaptureStorage,
|
||||
UsageBodyField, UsageProviderPerformanceQuery, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
UsageBodyField, UsageProviderPerformanceQuery, REALTIME_SESSION_METADATA_KEY,
|
||||
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -2675,13 +2730,26 @@ mod tests {
|
||||
fn websocket_transport_uses_typed_request_metadata() {
|
||||
let mut usage = sample_usage();
|
||||
assert!(!usage.is_websocket());
|
||||
assert!(usage.usage_available());
|
||||
assert!(usage.usage_pricing_available());
|
||||
|
||||
usage.request_metadata = Some(json!({
|
||||
WEBSOCKET_MODE_METADATA_KEY: true,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY: "responses",
|
||||
(WEBSOCKET_MODE_METADATA_KEY): true,
|
||||
(WEBSOCKET_TRANSPORT_METADATA_KEY): "responses",
|
||||
(USAGE_AVAILABLE_METADATA_KEY): false,
|
||||
(USAGE_PRICING_AVAILABLE_METADATA_KEY): false,
|
||||
(REALTIME_SESSION_METADATA_KEY): {
|
||||
"input_audio_tokens": 7,
|
||||
"output_audio_tokens": 3,
|
||||
},
|
||||
}));
|
||||
|
||||
assert!(usage.is_websocket());
|
||||
assert_eq!(usage.websocket_transport(), Some("responses"));
|
||||
assert!(!usage.usage_available());
|
||||
assert!(!usage.usage_pricing_available());
|
||||
assert_eq!(usage.realtime_input_audio_tokens(), Some(7));
|
||||
assert_eq!(usage.realtime_output_audio_tokens(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user