mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix: codex free accounts incorrectly marked as quota exhausted (#366)
* fix: codex free accounts incorrectly marked as quota exhausted when skip_exhausted_accounts enabled - Cross-validate stored exhausted flag against window used_ratio in snapshot reader - Guard has_credits:false check with window data presence in upstream_metadata fallback - Add windows.is_empty() precondition to exhausted_by_credits in snapshot builder Free codex accounts have has_credits:false (they use window-based quotas, not credits), but the old logic treated this as credits depleted, skipping actual window usage checks. This caused all free accounts to be incorrectly marked exhausted. * test: cover codex free quota windows --------- Co-authored-by: root <root@ser488556369540.local> Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
@@ -627,7 +627,7 @@ fn build_codex_quota_status_snapshot(
|
||||
.min();
|
||||
let reset_at = quota_windows_min_reset_at(&windows);
|
||||
let exhausted_by_credits =
|
||||
credits_unlimited != Some(true) && credits_has_credits == Some(false);
|
||||
windows.is_empty() && credits_unlimited != Some(true) && credits_has_credits == Some(false);
|
||||
let exhausted_by_window = usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6);
|
||||
let exhausted = exhausted_by_credits || exhausted_by_window;
|
||||
|
||||
@@ -1655,6 +1655,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_key_status_snapshot_payload_keeps_codex_free_window_quota_available() {
|
||||
let mut key = sample_catalog_key();
|
||||
key.upstream_metadata = Some(json!({
|
||||
"codex": {
|
||||
"updated_at": 1_775_553_285u64,
|
||||
"plan_type": "free",
|
||||
"primary_used_percent": 64.0,
|
||||
"primary_reset_at": 1_900_000_000u64,
|
||||
"secondary_used_percent": 3.0,
|
||||
"secondary_reset_at": 1_900_500_000u64,
|
||||
"has_credits": false,
|
||||
"credits_balance": 0.0,
|
||||
"credits_unlimited": false
|
||||
}
|
||||
}));
|
||||
|
||||
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
||||
let quota = payload
|
||||
.get("quota")
|
||||
.and_then(Value::as_object)
|
||||
.expect("quota snapshot should be object");
|
||||
|
||||
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
||||
assert_eq!(quota.get("exhausted"), Some(&json!(false)));
|
||||
assert_eq!(quota.get("usage_ratio"), Some(&json!(0.64)));
|
||||
assert_eq!(
|
||||
quota
|
||||
.get("credits")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|credits| credits.get("has_credits")),
|
||||
Some(&json!(false))
|
||||
);
|
||||
assert_eq!(
|
||||
quota.get("windows").and_then(Value::as_array).map(Vec::len),
|
||||
Some(2usize)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_key_status_snapshot_payload_derives_codex_reset_at_from_countdown() {
|
||||
let mut key = sample_catalog_key();
|
||||
|
||||
@@ -1779,7 +1779,7 @@ async fn gateway_prefers_status_snapshot_codex_quota_over_stale_metadata() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_shows_codex_quota_reset_for_exhausted_zero_usage_snapshot() {
|
||||
async fn gateway_treats_stale_codex_exhausted_snapshot_as_available_when_windows_have_capacity() {
|
||||
let mut provider = sample_provider("provider-codex", "codex", 10).with_transport_fields(
|
||||
true,
|
||||
false,
|
||||
@@ -1881,11 +1881,8 @@ async fn gateway_shows_codex_quota_reset_for_exhausted_zero_usage_snapshot() {
|
||||
.expect("json body should parse");
|
||||
let keys = payload["keys"].as_array().expect("keys should be array");
|
||||
|
||||
assert_eq!(keys[0]["scheduling_status"], json!("blocked"));
|
||||
assert_eq!(
|
||||
keys[0]["scheduling_reason"],
|
||||
json!("account_quota_exhausted")
|
||||
);
|
||||
assert_eq!(keys[0]["scheduling_status"], json!("available"));
|
||||
assert_eq!(keys[0]["scheduling_reason"], json!("available"));
|
||||
assert_eq!(
|
||||
keys[0]["account_quota"],
|
||||
json!("周剩余 100.0% (7天0小时后重置) | 5H剩余 100.0% (5小时0分钟后重置)")
|
||||
|
||||
@@ -183,8 +183,28 @@ pub fn admin_pool_key_account_quota_exhausted(
|
||||
key: &StoredProviderCatalogKey,
|
||||
provider_type: &str,
|
||||
) -> bool {
|
||||
if let Some(exhausted) = admin_pool_key_quota_snapshot(key, provider_type)
|
||||
.and_then(|quota_snapshot| admin_pool_json_bool(quota_snapshot.get("exhausted")))
|
||||
if let Some(exhausted) =
|
||||
admin_pool_key_quota_snapshot(key, provider_type).and_then(|quota_snapshot| {
|
||||
let exhausted = admin_pool_json_bool(quota_snapshot.get("exhausted"))?;
|
||||
if exhausted {
|
||||
let windows_max_ratio = quota_snapshot
|
||||
.get("windows")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|w| !w.is_empty())
|
||||
.and_then(|windows| {
|
||||
windows
|
||||
.iter()
|
||||
.filter_map(Value::as_object)
|
||||
.filter_map(|w| w.get("used_ratio"))
|
||||
.filter_map(Value::as_f64)
|
||||
.max_by(f64::total_cmp)
|
||||
});
|
||||
if windows_max_ratio.is_some_and(|ratio| ratio < 1.0 - 1e-6) {
|
||||
return Some(false);
|
||||
}
|
||||
}
|
||||
Some(exhausted)
|
||||
})
|
||||
{
|
||||
return exhausted;
|
||||
}
|
||||
@@ -200,7 +220,9 @@ pub fn admin_pool_key_account_quota_exhausted(
|
||||
if admin_pool_json_bool(bucket.get("credits_unlimited")) == Some(true) {
|
||||
return false;
|
||||
}
|
||||
if admin_pool_json_bool(bucket.get("has_credits")) == Some(false) {
|
||||
let has_window_data = admin_pool_json_f64(bucket.get("primary_used_percent")).is_some()
|
||||
|| admin_pool_json_f64(bucket.get("secondary_used_percent")).is_some();
|
||||
if !has_window_data && admin_pool_json_bool(bucket.get("has_credits")) == Some(false) {
|
||||
return true;
|
||||
}
|
||||
admin_pool_json_f64(bucket.get("primary_used_percent"))
|
||||
@@ -660,6 +682,17 @@ mod tests {
|
||||
}))),
|
||||
"codex",
|
||||
));
|
||||
assert!(!admin_pool_key_account_quota_exhausted(
|
||||
&sample_key(Some(json!({
|
||||
"codex": {
|
||||
"has_credits": false,
|
||||
"credits_unlimited": false,
|
||||
"primary_used_percent": 64.0,
|
||||
"secondary_used_percent": 3.0
|
||||
}
|
||||
}))),
|
||||
"codex",
|
||||
));
|
||||
assert!(!admin_pool_key_account_quota_exhausted(
|
||||
&sample_key(Some(json!({
|
||||
"codex": {
|
||||
@@ -708,6 +741,40 @@ mod tests {
|
||||
assert!(!admin_pool_key_account_quota_exhausted(&key, "codex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clears_stale_codex_exhausted_snapshot_when_windows_have_capacity() {
|
||||
let mut key = sample_key(Some(json!({
|
||||
"codex": {
|
||||
"has_credits": false,
|
||||
"primary_used_percent": 100.0
|
||||
}
|
||||
})));
|
||||
key.status_snapshot = Some(json!({
|
||||
"quota": {
|
||||
"version": 2,
|
||||
"provider_type": "codex",
|
||||
"code": "exhausted",
|
||||
"exhausted": true,
|
||||
"usage_ratio": 0.0,
|
||||
"updated_at": 1_776_395_200u64,
|
||||
"windows": [
|
||||
{
|
||||
"code": "weekly",
|
||||
"used_ratio": 0.0,
|
||||
"remaining_ratio": 1.0
|
||||
},
|
||||
{
|
||||
"code": "5h",
|
||||
"used_ratio": 0.0,
|
||||
"remaining_ratio": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
assert!(!admin_pool_key_account_quota_exhausted(&key, "codex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_kiro_exhaustion_from_metadata() {
|
||||
assert!(admin_pool_key_account_quota_exhausted(
|
||||
|
||||
Reference in New Issue
Block a user