fix(codex): self-heal Spark-contaminated account quotas

Keep model-scoped Spark windows out of account state, preserve authoritative WHAM exhaustion flags, and repair historical cross-family quota generations without weakening stale-response guards.
This commit is contained in:
ZheFox
2026-08-25 19:13:41 +08:00
parent 42deab67b3
commit 2f2d444f97
4 changed files with 732 additions and 61 deletions
@@ -13,8 +13,9 @@ use crate::ai_serving::AiExecutionDecision;
use crate::clock::current_unix_secs;
use crate::handlers::proxy::websocket::transport::UpstreamWebSocketErrorCodes;
use crate::orchestration::{
codex_account_id_from_headers, codex_quota_exhaustion_reset_at,
sync_codex_websocket_quota_metadata, ResponsesWebSocketAdapter,
codex_account_id_from_headers, codex_model_quota_exhaustion_reset_at,
codex_quota_exhaustion_reset_at, sync_codex_websocket_quota_metadata,
ResponsesWebSocketAdapter,
};
use crate::AppState;
@@ -114,16 +115,37 @@ impl ResponsesWebSocketProtocolAdapter for CodexResponsesWebSocketAdapter {
event: &Value,
) -> Option<ResponsesWebSocketAdapterObservation> {
let rate_limits = parse_codex_rate_limits(event)?;
let exhausted =
let account_exhausted =
aether_admin::provider::quota::codex_rate_limit_metadata_exhausted(&rate_limits);
let retry_exclusion_until_unix_secs =
codex_quota_exhaustion_reset_at(&rate_limits, current_unix_secs());
let active_limit_exhausted =
aether_admin::provider::quota::codex_websocket_response_has_usage_limit_error(event);
let now_unix_secs = current_unix_secs();
let scoped_reset_at = if account_exhausted {
codex_quota_exhaustion_reset_at(&rate_limits, now_unix_secs)
} else {
codex_model_quota_exhaustion_reset_at(&rate_limits, now_unix_secs)
};
let retry_exclusion_until_unix_secs = active_limit_exhausted
.then(|| {
aether_admin::provider::quota::codex_websocket_usage_limit_reset_at(
event,
now_unix_secs,
)
})
.flatten()
.or(scoped_reset_at);
Some(ResponsesWebSocketAdapterObservation {
drain: exhausted.then_some(ResponsesWebSocketDrainDirective {
error_code: "codex_account_quota_exhausted",
retry_current_turn: true,
retry_exclusion_until_unix_secs,
}),
drain: (account_exhausted || active_limit_exhausted).then_some(
ResponsesWebSocketDrainDirective {
error_code: if account_exhausted {
"codex_account_quota_exhausted"
} else {
"codex_active_limit_exhausted"
},
retry_current_turn: true,
retry_exclusion_until_unix_secs,
},
),
quota_metadata: Some(rate_limits),
})
}
@@ -337,6 +359,91 @@ mod tests {
);
}
#[test]
fn model_scoped_usage_limit_error_drains_without_account_exhaustion() {
let adapter = CodexResponsesWebSocketAdapter;
let event = json!({
"type": "error",
"error": {
"type": "usage_limit_reached",
"plan_type": "pro",
},
"status_code": 429,
"headers": {
"X-Codex-Plan-Type": "pro",
"X-Codex-Active-Limit": "codex_bengalfox",
"X-Codex-Primary-Used-Percent": "100",
"X-Codex-Primary-Window-Minutes": "300",
"X-Codex-Primary-Reset-At": "4000000000",
"X-Codex-Bengalfox-Limit-Name": "GPT-5.3-Codex-Spark",
"X-Codex-Bengalfox-Primary-Used-Percent": "100",
"X-Codex-Bengalfox-Primary-Window-Minutes": "300",
"X-Codex-Bengalfox-Primary-Reset-At": "4000000000",
},
});
let observation = adapter
.observe_upstream_event(&event)
.expect("model-scoped quota error should be observed");
let drain = observation
.drain
.expect("model-scoped quota error should retry the current turn");
let quota = observation
.quota_metadata
.expect("model-scoped quota metadata should be retained");
assert_eq!(drain.error_code, "codex_active_limit_exhausted");
assert!(drain.retry_current_turn);
assert_eq!(
drain.retry_exclusion_until_unix_secs,
Some(4_000_000_000u64)
);
assert_eq!(quota["spark_primary_used_percent"], json!(100.0));
assert!(quota.get("allowed").is_none());
assert!(quota.get("limit_reached").is_none());
assert!(!aether_admin::provider::quota::codex_rate_limit_metadata_exhausted(&quota));
}
#[test]
fn account_scoped_usage_limit_error_keeps_account_drain_semantics() {
let adapter = CodexResponsesWebSocketAdapter;
let event = json!({
"type": "error",
"error": {
"type": "usage_limit_reached",
"plan_type": "free",
"resets_at": 4_000_000_000u64,
},
"status_code": 429,
"headers": {
"X-Codex-Plan-Type": "free",
"X-Codex-Primary-Used-Percent": "100",
"X-Codex-Primary-Window-Minutes": "43200",
"X-Codex-Primary-Reset-At": "4000000000",
},
});
let observation = adapter
.observe_upstream_event(&event)
.expect("account quota error should be observed");
let drain = observation
.drain
.expect("account quota error should retry the current turn");
let quota = observation
.quota_metadata
.expect("account quota metadata should be retained");
assert_eq!(drain.error_code, "codex_account_quota_exhausted");
assert!(drain.retry_current_turn);
assert_eq!(
drain.retry_exclusion_until_unix_secs,
Some(4_000_000_000u64)
);
assert_eq!(quota["allowed"], json!(false));
assert_eq!(quota["limit_reached"], json!(true));
assert!(aether_admin::provider::quota::codex_rate_limit_metadata_exhausted(&quota));
}
#[test]
fn only_known_codex_pre_response_signals_are_safe_to_rebind() {
let adapter = CodexResponsesWebSocketAdapter;
@@ -167,17 +167,43 @@ fn codex_quota_breaker_ttl(quota_metadata: &Value, now_unix_secs: u64) -> (u64,
pub(crate) fn codex_quota_exhaustion_reset_at(
quota_metadata: &Value,
now_unix_secs: u64,
) -> Option<u64> {
codex_quota_exhaustion_reset_at_for_prefixes(
quota_metadata,
now_unix_secs,
&["primary", "secondary"],
)
}
/// Returns the reset deadline for an exhausted model-scoped Codex window without promoting that
/// window to account-wide exhaustion.
pub(crate) fn codex_model_quota_exhaustion_reset_at(
quota_metadata: &Value,
now_unix_secs: u64,
) -> Option<u64> {
codex_quota_exhaustion_reset_at_for_prefixes(
quota_metadata,
now_unix_secs,
&["spark_primary", "spark_secondary"],
)
}
fn codex_quota_exhaustion_reset_at_for_prefixes(
quota_metadata: &Value,
now_unix_secs: u64,
window_prefixes: &[&str],
) -> Option<u64> {
let Some(metadata) = quota_metadata.as_object() else {
return None;
};
let exhausted_windows = ["primary", "secondary"]
.into_iter()
let exhausted_windows = window_prefixes
.iter()
.copied()
.filter(|prefix| codex_window_is_exhausted(metadata, prefix))
.collect::<Vec<_>>();
let prefixes = if exhausted_windows.is_empty() {
vec!["primary", "secondary"]
window_prefixes.to_vec()
} else {
exhausted_windows
};
+4 -3
View File
@@ -34,9 +34,10 @@ pub(crate) use self::classifier::{
LocalTransportFailoverClassification,
};
pub(crate) use self::codex_quota_breaker::{
codex_account_id_from_headers, codex_quota_breaker_blocks_candidate,
codex_quota_exhaustion_reset_at, install_codex_quota_exhaustion_breaker,
log_codex_quota_breaker_check_failure, log_codex_quota_breaker_install_failure,
codex_account_id_from_headers, codex_model_quota_exhaustion_reset_at,
codex_quota_breaker_blocks_candidate, codex_quota_exhaustion_reset_at,
install_codex_quota_exhaustion_breaker, log_codex_quota_breaker_check_failure,
log_codex_quota_breaker_install_failure,
};
pub(crate) use self::effects::{
apply_local_execution_effect, apply_local_stream_failure_effects,