fix(pool): isolate model quotas and compact account display

This commit is contained in:
ZheFox
2026-09-01 10:15:57 +08:00
parent 9b819169d5
commit 2fe2600021
12 changed files with 383 additions and 43 deletions
+95 -2
View File
@@ -561,7 +561,7 @@ mod tests {
}
})));
let signals = service.member_signals("windsurf", &key, None);
let signals = service.member_signals("windsurf", &key, None, None);
assert!(!signals.quota_exhausted);
}
@@ -588,7 +588,7 @@ mod tests {
}
}));
let signals = service.member_signals("windsurf", &key, None);
let signals = service.member_signals("windsurf", &key, None, None);
assert!(signals.quota_exhausted);
}
@@ -845,6 +845,99 @@ mod tests {
assert!(provider_pool_key_account_quota_exhausted(&active, "codex"));
}
#[test]
fn antigravity_model_quota_exhaustion_does_not_block_other_models() {
let service = ProviderPoolService::with_builtin_adapters();
let mut key = sample_key(None);
key.status_snapshot = Some(json!({
"quota": {
"version": 2,
"provider_type": "antigravity",
"exhausted": false,
"windows": [
{
"code": "model:gemini-3.1-pro-high",
"scope": "model",
"model": "gemini-3.1-pro-high",
"used_ratio": 1.0,
"is_exhausted": true
},
{
"code": "model:gemini-3-flash-agent",
"scope": "model",
"model": "gemini-3-flash-agent",
"used_ratio": 0.1,
"is_exhausted": false
}
]
}
}));
let exhausted =
service.member_signals("antigravity", &key, None, Some("gemini-3.1-pro-high"));
let available =
service.member_signals("antigravity", &key, None, Some("gemini-3-flash-agent"));
assert!(exhausted.quota_exhausted);
assert!(!available.quota_exhausted);
}
#[test]
fn codex_standard_and_spark_quota_families_are_independent() {
let service = ProviderPoolService::with_builtin_adapters();
let mut standard_exhausted = sample_key(None);
standard_exhausted.status_snapshot = Some(json!({
"quota": {
"version": 2,
"provider_type": "codex",
"exhausted": true,
"allowed": false,
"limit_reached": true,
"windows": [
{ "code": "weekly", "used_ratio": 1.0, "is_exhausted": true },
{ "code": "5h", "used_ratio": 0.5, "is_exhausted": false },
{ "code": "spark_weekly", "used_ratio": 0.2, "is_exhausted": false },
{ "code": "spark_5h", "used_ratio": 0.1, "is_exhausted": false }
]
}
}));
let standard =
service.member_signals("codex", &standard_exhausted, None, Some("gpt-5.3-codex"));
let spark = service.member_signals(
"codex",
&standard_exhausted,
None,
Some("gpt-5.3-codex-spark"),
);
assert!(standard.quota_exhausted);
assert!(!standard.quota_hard_blocked);
assert!(!spark.quota_exhausted);
assert!(!spark.quota_hard_blocked);
let mut spark_exhausted = sample_key(None);
spark_exhausted.status_snapshot = Some(json!({
"quota": {
"version": 2,
"provider_type": "codex",
"exhausted": false,
"windows": [
{ "code": "weekly", "used_ratio": 0.2, "is_exhausted": false },
{ "code": "5h", "used_ratio": 0.1, "is_exhausted": false },
{ "code": "spark_weekly", "used_ratio": 1.0, "is_exhausted": true },
{ "code": "spark_5h", "used_ratio": 0.4, "is_exhausted": false }
]
}
}));
let standard =
service.member_signals("codex", &spark_exhausted, None, Some("gpt-5.3-codex"));
let spark =
service.member_signals("codex", &spark_exhausted, None, Some("gpt-5.3-codex-spark"));
assert!(!standard.quota_exhausted);
assert!(spark.quota_exhausted);
}
#[test]
fn provider_quota_exhaustion_metadata_expires_after_reset_at() {
let now = std::time::SystemTime::now()
@@ -16,6 +16,7 @@ pub struct ProviderPoolMemberInput<'a> {
pub provider_type: &'a str,
pub key: &'a StoredProviderCatalogKey,
pub auth_config: Option<&'a Map<String, Value>>,
pub provider_model_name: Option<&'a str>,
}
pub trait ProviderPoolAdapter: Send + Sync {
@@ -6,7 +6,9 @@ use serde_json::json;
use crate::capability::ProviderPoolCapabilities;
use crate::provider::{
provider_pool_endpoint_format_matches, provider_pool_matching_endpoint, ProviderPoolAdapter,
ProviderPoolMemberInput,
};
use crate::quota::provider_pool_model_quota_exhausted;
use crate::quota_refresh::ProviderPoolQuotaRequestSpec;
pub const ANTIGRAVITY_FETCH_AVAILABLE_MODELS_PATH: &str = "/v1internal:fetchAvailableModels";
@@ -26,6 +28,21 @@ impl ProviderPoolAdapter for AntigravityProviderPoolAdapter {
}
}
fn quota_exhausted(&self, input: &ProviderPoolMemberInput<'_>) -> bool {
input
.provider_model_name
.and_then(|model| {
provider_pool_model_quota_exhausted(input.key, input.provider_type, model)
})
.unwrap_or_else(|| {
crate::quota::provider_pool_quota_snapshot_exhausted_decision(
input.key,
input.provider_type,
)
.unwrap_or(false)
})
}
fn quota_refresh_endpoint(
&self,
endpoints: &[StoredProviderCatalogEndpoint],
@@ -12,8 +12,8 @@ use crate::provider::{
use crate::quota::{
provider_pool_current_unix_secs, provider_pool_json_bool, provider_pool_json_f64,
provider_pool_member_quota_snapshot, provider_pool_metadata_bucket,
provider_pool_quota_snapshot_exhausted_decision, provider_pool_reset_deadline_elapsed,
provider_pool_timestamp_unix_secs,
provider_pool_model_quota_exhausted, provider_pool_quota_snapshot_exhausted_decision,
provider_pool_reset_deadline_elapsed, provider_pool_timestamp_unix_secs,
};
use crate::quota_refresh::ProviderPoolQuotaRequestSpec;
@@ -49,6 +49,11 @@ impl ProviderPoolAdapter for CodexProviderPoolAdapter {
}
fn quota_exhausted(&self, input: &ProviderPoolMemberInput<'_>) -> bool {
if let Some(exhausted) = input.provider_model_name.and_then(|model| {
provider_pool_model_quota_exhausted(input.key, input.provider_type, model)
}) {
return exhausted;
}
if let Some(quota_snapshot) =
provider_pool_member_quota_snapshot(input.key, input.provider_type)
{
@@ -76,6 +81,11 @@ impl ProviderPoolAdapter for CodexProviderPoolAdapter {
}
fn quota_hard_blocked(&self, input: &ProviderPoolMemberInput<'_>) -> bool {
if input.provider_model_name.is_some_and(|model| {
provider_pool_model_quota_exhausted(input.key, input.provider_type, model).is_some()
}) {
return false;
}
codex_explicit_quota_block_active(input.key, input.provider_type)
}
+52
View File
@@ -15,6 +15,7 @@ pub fn provider_pool_key_account_quota_exhausted(
provider_type,
key,
auth_config: None,
provider_model_name: None,
})
}
@@ -27,9 +28,60 @@ pub fn provider_pool_key_quota_hard_blocked(
provider_type,
key,
auth_config: None,
provider_model_name: None,
})
}
pub(crate) fn provider_pool_model_quota_exhausted(
key: &StoredProviderCatalogKey,
provider_type: &str,
provider_model_name: &str,
) -> Option<bool> {
let quota_snapshot = provider_pool_member_quota_snapshot(key, provider_type)?;
let windows = quota_snapshot.get("windows")?.as_array()?;
let normalized_provider = provider_type.trim().to_ascii_lowercase();
let normalized_model = provider_model_name.trim().to_ascii_lowercase();
let matches_window = |window: &Map<String, Value>| {
let code = window
.get("code")
.and_then(Value::as_str)
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
if normalized_provider == "codex" {
let spark_model = normalized_model.contains("spark");
return code.starts_with("spark_") == spark_model;
}
if normalized_provider == "antigravity" {
return window
.get("model")
.and_then(Value::as_str)
.is_some_and(|model| model.trim().eq_ignore_ascii_case(&normalized_model));
}
false
};
let matching_windows = windows
.iter()
.filter_map(Value::as_object)
.filter(|window| matches_window(window))
.collect::<Vec<_>>();
if matching_windows.is_empty() {
return None;
}
let now_unix_secs = provider_pool_current_unix_secs();
let snapshot_observed_at = provider_pool_timestamp_unix_secs(quota_snapshot.get("observed_at"))
.or_else(|| provider_pool_timestamp_unix_secs(quota_snapshot.get("updated_at")));
Some(matching_windows.iter().any(|window| {
provider_pool_quota_window_is_exhausted(window)
&& !now_unix_secs.is_some_and(|now| {
provider_pool_reset_deadline_elapsed(window, snapshot_observed_at, now)
})
}))
}
pub fn provider_pool_member_quota_snapshot<'a>(
key: &'a StoredProviderCatalogKey,
provider_type: &str,
@@ -123,12 +123,14 @@ impl ProviderPoolService {
provider_type: &str,
key: &StoredProviderCatalogKey,
auth_config: Option<&Map<String, Value>>,
provider_model_name: Option<&str>,
) -> aether_pool_core::PoolMemberSignals {
let adapter = self.adapter(provider_type);
let input = ProviderPoolMemberInput {
provider_type,
key,
auth_config,
provider_model_name,
};
adapter.member_signals(&input)
}