mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 00:17:45 +08:00
fix(pool): isolate model quotas and compact account display
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user