mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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();
|
.min();
|
||||||
let reset_at = quota_windows_min_reset_at(&windows);
|
let reset_at = quota_windows_min_reset_at(&windows);
|
||||||
let exhausted_by_credits =
|
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_by_window = usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6);
|
||||||
let exhausted = exhausted_by_credits || exhausted_by_window;
|
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]
|
#[test]
|
||||||
fn provider_key_status_snapshot_payload_derives_codex_reset_at_from_countdown() {
|
fn provider_key_status_snapshot_payload_derives_codex_reset_at_from_countdown() {
|
||||||
let mut key = sample_catalog_key();
|
let mut key = sample_catalog_key();
|
||||||
|
|||||||
@@ -1779,7 +1779,7 @@ async fn gateway_prefers_status_snapshot_codex_quota_over_stale_metadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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(
|
let mut provider = sample_provider("provider-codex", "codex", 10).with_transport_fields(
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -1881,11 +1881,8 @@ async fn gateway_shows_codex_quota_reset_for_exhausted_zero_usage_snapshot() {
|
|||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
let keys = payload["keys"].as_array().expect("keys should be array");
|
let keys = payload["keys"].as_array().expect("keys should be array");
|
||||||
|
|
||||||
assert_eq!(keys[0]["scheduling_status"], json!("blocked"));
|
assert_eq!(keys[0]["scheduling_status"], json!("available"));
|
||||||
assert_eq!(
|
assert_eq!(keys[0]["scheduling_reason"], json!("available"));
|
||||||
keys[0]["scheduling_reason"],
|
|
||||||
json!("account_quota_exhausted")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keys[0]["account_quota"],
|
keys[0]["account_quota"],
|
||||||
json!("周剩余 100.0% (7天0小时后重置) | 5H剩余 100.0% (5小时0分钟后重置)")
|
json!("周剩余 100.0% (7天0小时后重置) | 5H剩余 100.0% (5小时0分钟后重置)")
|
||||||
|
|||||||
@@ -183,8 +183,28 @@ pub fn admin_pool_key_account_quota_exhausted(
|
|||||||
key: &StoredProviderCatalogKey,
|
key: &StoredProviderCatalogKey,
|
||||||
provider_type: &str,
|
provider_type: &str,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if let Some(exhausted) = admin_pool_key_quota_snapshot(key, provider_type)
|
if let Some(exhausted) =
|
||||||
.and_then(|quota_snapshot| admin_pool_json_bool(quota_snapshot.get("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;
|
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) {
|
if admin_pool_json_bool(bucket.get("credits_unlimited")) == Some(true) {
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
admin_pool_json_f64(bucket.get("primary_used_percent"))
|
admin_pool_json_f64(bucket.get("primary_used_percent"))
|
||||||
@@ -660,6 +682,17 @@ mod tests {
|
|||||||
}))),
|
}))),
|
||||||
"codex",
|
"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(
|
assert!(!admin_pool_key_account_quota_exhausted(
|
||||||
&sample_key(Some(json!({
|
&sample_key(Some(json!({
|
||||||
"codex": {
|
"codex": {
|
||||||
@@ -708,6 +741,40 @@ mod tests {
|
|||||||
assert!(!admin_pool_key_account_quota_exhausted(&key, "codex"));
|
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]
|
#[test]
|
||||||
fn detects_kiro_exhaustion_from_metadata() {
|
fn detects_kiro_exhaustion_from_metadata() {
|
||||||
assert!(admin_pool_key_account_quota_exhausted(
|
assert!(admin_pool_key_account_quota_exhausted(
|
||||||
|
|||||||
Reference in New Issue
Block a user