fix(usage): improve usage-query resilience and error surfacing

- useUsageQuery: retry once + keep-last-good — show the last successful
  result for up to 10min when a query fails transiently (network/timeout/
  HTTP 5xx), so a single blip no longer flips the card to red. Deterministic
  failures (auth, empty key, unknown provider, 4xx) surface immediately and
  clear the snapshot so a stale quota can't resurface after credentials change.
- bump native balance/coding-plan/subscription request timeouts 10s -> 15s
  for slow cross-border endpoints.
- coding_plan: return explicit errors ("API key is empty" / "Unknown coding
  plan provider") instead of a blank failure, mirroring balance.
- add unit tests for keep-last-good and transient/deterministic classification.
This commit is contained in:
Jason
2026-06-14 19:25:58 +08:00
parent 9a3d6a4e84
commit 89ff2d58d1
5 changed files with 321 additions and 19 deletions
+9 -7
View File
@@ -95,7 +95,7 @@ async fn query_kimi(api_key: &str) -> SubscriptionQuota {
.get("https://api.kimi.com/coding/v1/usages")
.header("Authorization", format!("Bearer {api_key}"))
.header("Accept", "application/json")
.timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(15))
.send()
.await;
@@ -308,7 +308,7 @@ async fn query_zhipu(base_url: &str, api_key: &str) -> SubscriptionQuota {
.header("Authorization", api_key) // 注意:智谱不加 Bearer 前缀
.header("Content-Type", "application/json")
.header("Accept-Language", "en-US,en")
.timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(15))
.send()
.await;
@@ -391,7 +391,7 @@ async fn query_minimax(api_key: &str, is_cn: bool) -> SubscriptionQuota {
.get(&url)
.header("Authorization", format!("Bearer {api_key}"))
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(15))
.send()
.await;
@@ -463,7 +463,7 @@ async fn query_zenmux(base_url: &str, api_key: &str) -> SubscriptionQuota {
.get(base_url)
.header("Authorization", format!("Bearer {api_key}"))
.header("Accept", "application/json")
.timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(15))
.send()
.await;
@@ -666,7 +666,8 @@ pub async fn get_coding_plan_quota(
success: false,
tiers: vec![],
extra_usage: None,
error: None,
// 与 balance::get_balance 一致:给出明确错误,避免 footer 显示无信息的失败
error: Some("API key is empty".to_string()),
queried_at: None,
});
}
@@ -681,9 +682,10 @@ pub async fn get_coding_plan_quota(
success: false,
tiers: vec![],
extra_usage: None,
error: None,
// 域名未命中已知套餐供应商(如第三方中转站):给出明确错误而非静默失败
error: Some("Unknown coding plan provider".to_string()),
queried_at: None,
})
});
}
};