mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47: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,4 +1,6 @@
|
||||
use aether_data_contracts::repository::usage::UpsertUsageRecord;
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UpsertUsageRecord, USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
};
|
||||
use aether_data_contracts::DataLayerError;
|
||||
|
||||
use crate::request_metadata::{
|
||||
@@ -33,7 +35,8 @@ fn metadata_u64(metadata: Option<&serde_json::Value>, key: &str) -> Option<u64>
|
||||
pub fn build_upsert_usage_record_from_event(
|
||||
event: &UsageEvent,
|
||||
) -> Result<UpsertUsageRecord, DataLayerError> {
|
||||
let (status, billing_status) = lifecycle_status_and_billing(event.event_type);
|
||||
let (status, billing_status) =
|
||||
lifecycle_status_and_billing(event.event_type, event.data.request_metadata.as_ref());
|
||||
let finalized_at_unix_secs = match event.event_type {
|
||||
UsageEventType::Pending | UsageEventType::Streaming => None,
|
||||
UsageEventType::Completed | UsageEventType::Failed | UsageEventType::Cancelled => {
|
||||
@@ -41,6 +44,11 @@ pub fn build_upsert_usage_record_from_event(
|
||||
}
|
||||
};
|
||||
let mut data = event.data.clone();
|
||||
if usage_is_explicitly_unavailable(data.request_metadata.as_ref()) {
|
||||
clear_unavailable_usage_fields(&mut data);
|
||||
} else if usage_pricing_is_explicitly_unavailable(data.request_metadata.as_ref()) {
|
||||
clear_unavailable_pricing_fields(&mut data);
|
||||
}
|
||||
// Request-derived facts are captured before body capture policy is applied. Do not let a
|
||||
// truncation/disabled placeholder clear those facts while converting the queued event into a
|
||||
// database record. Inline (or ref-loaded) bodies remain authoritative and may clear stale
|
||||
@@ -177,16 +185,62 @@ pub fn build_upsert_usage_record_from_event(
|
||||
})
|
||||
}
|
||||
|
||||
fn lifecycle_status_and_billing(event_type: UsageEventType) -> (&'static str, &'static str) {
|
||||
fn clear_unavailable_usage_fields(data: &mut crate::UsageEventData) {
|
||||
data.input_tokens = None;
|
||||
data.output_tokens = None;
|
||||
data.total_tokens = None;
|
||||
data.cache_creation_input_tokens = None;
|
||||
data.cache_creation_ephemeral_5m_input_tokens = None;
|
||||
data.cache_creation_ephemeral_1h_input_tokens = None;
|
||||
data.cache_read_input_tokens = None;
|
||||
data.cache_creation_cost_usd = None;
|
||||
data.cache_read_cost_usd = None;
|
||||
data.total_cost_usd = None;
|
||||
data.actual_total_cost_usd = None;
|
||||
}
|
||||
|
||||
fn clear_unavailable_pricing_fields(data: &mut crate::UsageEventData) {
|
||||
data.cache_creation_cost_usd = None;
|
||||
data.cache_read_cost_usd = None;
|
||||
data.total_cost_usd = None;
|
||||
data.actual_total_cost_usd = None;
|
||||
}
|
||||
|
||||
fn lifecycle_status_and_billing(
|
||||
event_type: UsageEventType,
|
||||
request_metadata: Option<&serde_json::Value>,
|
||||
) -> (&'static str, &'static str) {
|
||||
match event_type {
|
||||
UsageEventType::Pending => ("pending", "pending"),
|
||||
UsageEventType::Streaming => ("streaming", "pending"),
|
||||
UsageEventType::Completed
|
||||
if usage_is_explicitly_unavailable(request_metadata)
|
||||
|| usage_pricing_is_explicitly_unavailable(request_metadata) =>
|
||||
{
|
||||
("completed", "void")
|
||||
}
|
||||
UsageEventType::Completed => ("completed", "pending"),
|
||||
UsageEventType::Failed => ("failed", "void"),
|
||||
UsageEventType::Cancelled => ("cancelled", "void"),
|
||||
}
|
||||
}
|
||||
|
||||
fn usage_is_explicitly_unavailable(request_metadata: Option<&serde_json::Value>) -> bool {
|
||||
request_metadata
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|metadata| metadata.get(USAGE_AVAILABLE_METADATA_KEY))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== Some(false)
|
||||
}
|
||||
|
||||
fn usage_pricing_is_explicitly_unavailable(request_metadata: Option<&serde_json::Value>) -> bool {
|
||||
request_metadata
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.and_then(|metadata| metadata.get(USAGE_PRICING_AVAILABLE_METADATA_KEY))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
== Some(false)
|
||||
}
|
||||
|
||||
fn empty_to_none(value: Option<String>) -> Option<String> {
|
||||
value
|
||||
.map(|value| value.trim().to_string())
|
||||
@@ -437,6 +491,97 @@ mod tests {
|
||||
assert_eq!(record.first_byte_time_ms, Some(50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_unmetered_session_audit_is_void_without_fabricated_usage() {
|
||||
let record = build_upsert_usage_record_from_event(&UsageEvent {
|
||||
event_type: UsageEventType::Completed,
|
||||
request_id: "req-live-session".to_string(),
|
||||
timestamp_ms: 1_700_000_000_000,
|
||||
data: UsageEventData {
|
||||
provider_name: "OpenAI".to_string(),
|
||||
model: "gpt-live".to_string(),
|
||||
status_code: Some(200),
|
||||
input_tokens: Some(100),
|
||||
output_tokens: Some(20),
|
||||
total_tokens: Some(120),
|
||||
total_cost_usd: Some(1.25),
|
||||
actual_total_cost_usd: Some(1.25),
|
||||
request_metadata: Some(serde_json::json!({
|
||||
"usage_available": false,
|
||||
"websocket_mode": true,
|
||||
"websocket_transport": "codex_live_direct",
|
||||
})),
|
||||
..UsageEventData::default()
|
||||
},
|
||||
})
|
||||
.expect("record should build");
|
||||
|
||||
assert_eq!(record.status, "completed");
|
||||
assert_eq!(record.billing_status, "void");
|
||||
assert_eq!(record.input_tokens, None);
|
||||
assert_eq!(record.output_tokens, None);
|
||||
assert_eq!(record.total_tokens, None);
|
||||
assert_eq!(record.total_cost_usd, None);
|
||||
assert_eq!(record.actual_total_cost_usd, None);
|
||||
assert_eq!(
|
||||
record
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("usage_available"))
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_authoritative_unpriced_session_is_void_but_keeps_tokens() {
|
||||
let record = build_upsert_usage_record_from_event(&UsageEvent {
|
||||
event_type: UsageEventType::Completed,
|
||||
request_id: "req-realtime-audio".to_string(),
|
||||
timestamp_ms: 1_700_000_000_000,
|
||||
data: UsageEventData {
|
||||
provider_name: "OpenAI".to_string(),
|
||||
model: "gpt-realtime".to_string(),
|
||||
status_code: Some(200),
|
||||
input_tokens: Some(120),
|
||||
output_tokens: Some(40),
|
||||
total_tokens: Some(160),
|
||||
cache_read_input_tokens: Some(30),
|
||||
cache_creation_cost_usd: Some(0.25),
|
||||
cache_read_cost_usd: Some(0.1),
|
||||
total_cost_usd: Some(1.25),
|
||||
actual_total_cost_usd: Some(1.5),
|
||||
request_metadata: Some(serde_json::json!({
|
||||
"usage_available": true,
|
||||
"usage_pricing_available": false,
|
||||
"websocket_mode": true,
|
||||
"websocket_transport": "openai_realtime",
|
||||
})),
|
||||
..UsageEventData::default()
|
||||
},
|
||||
})
|
||||
.expect("record should build");
|
||||
|
||||
assert_eq!(record.status, "completed");
|
||||
assert_eq!(record.billing_status, "void");
|
||||
assert_eq!(record.input_tokens, Some(120));
|
||||
assert_eq!(record.output_tokens, Some(40));
|
||||
assert_eq!(record.total_tokens, Some(160));
|
||||
assert_eq!(record.cache_read_input_tokens, Some(30));
|
||||
assert_eq!(record.cache_creation_cost_usd, None);
|
||||
assert_eq!(record.cache_read_cost_usd, None);
|
||||
assert_eq!(record.total_cost_usd, None);
|
||||
assert_eq!(record.actual_total_cost_usd, None);
|
||||
assert_eq!(
|
||||
record
|
||||
.request_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.get("usage_pricing_available"))
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitizes_request_metadata_before_building_upsert_record() {
|
||||
let record = build_upsert_usage_record_from_event(&UsageEvent {
|
||||
|
||||
@@ -7,10 +7,12 @@ use aether_data_contracts::repository::usage::{
|
||||
extract_provider_actual_service_tier_from_response,
|
||||
extract_provider_reasoning_effort_from_body, extract_provider_service_tier_from_body,
|
||||
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes, UsageBodyCaptureState,
|
||||
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY,
|
||||
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
|
||||
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, WEBSOCKET_MODE_METADATA_KEY,
|
||||
ROUTING_FAILURE_DIAGNOSTIC_METADATA_KEY, USAGE_AVAILABLE_METADATA_KEY,
|
||||
USAGE_PRICING_AVAILABLE_METADATA_KEY, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
@@ -132,6 +134,12 @@ pub(crate) fn retain_first_byte_request_metadata(value: Option<Value>) -> Option
|
||||
| "upstream_is_stream"
|
||||
| "client_session_affinity"
|
||||
| "api_key_is_standalone"
|
||||
| "websocket_mode"
|
||||
| "websocket_transport"
|
||||
| "usage_available"
|
||||
| "usage_pricing_available"
|
||||
| "live_session"
|
||||
| "realtime_session"
|
||||
| "request_path"
|
||||
| "request_query_string"
|
||||
| "request_path_and_query"
|
||||
@@ -351,6 +359,10 @@ fn copy_allowed_metadata_fields(source: &Map<String, Value>, target: &mut Map<St
|
||||
copy_bool(source, target, "api_key_is_standalone");
|
||||
copy_bool(source, target, WEBSOCKET_MODE_METADATA_KEY);
|
||||
copy_non_empty_string(source, target, WEBSOCKET_TRANSPORT_METADATA_KEY);
|
||||
copy_bool(source, target, USAGE_AVAILABLE_METADATA_KEY);
|
||||
copy_bool(source, target, USAGE_PRICING_AVAILABLE_METADATA_KEY);
|
||||
copy_non_null_value(source, target, LIVE_SESSION_METADATA_KEY);
|
||||
copy_non_null_value(source, target, REALTIME_SESSION_METADATA_KEY);
|
||||
copy_non_empty_string(source, target, "request_path");
|
||||
copy_non_empty_string(source, target, "request_query_string");
|
||||
copy_non_empty_string(source, target, "request_path_and_query");
|
||||
@@ -407,6 +419,10 @@ fn move_allowed_metadata_fields(mut source: Map<String, Value>, target: &mut Map
|
||||
remove_bool(&mut source, target, "api_key_is_standalone");
|
||||
remove_bool(&mut source, target, WEBSOCKET_MODE_METADATA_KEY);
|
||||
remove_non_empty_string(&mut source, target, WEBSOCKET_TRANSPORT_METADATA_KEY);
|
||||
remove_bool(&mut source, target, USAGE_AVAILABLE_METADATA_KEY);
|
||||
remove_bool(&mut source, target, USAGE_PRICING_AVAILABLE_METADATA_KEY);
|
||||
remove_non_null_value(&mut source, target, LIVE_SESSION_METADATA_KEY);
|
||||
remove_non_null_value(&mut source, target, REALTIME_SESSION_METADATA_KEY);
|
||||
remove_non_empty_string(&mut source, target, "request_path");
|
||||
remove_non_empty_string(&mut source, target, "request_query_string");
|
||||
remove_non_empty_string(&mut source, target, "request_path_and_query");
|
||||
|
||||
@@ -3,8 +3,9 @@ use std::collections::BTreeMap;
|
||||
use aether_ai_formats::UPSTREAM_IS_STREAM_KEY;
|
||||
use aether_contracts::{ExecutionPlan, ExecutionTelemetry};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UpsertUsageRecord, UsageBodyCaptureState, WEBSOCKET_MODE_METADATA_KEY,
|
||||
WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
UpsertUsageRecord, UsageBodyCaptureState, LIVE_SESSION_METADATA_KEY,
|
||||
USAGE_AVAILABLE_METADATA_KEY, USAGE_PRICING_AVAILABLE_METADATA_KEY,
|
||||
WEBSOCKET_MODE_METADATA_KEY, WEBSOCKET_TRANSPORT_METADATA_KEY,
|
||||
};
|
||||
use aether_data_contracts::DataLayerError;
|
||||
use base64::Engine as _;
|
||||
@@ -2129,6 +2130,21 @@ fn build_runtime_request_metadata_seed_from_parts(
|
||||
Value::String(websocket_transport),
|
||||
);
|
||||
}
|
||||
if let Some(usage_available) = context_bool(context, USAGE_AVAILABLE_METADATA_KEY) {
|
||||
metadata.insert(
|
||||
USAGE_AVAILABLE_METADATA_KEY.to_string(),
|
||||
Value::Bool(usage_available),
|
||||
);
|
||||
}
|
||||
if let Some(pricing_available) = context_bool(context, USAGE_PRICING_AVAILABLE_METADATA_KEY) {
|
||||
metadata.insert(
|
||||
USAGE_PRICING_AVAILABLE_METADATA_KEY.to_string(),
|
||||
Value::Bool(pricing_available),
|
||||
);
|
||||
}
|
||||
if let Some(live_session) = context_value_ref(context, LIVE_SESSION_METADATA_KEY) {
|
||||
metadata.insert(LIVE_SESSION_METADATA_KEY.to_string(), live_session.clone());
|
||||
}
|
||||
let provider_source_bytes = provider_request_body_base64.and_then(decoded_base64_len_hint);
|
||||
append_runtime_body_capture_metadata(
|
||||
&mut metadata,
|
||||
|
||||
Reference in New Issue
Block a user