mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 14:10:19 +08:00
4453 lines
154 KiB
Rust
4453 lines
154 KiB
Rust
use crate::handlers::shared::{json_string_list, unix_secs_to_rfc3339};
|
|
use crate::provider_key_auth::{
|
|
provider_key_auth_config_is_agent_identity, provider_key_auth_config_uses_header_authorization,
|
|
provider_key_auth_semantics, provider_key_can_export_oauth, provider_key_can_refresh_oauth,
|
|
provider_key_configured_api_formats, provider_key_inherits_provider_api_formats,
|
|
};
|
|
use crate::AppState;
|
|
use aether_admin::provider::quota as admin_provider_quota_pure;
|
|
use aether_admin::provider::status as admin_provider_status_pure;
|
|
#[cfg(test)]
|
|
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
|
use aether_crypto::{decrypt_python_fernet_ciphertext, encrypt_python_fernet_plaintext};
|
|
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
|
use aether_provider_pool::{
|
|
grok_pool_tier_from_quota_bucket, grok_supported_quota_windows_for_tier,
|
|
};
|
|
use aether_scheduler_core::provider_key_circuit_payload_is_active_open_at;
|
|
use serde_json::{json, Map, Value};
|
|
use std::borrow::Cow;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
const OAUTH_ACCOUNT_BLOCK_PREFIX: &str = "[ACCOUNT_BLOCK] ";
|
|
const OAUTH_EXPIRED_PREFIX: &str = "[OAUTH_EXPIRED] ";
|
|
const OAUTH_REFRESH_FAILED_PREFIX: &str = "[REFRESH_FAILED] ";
|
|
const OAUTH_REQUEST_FAILED_PREFIX: &str = "[REQUEST_FAILED] ";
|
|
|
|
pub(crate) fn provider_catalog_key_supports_format(
|
|
key: &StoredProviderCatalogKey,
|
|
provider_type: &str,
|
|
api_format: &str,
|
|
) -> bool {
|
|
if provider_key_inherits_provider_api_formats(key, provider_type) {
|
|
return true;
|
|
}
|
|
let formats = provider_key_configured_api_formats(key);
|
|
if formats.is_empty() {
|
|
return true;
|
|
}
|
|
formats
|
|
.iter()
|
|
.any(|candidate| crate::ai_serving::api_format_permission_covers(candidate, api_format))
|
|
}
|
|
|
|
pub(crate) fn decrypt_catalog_secret_with_fallbacks(
|
|
encryption_key: Option<&str>,
|
|
ciphertext: &str,
|
|
) -> Option<String> {
|
|
let encryption_key = encryption_key.map(str::trim).unwrap_or("");
|
|
if !encryption_key.is_empty() {
|
|
if let Ok(value) = decrypt_python_fernet_ciphertext(encryption_key, ciphertext) {
|
|
return Some(value);
|
|
}
|
|
}
|
|
for env_key in ["AETHER_GATEWAY_DATA_ENCRYPTION_KEY", "ENCRYPTION_KEY"] {
|
|
let Ok(fallback) = std::env::var(env_key) else {
|
|
continue;
|
|
};
|
|
let fallback = fallback.trim();
|
|
if fallback.is_empty() || fallback == encryption_key {
|
|
continue;
|
|
}
|
|
if let Ok(value) = decrypt_python_fernet_ciphertext(fallback, ciphertext) {
|
|
return Some(value);
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
if encryption_key != DEVELOPMENT_ENCRYPTION_KEY {
|
|
if let Ok(value) = decrypt_python_fernet_ciphertext(DEVELOPMENT_ENCRYPTION_KEY, ciphertext)
|
|
{
|
|
return Some(value);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(crate) fn effective_catalog_encryption_key(state: &AppState) -> Option<Cow<'_, str>> {
|
|
let encryption_key = state.encryption_key().map(str::trim).unwrap_or("");
|
|
if !encryption_key.is_empty() {
|
|
return Some(Cow::Borrowed(encryption_key));
|
|
}
|
|
for env_key in ["AETHER_GATEWAY_DATA_ENCRYPTION_KEY", "ENCRYPTION_KEY"] {
|
|
let Ok(candidate) = std::env::var(env_key) else {
|
|
continue;
|
|
};
|
|
let trimmed = candidate.trim();
|
|
if !trimmed.is_empty() {
|
|
return Some(if trimmed.len() == candidate.len() {
|
|
Cow::Owned(candidate)
|
|
} else {
|
|
Cow::Owned(trimmed.to_string())
|
|
});
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
{
|
|
return Some(Cow::Borrowed(DEVELOPMENT_ENCRYPTION_KEY));
|
|
}
|
|
#[allow(unreachable_code)]
|
|
None
|
|
}
|
|
|
|
pub(crate) fn encrypt_catalog_secret_with_fallbacks(
|
|
state: &AppState,
|
|
plaintext: &str,
|
|
) -> Option<String> {
|
|
let encryption_key = effective_catalog_encryption_key(state)?;
|
|
encrypt_python_fernet_plaintext(encryption_key.as_ref(), plaintext).ok()
|
|
}
|
|
|
|
pub(crate) fn take_secret_prefix(value: &str, prefix_chars: usize) -> &str {
|
|
let end = value
|
|
.char_indices()
|
|
.nth(prefix_chars)
|
|
.map(|(index, _)| index)
|
|
.unwrap_or(value.len());
|
|
&value[..end]
|
|
}
|
|
|
|
pub(crate) fn take_secret_suffix(value: &str, suffix_chars: usize) -> &str {
|
|
if suffix_chars == 0 {
|
|
return &value[value.len()..];
|
|
}
|
|
|
|
let start = value
|
|
.char_indices()
|
|
.rev()
|
|
.nth(suffix_chars - 1)
|
|
.map(|(index, _)| index)
|
|
.unwrap_or(0);
|
|
&value[start..]
|
|
}
|
|
|
|
pub(crate) fn masked_catalog_api_key(state: &AppState, key: &StoredProviderCatalogKey) -> String {
|
|
match key.auth_type.trim() {
|
|
"service_account" | "vertex_ai" => "[Service Account]".to_string(),
|
|
"oauth" => {
|
|
if provider_key_auth_config_uses_header_authorization(
|
|
parse_catalog_auth_config_json(state, key).as_ref(),
|
|
) {
|
|
"[OAuth Header]".to_string()
|
|
} else {
|
|
"[OAuth Token]".to_string()
|
|
}
|
|
}
|
|
_ => {
|
|
let Some(ciphertext) = key
|
|
.encrypted_api_key
|
|
.as_deref()
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
else {
|
|
return "[未设置]".to_string();
|
|
};
|
|
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
|
|
.map(|value| {
|
|
if value.chars().count() <= 12 {
|
|
format!("{value}***")
|
|
} else {
|
|
format!(
|
|
"{}***{}",
|
|
take_secret_prefix(&value, 8),
|
|
take_secret_suffix(&value, 4)
|
|
)
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "***ERROR***".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn masked_catalog_api_key_for_provider(
|
|
state: &AppState,
|
|
key: &StoredProviderCatalogKey,
|
|
provider_type: &str,
|
|
) -> String {
|
|
let auth_config = parse_catalog_auth_config_json(state, key);
|
|
if provider_key_auth_config_is_agent_identity(provider_type, auth_config.as_ref()) {
|
|
"[Agent Identity]".to_string()
|
|
} else {
|
|
masked_catalog_api_key(state, key)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_catalog_auth_config_json(
|
|
state: &AppState,
|
|
key: &StoredProviderCatalogKey,
|
|
) -> Option<serde_json::Map<String, serde_json::Value>> {
|
|
let ciphertext = key.encrypted_auth_config.as_deref()?.trim();
|
|
if ciphertext.is_empty() {
|
|
return None;
|
|
}
|
|
let plaintext = decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)?;
|
|
serde_json::from_str::<serde_json::Value>(&plaintext)
|
|
.ok()?
|
|
.as_object()
|
|
.cloned()
|
|
}
|
|
|
|
pub(crate) fn default_provider_key_status_snapshot() -> serde_json::Value {
|
|
json!({
|
|
"oauth": {
|
|
"code": "none",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"expires_at": serde_json::Value::Null,
|
|
"invalid_at": serde_json::Value::Null,
|
|
"source": serde_json::Value::Null,
|
|
"requires_reauth": false,
|
|
"expiring_soon": false,
|
|
},
|
|
"account": {
|
|
"code": "ok",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"blocked": false,
|
|
"source": serde_json::Value::Null,
|
|
"recoverable": false,
|
|
},
|
|
"quota": {
|
|
"code": "unknown",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"exhausted": false,
|
|
"usage_ratio": serde_json::Value::Null,
|
|
"updated_at": serde_json::Value::Null,
|
|
"reset_seconds": serde_json::Value::Null,
|
|
"plan_type": serde_json::Value::Null,
|
|
}
|
|
})
|
|
}
|
|
|
|
fn default_oauth_status_snapshot_value() -> Value {
|
|
default_provider_key_status_snapshot()
|
|
.get("oauth")
|
|
.cloned()
|
|
.unwrap_or_else(|| {
|
|
json!({
|
|
"code": "none",
|
|
"label": Value::Null,
|
|
"reason": Value::Null,
|
|
"expires_at": Value::Null,
|
|
"invalid_at": Value::Null,
|
|
"source": Value::Null,
|
|
"requires_reauth": false,
|
|
"expiring_soon": false,
|
|
})
|
|
})
|
|
}
|
|
|
|
fn trimmed_oauth_invalid_reason(reason: Option<&str>) -> Option<String> {
|
|
reason
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
}
|
|
|
|
fn tagged_oauth_invalid_reason(reason: Option<&str>, prefix: &str) -> Option<String> {
|
|
reason.and_then(|value| {
|
|
value
|
|
.lines()
|
|
.map(str::trim)
|
|
.find_map(|line| line.strip_prefix(prefix))
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
})
|
|
}
|
|
|
|
fn oauth_access_token_expired(expires_at_unix_secs: Option<u64>, now_unix_secs: u64) -> bool {
|
|
expires_at_unix_secs.is_none_or(|expires_at| expires_at == 0 || expires_at <= now_unix_secs)
|
|
}
|
|
|
|
fn build_provider_key_oauth_status_snapshot(key: &StoredProviderCatalogKey) -> Value {
|
|
if !key.auth_type.trim().eq_ignore_ascii_case("oauth") {
|
|
return default_oauth_status_snapshot_value();
|
|
}
|
|
|
|
let now_unix_secs = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.ok()
|
|
.map(|duration| duration.as_secs())
|
|
.unwrap_or(0);
|
|
let expires_at_unix_secs = key.expires_at_unix_secs;
|
|
let invalid_at_unix_secs = key.oauth_invalid_at_unix_secs;
|
|
let invalid_reason = trimmed_oauth_invalid_reason(key.oauth_invalid_reason.as_deref());
|
|
|
|
if let Some(reason) =
|
|
tagged_oauth_invalid_reason(invalid_reason.as_deref(), OAUTH_EXPIRED_PREFIX)
|
|
{
|
|
let (code, label) =
|
|
admin_provider_status_pure::oauth_token_snapshot_status_parts(reason.as_str());
|
|
return json!({
|
|
"code": code,
|
|
"label": label,
|
|
"reason": reason,
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": invalid_at_unix_secs,
|
|
"source": "oauth_invalid",
|
|
"requires_reauth": code == "invalid",
|
|
"expiring_soon": false,
|
|
});
|
|
}
|
|
if let Some(reason) =
|
|
tagged_oauth_invalid_reason(invalid_reason.as_deref(), OAUTH_REFRESH_FAILED_PREFIX)
|
|
{
|
|
let access_token_expired = oauth_access_token_expired(expires_at_unix_secs, now_unix_secs);
|
|
return json!({
|
|
"code": if access_token_expired { "invalid" } else { "reauth_required" },
|
|
"label": if access_token_expired { "已失效" } else { "续期失败" },
|
|
"reason": reason,
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": invalid_at_unix_secs,
|
|
"source": "oauth_refresh",
|
|
"requires_reauth": true,
|
|
"usable_until_expiry": !access_token_expired,
|
|
"expiring_soon": false,
|
|
});
|
|
}
|
|
if let Some(reason) =
|
|
tagged_oauth_invalid_reason(invalid_reason.as_deref(), OAUTH_REQUEST_FAILED_PREFIX)
|
|
{
|
|
if admin_provider_quota_pure::codex_looks_like_token_invalidated(Some(&reason)) {
|
|
return json!({
|
|
"code": "invalid",
|
|
"label": "已失效",
|
|
"reason": reason,
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": invalid_at_unix_secs,
|
|
"source": "oauth_invalid",
|
|
"requires_reauth": true,
|
|
"expiring_soon": false,
|
|
});
|
|
}
|
|
return json!({
|
|
"code": "check_failed",
|
|
"label": "检查失败",
|
|
"reason": reason,
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": Value::Null,
|
|
"source": "oauth_request",
|
|
"requires_reauth": false,
|
|
"expiring_soon": false,
|
|
});
|
|
}
|
|
if invalid_reason
|
|
.as_deref()
|
|
.is_some_and(|reason| !reason.starts_with(OAUTH_ACCOUNT_BLOCK_PREFIX))
|
|
|| invalid_at_unix_secs.is_some()
|
|
{
|
|
return json!({
|
|
"code": "invalid",
|
|
"label": "已失效",
|
|
"reason": invalid_reason,
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": invalid_at_unix_secs,
|
|
"source": "oauth_invalid",
|
|
"requires_reauth": true,
|
|
"expiring_soon": false,
|
|
});
|
|
}
|
|
|
|
let Some(expires_at_unix_secs) = expires_at_unix_secs else {
|
|
return default_oauth_status_snapshot_value();
|
|
};
|
|
if expires_at_unix_secs <= now_unix_secs {
|
|
return json!({
|
|
"code": "expired",
|
|
"label": "已过期",
|
|
"reason": "Access Token 已过期,等待自动续期",
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": Value::Null,
|
|
"source": "expires_at",
|
|
"requires_reauth": false,
|
|
"expiring_soon": false,
|
|
});
|
|
}
|
|
|
|
let expiring_soon = expires_at_unix_secs.saturating_sub(now_unix_secs) < 24 * 60 * 60;
|
|
json!({
|
|
"code": if expiring_soon { "expiring" } else { "valid" },
|
|
"label": if expiring_soon { "即将过期" } else { "有效" },
|
|
"reason": Value::Null,
|
|
"expires_at": expires_at_unix_secs,
|
|
"invalid_at": Value::Null,
|
|
"source": "expires_at",
|
|
"requires_reauth": false,
|
|
"expiring_soon": expiring_soon,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn sync_provider_key_oauth_status_snapshot(
|
|
status_snapshot: Option<&Value>,
|
|
key: &StoredProviderCatalogKey,
|
|
) -> Option<Value> {
|
|
let mut snapshot = provider_key_status_snapshot_object(status_snapshot)
|
|
.or_else(|| default_provider_key_status_snapshot().as_object().cloned())
|
|
.unwrap_or_default();
|
|
snapshot.insert(
|
|
"oauth".to_string(),
|
|
build_provider_key_oauth_status_snapshot(key),
|
|
);
|
|
Some(Value::Object(snapshot))
|
|
}
|
|
|
|
fn build_provider_key_account_status_snapshot(
|
|
key: &StoredProviderCatalogKey,
|
|
provider_type: &str,
|
|
) -> Value {
|
|
let snapshot = admin_provider_status_pure::resolve_account_status_snapshot(
|
|
Some(provider_type),
|
|
key.upstream_metadata.as_ref(),
|
|
key.oauth_invalid_reason.as_deref(),
|
|
);
|
|
json!({
|
|
"code": snapshot.code,
|
|
"label": snapshot.label,
|
|
"reason": snapshot.reason,
|
|
"blocked": snapshot.blocked,
|
|
"source": snapshot.source,
|
|
"recoverable": snapshot.recoverable,
|
|
})
|
|
}
|
|
|
|
fn provider_key_status_snapshot_object(
|
|
status_snapshot: Option<&Value>,
|
|
) -> Option<Map<String, Value>> {
|
|
status_snapshot.and_then(|value| match value {
|
|
Value::Object(object) => Some(object.clone()),
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
fn provider_quota_metadata_bucket<'a>(
|
|
upstream_metadata: Option<&'a Value>,
|
|
provider_type: &str,
|
|
) -> Option<&'a Map<String, Value>> {
|
|
upstream_metadata
|
|
.and_then(Value::as_object)
|
|
.and_then(|metadata| metadata.get(&provider_type.trim().to_ascii_lowercase()))
|
|
.and_then(Value::as_object)
|
|
}
|
|
|
|
fn provider_quota_timestamp_unix_secs(value: Option<&Value>) -> Option<u64> {
|
|
let mut parsed = match value {
|
|
Some(Value::Number(number)) => number.as_f64(),
|
|
Some(Value::String(text)) => {
|
|
let text = text.trim();
|
|
if let Ok(timestamp) = text.parse::<f64>() {
|
|
Some(timestamp)
|
|
} else {
|
|
return chrono::DateTime::parse_from_rfc3339(text)
|
|
.ok()?
|
|
.timestamp()
|
|
.try_into()
|
|
.ok();
|
|
}
|
|
}
|
|
_ => None,
|
|
}?;
|
|
if !parsed.is_finite() || parsed <= 0.0 {
|
|
return None;
|
|
}
|
|
if parsed > 1_000_000_000_000.0 {
|
|
parsed /= 1000.0;
|
|
}
|
|
Some(parsed.floor() as u64)
|
|
}
|
|
|
|
fn provider_quota_model_bucket(metadata: &Map<String, Value>) -> Option<&Map<String, Value>> {
|
|
metadata
|
|
.get("quota_by_model")
|
|
.or_else(|| metadata.get("models"))
|
|
.and_then(Value::as_object)
|
|
}
|
|
|
|
fn quota_window_reset_seconds(
|
|
observed_at_unix_secs: Option<u64>,
|
|
reset_at_unix_secs: Option<u64>,
|
|
) -> Option<u64> {
|
|
observed_at_unix_secs
|
|
.zip(reset_at_unix_secs)
|
|
.map(|(observed_at, reset_at)| reset_at.saturating_sub(observed_at))
|
|
}
|
|
|
|
fn chatgpt_web_image_quota_limit(
|
|
metadata: &Map<String, Value>,
|
|
remaining: Option<f64>,
|
|
) -> Option<f64> {
|
|
let explicit_limit = metadata
|
|
.get("image_quota_total")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.filter(|value| *value > 0.0);
|
|
let plan_type = metadata
|
|
.get("plan_type")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(|value| value.to_ascii_lowercase());
|
|
let limit_source = metadata
|
|
.get("image_quota_limit_source")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty());
|
|
if let Some(limit) = explicit_limit {
|
|
if !chatgpt_web_image_quota_limit_is_legacy_free_default(
|
|
limit,
|
|
limit_source,
|
|
plan_type.as_deref(),
|
|
remaining,
|
|
) {
|
|
return Some(limit);
|
|
}
|
|
}
|
|
|
|
remaining.filter(|value| *value > 0.0)
|
|
}
|
|
|
|
fn chatgpt_web_image_quota_limit_is_legacy_free_default(
|
|
limit: f64,
|
|
limit_source: Option<&str>,
|
|
plan_type: Option<&str>,
|
|
remaining: Option<f64>,
|
|
) -> bool {
|
|
let plan_type_is_free = plan_type
|
|
.map(str::trim)
|
|
.is_some_and(|value| value.eq_ignore_ascii_case("free"));
|
|
if !plan_type_is_free || limit_source.is_some() {
|
|
return false;
|
|
}
|
|
if (limit - 25.0).abs() > f64::EPSILON {
|
|
return false;
|
|
}
|
|
remaining.is_none_or(|value| value < limit)
|
|
}
|
|
|
|
fn model_quota_window_snapshot(
|
|
model_name: &str,
|
|
item: &Map<String, Value>,
|
|
observed_at_unix_secs: Option<u64>,
|
|
) -> Option<Value> {
|
|
let remaining_value = item
|
|
.get("remaining")
|
|
.or_else(|| item.get("remaining_value"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let limit_value = item
|
|
.get("total")
|
|
.or_else(|| item.get("limit_value"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.filter(|value| *value > 0.0);
|
|
let used_value = item
|
|
.get("used")
|
|
.or_else(|| item.get("used_value"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.or_else(|| {
|
|
remaining_value
|
|
.zip(limit_value)
|
|
.map(|(remaining, limit)| (limit - remaining).max(0.0))
|
|
});
|
|
let used_ratio = item
|
|
.get("used_percent")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.map(|value| (value / 100.0).clamp(0.0, 1.0))
|
|
.or_else(|| {
|
|
item.get("remaining_fraction")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.map(|value| (1.0 - value.clamp(0.0, 1.0)).clamp(0.0, 1.0))
|
|
});
|
|
let remaining_ratio = item
|
|
.get("remaining_fraction")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.map(|value| value.clamp(0.0, 1.0))
|
|
.or_else(|| used_ratio.map(|value| (1.0 - value).max(0.0)));
|
|
let reset_at = provider_quota_timestamp_unix_secs(
|
|
item.get("reset_at")
|
|
.or_else(|| item.get("next_reset_at"))
|
|
.or_else(|| item.get("reset_time"))
|
|
.or_else(|| item.get("next_reset_time")),
|
|
);
|
|
let reset_seconds = quota_window_reset_seconds(observed_at_unix_secs, reset_at);
|
|
let is_exhausted = item
|
|
.get("is_exhausted")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
.or_else(|| used_ratio.map(|value| value >= 1.0 - 1e-6));
|
|
|
|
if used_ratio.is_none()
|
|
&& remaining_ratio.is_none()
|
|
&& reset_at.is_none()
|
|
&& reset_seconds.is_none()
|
|
&& is_exhausted.is_none()
|
|
&& remaining_value.is_none()
|
|
&& limit_value.is_none()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let mut window = Map::new();
|
|
let label = item
|
|
.get("display_name")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or(model_name);
|
|
window.insert("code".to_string(), json!(format!("model:{model_name}")));
|
|
window.insert("label".to_string(), json!(label));
|
|
window.insert("scope".to_string(), json!("model"));
|
|
window.insert("unit".to_string(), json!("percent"));
|
|
window.insert("model".to_string(), json!(model_name));
|
|
window.insert("used_ratio".to_string(), json!(used_ratio));
|
|
window.insert("remaining_ratio".to_string(), json!(remaining_ratio));
|
|
window.insert("used_value".to_string(), json!(used_value));
|
|
window.insert("remaining_value".to_string(), json!(remaining_value));
|
|
window.insert("limit_value".to_string(), json!(limit_value));
|
|
window.insert("reset_at".to_string(), json!(reset_at));
|
|
window.insert("reset_seconds".to_string(), json!(reset_seconds));
|
|
window.insert("is_exhausted".to_string(), json!(is_exhausted));
|
|
Some(Value::Object(window))
|
|
}
|
|
|
|
fn canonical_antigravity_model_label(model_name: &str) -> Option<&'static str> {
|
|
match model_name.trim() {
|
|
"claude-opus-4-6-thinking" => Some("Claude Opus 4.6 (Thinking)"),
|
|
"claude-sonnet-4-6" | "claude-sonnet-4-6-thinking" => Some("Claude Sonnet 4.6 (Thinking)"),
|
|
"gemini-3-flash-agent" => Some("Gemini 3.5 Flash (High)"),
|
|
"gemini-3.5-flash-low" => Some("Gemini 3.5 Flash (Medium)"),
|
|
"gemini-3.5-flash-extra-low" => Some("Gemini 3.5 Flash (Low)"),
|
|
"gemini-3.1-pro-high" | "gemini-pro-agent" => Some("Gemini 3.1 Pro (High)"),
|
|
"gemini-3.1-pro-low" => Some("Gemini 3.1 Pro (Low)"),
|
|
"gemini-3.1-flash-image" => Some("Gemini 3.1 Flash Image"),
|
|
"gemini-3.1-flash-lite" => Some("Gemini 3.1 Flash Lite"),
|
|
"gemini-3-flash" => Some("Gemini 3 Flash"),
|
|
"gemini-2.5-pro" => Some("Gemini 2.5 Pro"),
|
|
"gemini-2.5-flash-thinking" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" => {
|
|
Some("Gemini 3.1 Flash Lite")
|
|
}
|
|
"gpt-oss-120b-medium" => Some("GPT-OSS 120B (Medium)"),
|
|
"tab_flash_lite_preview" => Some("Tab Flash Lite Preview"),
|
|
"tab_jump_flash_lite_preview" => Some("Tab Jump Flash Lite Preview"),
|
|
"models/proactive-observer" => Some("Proactive Observer"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn antigravity_model_quota_window_snapshot(
|
|
model_name: &str,
|
|
item: &Map<String, Value>,
|
|
observed_at_unix_secs: Option<u64>,
|
|
) -> Option<Value> {
|
|
let mut window = model_quota_window_snapshot(model_name, item, observed_at_unix_secs)?;
|
|
if let Some(label) = canonical_antigravity_model_label(model_name) {
|
|
if let Some(window) = window.as_object_mut() {
|
|
window.insert("label".to_string(), json!(label));
|
|
}
|
|
}
|
|
Some(window)
|
|
}
|
|
|
|
fn antigravity_grouped_quota_window_snapshots(
|
|
metadata: &Map<String, Value>,
|
|
observed_at_unix_secs: Option<u64>,
|
|
) -> Vec<Value> {
|
|
let mut windows = Vec::new();
|
|
let Some(groups) = metadata.get("quota_groups").and_then(Value::as_array) else {
|
|
return windows;
|
|
};
|
|
|
|
for (group_index, group) in groups.iter().filter_map(Value::as_object).enumerate() {
|
|
let group_code = group
|
|
.get("group_id")
|
|
.or_else(|| group.get("groupId"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.unwrap_or_else(|| format!("group:{group_index}"));
|
|
let group_label = group
|
|
.get("display_name")
|
|
.or_else(|| group.get("displayName"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.unwrap_or_else(|| format!("Quota group {}", group_index + 1));
|
|
let group_description = group
|
|
.get("description")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
|
|
for (bucket_index, bucket) in group
|
|
.get("buckets")
|
|
.and_then(Value::as_array)
|
|
.into_iter()
|
|
.flatten()
|
|
.filter_map(Value::as_object)
|
|
.enumerate()
|
|
{
|
|
let bucket_id = bucket
|
|
.get("bucket_id")
|
|
.or_else(|| bucket.get("bucketId"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.unwrap_or_else(|| format!("bucket-{}", bucket_index + 1));
|
|
let Some(mut window) =
|
|
model_quota_window_snapshot(&bucket_id, bucket, observed_at_unix_secs)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(window) = window.as_object_mut() else {
|
|
continue;
|
|
};
|
|
let period = bucket
|
|
.get("window")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
let bucket_detail = bucket
|
|
.get("display_name")
|
|
.or_else(|| bucket.get("displayName"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.or_else(|| period.clone());
|
|
let bucket_label = bucket_detail
|
|
.filter(|detail| !detail.eq_ignore_ascii_case(&group_label))
|
|
.map(|detail| format!("{group_label} · {detail}"))
|
|
.unwrap_or_else(|| group_label.clone());
|
|
let description = bucket
|
|
.get("description")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.or_else(|| group_description.clone());
|
|
|
|
window.insert(
|
|
"code".to_string(),
|
|
json!(format!("group:{group_index}:{bucket_id}")),
|
|
);
|
|
window.insert("label".to_string(), json!(bucket_label));
|
|
window.insert("scope".to_string(), json!("quota_group"));
|
|
window.remove("model");
|
|
window.insert("quota_group".to_string(), json!(group_code));
|
|
window.insert("quota_group_label".to_string(), json!(group_label));
|
|
window.insert("bucket_id".to_string(), json!(bucket_id));
|
|
if let Some(period) = period {
|
|
window.insert("window".to_string(), json!(period));
|
|
}
|
|
if let Some(description) = description {
|
|
window.insert("description".to_string(), json!(description));
|
|
}
|
|
windows.push(Value::Object(window.clone()));
|
|
}
|
|
}
|
|
|
|
windows
|
|
}
|
|
|
|
fn provider_quota_metadata_string(
|
|
metadata: &Map<String, Value>,
|
|
fields: &[&str],
|
|
) -> Option<String> {
|
|
fields.iter().find_map(|field| {
|
|
metadata
|
|
.get(*field)
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
})
|
|
}
|
|
|
|
fn provider_quota_metadata_value_by_path<'a>(
|
|
metadata: &'a Map<String, Value>,
|
|
path: &[&str],
|
|
) -> Option<&'a Value> {
|
|
let (first, rest) = path.split_first()?;
|
|
let mut current = metadata.get(*first)?;
|
|
for segment in rest {
|
|
current = current.as_object()?.get(*segment)?;
|
|
}
|
|
Some(current)
|
|
}
|
|
|
|
fn provider_quota_metadata_number_by_paths(
|
|
metadata: &Map<String, Value>,
|
|
paths: &[&[&str]],
|
|
) -> Option<f64> {
|
|
paths.iter().find_map(|path| {
|
|
provider_quota_metadata_value_by_path(metadata, path)
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.filter(|value| value.is_finite())
|
|
})
|
|
}
|
|
|
|
fn provider_quota_metadata_bool_by_paths(
|
|
metadata: &Map<String, Value>,
|
|
paths: &[&[&str]],
|
|
) -> Option<bool> {
|
|
paths.iter().find_map(|path| {
|
|
provider_quota_metadata_value_by_path(metadata, path)
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
})
|
|
}
|
|
|
|
fn provider_quota_metadata_string_by_paths(
|
|
metadata: &Map<String, Value>,
|
|
paths: &[&[&str]],
|
|
) -> Option<String> {
|
|
paths.iter().find_map(|path| {
|
|
provider_quota_metadata_value_by_path(metadata, path)
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
})
|
|
}
|
|
|
|
fn quota_windows_usage_ratio(windows: &[Value]) -> Option<f64> {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.filter_map(|window| window.get("used_ratio"))
|
|
.filter_map(Value::as_f64)
|
|
.max_by(f64::total_cmp)
|
|
}
|
|
|
|
fn quota_windows_min_reset_seconds(windows: &[Value]) -> Option<u64> {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.filter_map(|window| window.get("reset_seconds"))
|
|
.filter_map(admin_provider_quota_pure::coerce_json_u64)
|
|
.min()
|
|
}
|
|
|
|
fn quota_windows_min_reset_at(windows: &[Value]) -> Option<u64> {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.filter_map(|window| window.get("reset_at"))
|
|
.filter_map(|value| provider_quota_timestamp_unix_secs(Some(value)))
|
|
.min()
|
|
}
|
|
|
|
fn quota_windows_all_exhausted(windows: &[Value]) -> bool {
|
|
let mut total = 0usize;
|
|
let mut exhausted = 0usize;
|
|
for window in windows.iter().filter_map(Value::as_object) {
|
|
total += 1;
|
|
let is_exhausted = window
|
|
.get("is_exhausted")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
.or_else(|| {
|
|
window
|
|
.get("used_ratio")
|
|
.and_then(Value::as_f64)
|
|
.map(|value| value >= 1.0 - 1e-6)
|
|
})
|
|
.unwrap_or(false);
|
|
if is_exhausted {
|
|
exhausted += 1;
|
|
}
|
|
}
|
|
total > 0 && exhausted == total
|
|
}
|
|
|
|
fn preserve_quota_window_usage_state(current_status_snapshot: Option<&Value>, quota: &mut Value) {
|
|
let Some(current_windows) = current_status_snapshot
|
|
.and_then(Value::as_object)
|
|
.and_then(|snapshot| snapshot.get("quota"))
|
|
.and_then(Value::as_object)
|
|
.and_then(|quota| quota.get("windows"))
|
|
.and_then(Value::as_array)
|
|
else {
|
|
return;
|
|
};
|
|
let Some(next_windows) = quota.get_mut("windows").and_then(Value::as_array_mut) else {
|
|
return;
|
|
};
|
|
|
|
for next_window in next_windows.iter_mut().filter_map(Value::as_object_mut) {
|
|
let Some(code) = next_window
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|code| !code.is_empty())
|
|
else {
|
|
continue;
|
|
};
|
|
let current_window =
|
|
current_windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|current_window| {
|
|
current_window
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.is_some_and(|current_code| current_code.eq_ignore_ascii_case(code))
|
|
});
|
|
let Some(current_window) = current_window else {
|
|
continue;
|
|
};
|
|
let current_reset_at = current_window
|
|
.get("reset_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
let next_reset_at = next_window
|
|
.get("reset_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
if current_reset_at.is_none() || current_reset_at != next_reset_at {
|
|
continue;
|
|
}
|
|
|
|
if next_window
|
|
.get("window_minutes")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.is_none()
|
|
{
|
|
if let Some(window_minutes) = current_window
|
|
.get("window_minutes")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.or_else(|| codex_default_window_minutes(code))
|
|
{
|
|
next_window.insert("window_minutes".to_string(), json!(window_minutes));
|
|
}
|
|
}
|
|
if let Some(usage_reset_at) = current_window
|
|
.get("usage_reset_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
{
|
|
next_window.insert("usage_reset_at".to_string(), json!(usage_reset_at));
|
|
}
|
|
if let Some(usage) = current_window.get("usage") {
|
|
next_window.insert("usage".to_string(), usage.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
fn codex_default_window_minutes(code: &str) -> Option<u64> {
|
|
if code.eq_ignore_ascii_case("5h") || code.eq_ignore_ascii_case("spark_5h") {
|
|
Some(300)
|
|
} else if code.eq_ignore_ascii_case("weekly") || code.eq_ignore_ascii_case("spark_weekly") {
|
|
Some(10_080)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn codex_quota_period_identity(window_minutes: u64) -> (String, String) {
|
|
const MINUTES_PER_HOUR: u64 = 60;
|
|
const MINUTES_PER_DAY: u64 = 24 * MINUTES_PER_HOUR;
|
|
const MINUTES_PER_WEEK: u64 = 7 * MINUTES_PER_DAY;
|
|
|
|
if window_minutes == 5 * MINUTES_PER_HOUR {
|
|
return ("5h".to_string(), "5H".to_string());
|
|
}
|
|
if window_minutes == MINUTES_PER_WEEK {
|
|
return ("weekly".to_string(), "周".to_string());
|
|
}
|
|
if (28 * MINUTES_PER_DAY..=31 * MINUTES_PER_DAY).contains(&window_minutes) {
|
|
return ("monthly".to_string(), "月".to_string());
|
|
}
|
|
|
|
let label = if window_minutes.is_multiple_of(MINUTES_PER_WEEK) {
|
|
format!("{}周", window_minutes / MINUTES_PER_WEEK)
|
|
} else if window_minutes.is_multiple_of(MINUTES_PER_DAY) {
|
|
format!("{}天", window_minutes / MINUTES_PER_DAY)
|
|
} else if window_minutes.is_multiple_of(MINUTES_PER_HOUR) {
|
|
format!("{}H", window_minutes / MINUTES_PER_HOUR)
|
|
} else {
|
|
format!("{window_minutes}分钟")
|
|
};
|
|
(format!("window_{window_minutes}m"), label)
|
|
}
|
|
|
|
fn codex_quota_window_identity(
|
|
fallback_code: &str,
|
|
fallback_label: &str,
|
|
window_minutes: Option<u64>,
|
|
) -> (String, String) {
|
|
let Some(window_minutes) = window_minutes else {
|
|
return (fallback_code.to_string(), fallback_label.to_string());
|
|
};
|
|
let is_spark = fallback_code.to_ascii_lowercase().starts_with("spark_");
|
|
let (code, label) = codex_quota_period_identity(window_minutes);
|
|
if is_spark {
|
|
(format!("spark_{code}"), format!("Spark {label}"))
|
|
} else {
|
|
(code, label)
|
|
}
|
|
}
|
|
|
|
fn codex_quota_window_snapshot(
|
|
metadata: &Map<String, Value>,
|
|
prefix: &str,
|
|
code: &str,
|
|
label: &str,
|
|
observed_at_unix_secs: Option<u64>,
|
|
) -> Option<Value> {
|
|
let used_percent_key = format!("{prefix}_used_percent");
|
|
let reset_seconds_key = format!("{prefix}_reset_seconds");
|
|
let reset_after_seconds_key = format!("{prefix}_reset_after_seconds");
|
|
let reset_at_key = format!("{prefix}_reset_at");
|
|
let window_minutes_key = format!("{prefix}_window_minutes");
|
|
|
|
let used_percent = metadata
|
|
.get(&used_percent_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let explicit_reset_at = metadata
|
|
.get(&reset_at_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
let reset_seconds = metadata
|
|
.get(&reset_seconds_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.or_else(|| {
|
|
metadata
|
|
.get(&reset_after_seconds_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
})
|
|
.or_else(|| {
|
|
observed_at_unix_secs
|
|
.zip(explicit_reset_at)
|
|
.map(|(observed_at, reset_at)| reset_at.saturating_sub(observed_at))
|
|
});
|
|
let reset_at = explicit_reset_at.or_else(|| {
|
|
observed_at_unix_secs
|
|
.zip(reset_seconds)
|
|
.map(|(observed_at, reset_seconds)| observed_at.saturating_add(reset_seconds))
|
|
});
|
|
let explicit_window_minutes = metadata
|
|
.get(&window_minutes_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
|
|
if explicit_window_minutes == Some(0) {
|
|
return None;
|
|
}
|
|
|
|
if used_percent.is_none()
|
|
&& reset_at.is_none()
|
|
&& reset_seconds.is_none()
|
|
&& explicit_window_minutes.is_none()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let window_minutes = explicit_window_minutes.or_else(|| codex_default_window_minutes(code));
|
|
let (code, label) = codex_quota_window_identity(code, label, window_minutes);
|
|
let used_ratio = used_percent.map(|value| (value / 100.0).clamp(0.0, 1.0));
|
|
let remaining_ratio = used_ratio.map(|value| (1.0 - value).max(0.0));
|
|
|
|
let mut window = Map::new();
|
|
window.insert("code".to_string(), json!(code));
|
|
window.insert("label".to_string(), json!(label));
|
|
window.insert("scope".to_string(), json!("account"));
|
|
window.insert("unit".to_string(), json!("percent"));
|
|
window.insert("used_ratio".to_string(), json!(used_ratio));
|
|
window.insert("remaining_ratio".to_string(), json!(remaining_ratio));
|
|
window.insert("reset_at".to_string(), json!(reset_at));
|
|
window.insert("reset_seconds".to_string(), json!(reset_seconds));
|
|
window.insert("window_minutes".to_string(), json!(window_minutes));
|
|
Some(Value::Object(window))
|
|
}
|
|
|
|
fn build_codex_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "codex")?;
|
|
let observed_at_unix_secs = metadata
|
|
.get("updated_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
let plan_type = metadata
|
|
.get("plan_type")
|
|
.and_then(Value::as_str)
|
|
.and_then(|value| admin_provider_quota_pure::normalize_codex_plan_type(Some(value)));
|
|
let credits_has_credits = metadata
|
|
.get("has_credits")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
|
let credits_balance = metadata
|
|
.get("credits_balance")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let credits_unlimited = metadata
|
|
.get("credits_unlimited")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
|
let allowed = metadata
|
|
.get("allowed")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
|
let limit_reached = metadata
|
|
.get("limit_reached")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool);
|
|
let reset_credits = build_codex_reset_credits_status_snapshot(metadata, observed_at_unix_secs);
|
|
|
|
let mut windows = [
|
|
codex_quota_window_snapshot(metadata, "primary", "weekly", "周", observed_at_unix_secs),
|
|
codex_quota_window_snapshot(metadata, "secondary", "5h", "5H", observed_at_unix_secs),
|
|
codex_quota_window_snapshot(
|
|
metadata,
|
|
"spark_primary",
|
|
"spark_5h",
|
|
"Spark 5H",
|
|
observed_at_unix_secs,
|
|
),
|
|
codex_quota_window_snapshot(
|
|
metadata,
|
|
"spark_secondary",
|
|
"spark_weekly",
|
|
"Spark 周",
|
|
observed_at_unix_secs,
|
|
),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.collect::<Vec<_>>();
|
|
if let Some(additional_windows) = metadata
|
|
.get("additional_quota_windows")
|
|
.and_then(Value::as_array)
|
|
{
|
|
windows.extend(
|
|
additional_windows
|
|
.iter()
|
|
.filter(|window| window.is_object())
|
|
.cloned(),
|
|
);
|
|
}
|
|
|
|
if windows.is_empty()
|
|
&& plan_type.is_none()
|
|
&& credits_has_credits.is_none()
|
|
&& credits_balance.is_none()
|
|
&& credits_unlimited.is_none()
|
|
&& allowed.is_none()
|
|
&& limit_reached.is_none()
|
|
&& reset_credits.is_none()
|
|
&& observed_at_unix_secs.is_none()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let primary_windows = windows
|
|
.iter()
|
|
.filter(|window| {
|
|
let code = window
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default();
|
|
let scope = window
|
|
.get("scope")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or_default();
|
|
scope.eq_ignore_ascii_case("account")
|
|
&& !code.to_ascii_lowercase().starts_with("spark_")
|
|
})
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
let usage_ratio = primary_windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.filter_map(|window| window.get("used_ratio"))
|
|
.filter_map(Value::as_f64)
|
|
.max_by(f64::total_cmp);
|
|
let reset_seconds = primary_windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.filter_map(|window| window.get("reset_seconds"))
|
|
.filter_map(admin_provider_quota_pure::coerce_json_u64)
|
|
.min();
|
|
let reset_at = quota_windows_min_reset_at(&primary_windows);
|
|
let explicitly_blocked = allowed == Some(false) || limit_reached == Some(true);
|
|
let explicitly_available =
|
|
!explicitly_blocked && (allowed == Some(true) || limit_reached == Some(false));
|
|
let exhausted_by_credits = !explicitly_available
|
|
&& primary_windows.is_empty()
|
|
&& credits_unlimited != Some(true)
|
|
&& credits_has_credits == Some(false);
|
|
let exhausted_by_window =
|
|
!explicitly_available && usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6);
|
|
let exhausted_by_signal = admin_provider_quota_pure::codex_rate_limit_metadata_exhausted(
|
|
&Value::Object(metadata.clone()),
|
|
);
|
|
let exhausted = exhausted_by_signal || exhausted_by_credits || exhausted_by_window;
|
|
|
|
let mut credits = Map::new();
|
|
if let Some(value) = credits_has_credits {
|
|
credits.insert("has_credits".to_string(), json!(value));
|
|
}
|
|
if let Some(value) = credits_balance {
|
|
credits.insert("balance".to_string(), json!(value));
|
|
}
|
|
if let Some(value) = credits_unlimited {
|
|
credits.insert("unlimited".to_string(), json!(value));
|
|
}
|
|
|
|
let reason = if exhausted_by_signal {
|
|
Some("上游已拒绝继续使用该账号")
|
|
} else if exhausted_by_credits {
|
|
Some("无可用积分")
|
|
} else if exhausted_by_window {
|
|
Some("额度窗口已耗尽")
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "codex",
|
|
"code": if exhausted { "exhausted" } else { "ok" },
|
|
"label": if exhausted { Some("额度耗尽") } else { None::<&str> },
|
|
"reason": reason,
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": plan_type,
|
|
"allowed": allowed,
|
|
"limit_reached": limit_reached,
|
|
"credits": if credits.is_empty() {
|
|
Value::Null
|
|
} else {
|
|
Value::Object(credits)
|
|
},
|
|
"reset_credits": reset_credits,
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn build_kiro_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "kiro")?;
|
|
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"));
|
|
let usage_limit = metadata
|
|
.get("usage_limit")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let current_usage = metadata
|
|
.get("current_usage")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let remaining = metadata
|
|
.get("remaining")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let usage_ratio = metadata
|
|
.get("usage_percentage")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.map(|value| (value / 100.0).clamp(0.0, 1.0))
|
|
.or_else(|| {
|
|
current_usage
|
|
.zip(usage_limit)
|
|
.and_then(|(current_usage, usage_limit)| {
|
|
(usage_limit > 0.0).then_some((current_usage / usage_limit).clamp(0.0, 1.0))
|
|
})
|
|
});
|
|
let remaining_ratio = usage_ratio.map(|value| (1.0 - value).max(0.0));
|
|
let next_reset_at = provider_quota_timestamp_unix_secs(metadata.get("next_reset_at"));
|
|
let reset_seconds = quota_window_reset_seconds(observed_at_unix_secs, next_reset_at);
|
|
let plan_type = metadata
|
|
.get("subscription_title")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
let is_banned = metadata
|
|
.get("is_banned")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
== Some(true);
|
|
let ban_reason = metadata
|
|
.get("ban_reason")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
|
|
let mut windows = Vec::new();
|
|
if usage_ratio.is_some()
|
|
|| remaining.is_some()
|
|
|| usage_limit.is_some()
|
|
|| current_usage.is_some()
|
|
|| next_reset_at.is_some()
|
|
{
|
|
windows.push(json!({
|
|
"code": "usage",
|
|
"label": "额度",
|
|
"scope": "account",
|
|
"unit": "count",
|
|
"used_ratio": usage_ratio,
|
|
"remaining_ratio": remaining_ratio,
|
|
"used_value": current_usage,
|
|
"remaining_value": remaining,
|
|
"limit_value": usage_limit,
|
|
"reset_at": next_reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
}));
|
|
}
|
|
|
|
if windows.is_empty() && plan_type.is_none() && observed_at_unix_secs.is_none() && !is_banned {
|
|
return None;
|
|
}
|
|
|
|
let exhausted = !is_banned
|
|
&& (remaining.is_some_and(|value| value <= 0.0)
|
|
|| usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6));
|
|
let reason = if is_banned {
|
|
ban_reason
|
|
} else if exhausted {
|
|
Some("额度已耗尽".to_string())
|
|
} else {
|
|
None
|
|
};
|
|
let label = if is_banned {
|
|
Some("账号已封禁")
|
|
} else if exhausted {
|
|
Some("额度耗尽")
|
|
} else {
|
|
None
|
|
};
|
|
let code = if is_banned {
|
|
"banned"
|
|
} else if exhausted {
|
|
"exhausted"
|
|
} else {
|
|
"ok"
|
|
};
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "kiro",
|
|
"code": code,
|
|
"label": label,
|
|
"reason": reason,
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": next_reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": plan_type,
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn build_chatgpt_web_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "chatgpt_web")?;
|
|
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"));
|
|
let remaining = metadata
|
|
.get("image_quota_remaining")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let limit = chatgpt_web_image_quota_limit(metadata, remaining);
|
|
let used = metadata
|
|
.get("image_quota_used")
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.or_else(|| {
|
|
limit
|
|
.zip(remaining)
|
|
.map(|(limit, remaining)| (limit - remaining).max(0.0))
|
|
});
|
|
let reset_at = provider_quota_timestamp_unix_secs(metadata.get("image_quota_reset_at"));
|
|
let reset_seconds = quota_window_reset_seconds(observed_at_unix_secs, reset_at);
|
|
let plan_type = metadata
|
|
.get("plan_type")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(|value| value.to_ascii_lowercase());
|
|
let image_blocked = metadata
|
|
.get("image_quota_blocked")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
== Some(true);
|
|
let usage_ratio = used
|
|
.zip(limit)
|
|
.and_then(|(used, limit)| (limit > 0.0).then_some((used / limit).clamp(0.0, 1.0)));
|
|
let remaining_ratio = remaining.zip(limit).and_then(|(remaining, limit)| {
|
|
(limit > 0.0).then_some((remaining / limit).clamp(0.0, 1.0))
|
|
});
|
|
|
|
let mut windows = Vec::new();
|
|
if remaining.is_some()
|
|
|| limit.is_some()
|
|
|| used.is_some()
|
|
|| reset_at.is_some()
|
|
|| image_blocked
|
|
{
|
|
windows.push(json!({
|
|
"code": "image_gen",
|
|
"label": "生图",
|
|
"scope": "account",
|
|
"unit": "count",
|
|
"used_ratio": usage_ratio,
|
|
"remaining_ratio": remaining_ratio,
|
|
"used_value": used,
|
|
"remaining_value": remaining,
|
|
"limit_value": limit,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"is_exhausted": image_blocked || remaining.is_some_and(|value| value <= 0.0),
|
|
}));
|
|
}
|
|
|
|
if windows.is_empty() && plan_type.is_none() && observed_at_unix_secs.is_none() {
|
|
return None;
|
|
}
|
|
|
|
let exhausted = image_blocked
|
|
|| remaining.is_some_and(|value| value <= 0.0)
|
|
|| usage_ratio.is_some_and(|value| value >= 1.0 - 1e-6);
|
|
let reason = if exhausted {
|
|
Some("生图额度已耗尽")
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "chatgpt_web",
|
|
"code": if exhausted { "exhausted" } else { "ok" },
|
|
"label": if exhausted { Some("额度耗尽") } else { None::<&str> },
|
|
"reason": reason,
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": plan_type,
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn windsurf_percent_quota_window_snapshot(
|
|
metadata: &Map<String, Value>,
|
|
code: &str,
|
|
label: &str,
|
|
remaining_percent_key: &str,
|
|
reset_at_key: &str,
|
|
observed_at_unix_secs: Option<u64>,
|
|
) -> Option<Value> {
|
|
let remaining_percent = metadata
|
|
.get(remaining_percent_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let reset_at = provider_quota_timestamp_unix_secs(metadata.get(reset_at_key));
|
|
if remaining_percent.is_none() && reset_at.is_none() {
|
|
return None;
|
|
}
|
|
|
|
let remaining_ratio = remaining_percent.map(|value| (value / 100.0).clamp(0.0, 1.0));
|
|
let used_ratio = remaining_ratio.map(|value| (1.0 - value).clamp(0.0, 1.0));
|
|
let reset_seconds = quota_window_reset_seconds(observed_at_unix_secs, reset_at);
|
|
|
|
Some(json!({
|
|
"code": code,
|
|
"label": label,
|
|
"scope": "account",
|
|
"unit": "percent",
|
|
"used_ratio": used_ratio,
|
|
"remaining_ratio": remaining_ratio,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"is_exhausted": remaining_ratio.map(|value| value <= 1e-6),
|
|
}))
|
|
}
|
|
|
|
fn windsurf_count_quota_window_snapshot(
|
|
metadata: &Map<String, Value>,
|
|
code: &str,
|
|
label: &str,
|
|
used_key: &str,
|
|
limit_key: &str,
|
|
remaining_key: &str,
|
|
) -> Option<Value> {
|
|
let used = metadata
|
|
.get(used_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let limit = metadata
|
|
.get(limit_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64);
|
|
let remaining = metadata
|
|
.get(remaining_key)
|
|
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
|
.or_else(|| limit.zip(used).map(|(limit, used)| (limit - used).max(0.0)));
|
|
if used.is_none() && limit.is_none() && remaining.is_none() {
|
|
return None;
|
|
}
|
|
|
|
let used_ratio = used
|
|
.zip(limit)
|
|
.and_then(|(used, limit)| (limit > 0.0).then_some((used / limit).clamp(0.0, 1.0)));
|
|
let remaining_ratio = remaining.zip(limit).and_then(|(remaining, limit)| {
|
|
(limit > 0.0).then_some((remaining / limit).clamp(0.0, 1.0))
|
|
});
|
|
|
|
Some(json!({
|
|
"code": code,
|
|
"label": label,
|
|
"scope": "account",
|
|
"unit": "count",
|
|
"used_ratio": used_ratio,
|
|
"remaining_ratio": remaining_ratio,
|
|
"used_value": used,
|
|
"remaining_value": remaining,
|
|
"limit_value": limit,
|
|
"is_exhausted": remaining.is_some_and(|value| value <= 0.0),
|
|
}))
|
|
}
|
|
|
|
fn build_windsurf_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "windsurf")?;
|
|
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"));
|
|
let plan_type = metadata
|
|
.get("plan_name")
|
|
.or_else(|| metadata.get("plan_type"))
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
let rate_limit = metadata
|
|
.get("rate_limit")
|
|
.cloned()
|
|
.filter(|value| !value.is_null());
|
|
let last_error = metadata
|
|
.get("last_error")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
let banned = metadata
|
|
.get("banned")
|
|
.or_else(|| metadata.get("is_banned"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
== Some(true);
|
|
let quarantined = metadata
|
|
.get("quarantined")
|
|
.or_else(|| metadata.get("is_quarantined"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
== Some(true);
|
|
|
|
let mut windows = [
|
|
windsurf_percent_quota_window_snapshot(
|
|
metadata,
|
|
"daily",
|
|
"日",
|
|
"daily_remaining_percent",
|
|
"daily_reset_at",
|
|
observed_at_unix_secs,
|
|
),
|
|
windsurf_percent_quota_window_snapshot(
|
|
metadata,
|
|
"weekly",
|
|
"周",
|
|
"weekly_remaining_percent",
|
|
"weekly_reset_at",
|
|
observed_at_unix_secs,
|
|
),
|
|
windsurf_count_quota_window_snapshot(
|
|
metadata,
|
|
"prompt",
|
|
"Prompt",
|
|
"prompt_used",
|
|
"prompt_limit",
|
|
"prompt_remaining",
|
|
),
|
|
windsurf_count_quota_window_snapshot(
|
|
metadata,
|
|
"flex",
|
|
"Flex",
|
|
"flex_used",
|
|
"flex_limit",
|
|
"flex_remaining",
|
|
),
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut rate_limit_cooling = false;
|
|
let mut rate_limit_reset_seconds = None;
|
|
let mut rate_limit_reason = None::<String>;
|
|
if let Some(rate_limit) = rate_limit.as_ref() {
|
|
if let Some(rate_limit_object) = rate_limit.as_object() {
|
|
let retry_after_ms = rate_limit_object
|
|
.get("retry_after_ms")
|
|
.or_else(|| rate_limit_object.get("retryAfterMs"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.filter(|value| *value > 0);
|
|
if let Some(retry_after_ms) = retry_after_ms {
|
|
rate_limit_cooling = true;
|
|
rate_limit_reset_seconds = Some(retry_after_ms.saturating_add(999) / 1000);
|
|
rate_limit_reason = rate_limit_object
|
|
.get("message")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
windows.push(json!({
|
|
"code": "rate_limit",
|
|
"label": "速率",
|
|
"scope": "account",
|
|
"unit": "count",
|
|
"is_exhausted": false,
|
|
"reset_seconds": rate_limit_reset_seconds,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
let allowed_models_count = metadata
|
|
.get("allowed_models_count")
|
|
.or_else(|| metadata.get("models_count"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
|
|
if windows.is_empty()
|
|
&& plan_type.is_none()
|
|
&& observed_at_unix_secs.is_none()
|
|
&& rate_limit.is_none()
|
|
&& allowed_models_count.is_none()
|
|
&& !banned
|
|
&& !quarantined
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let usage_ratio = quota_windows_usage_ratio(&windows);
|
|
let reset_seconds = if rate_limit_cooling {
|
|
rate_limit_reset_seconds.or_else(|| quota_windows_min_reset_seconds(&windows))
|
|
} else {
|
|
quota_windows_min_reset_seconds(&windows)
|
|
};
|
|
let reset_at = if rate_limit_cooling {
|
|
None
|
|
} else {
|
|
quota_windows_min_reset_at(&windows)
|
|
};
|
|
let exhausted_by_window = windows.iter().filter_map(Value::as_object).any(|window| {
|
|
window
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|code| {
|
|
code.eq_ignore_ascii_case("daily") || code.eq_ignore_ascii_case("weekly")
|
|
})
|
|
&& window
|
|
.get("is_exhausted")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
.unwrap_or(false)
|
|
});
|
|
let exhausted = banned || quarantined || exhausted_by_window;
|
|
let (code, label, reason) = if banned {
|
|
(
|
|
"banned",
|
|
Some("账号已封禁"),
|
|
last_error
|
|
.clone()
|
|
.or_else(|| Some("账号被 Windsurf 标记为不可用".to_string())),
|
|
)
|
|
} else if quarantined {
|
|
(
|
|
"quarantined",
|
|
Some("账号隔离中"),
|
|
last_error
|
|
.clone()
|
|
.or_else(|| Some("账号处于隔离状态".to_string())),
|
|
)
|
|
} else if rate_limit_cooling {
|
|
(
|
|
"cooldown",
|
|
Some("冷却中"),
|
|
last_error.clone().or(rate_limit_reason),
|
|
)
|
|
} else if exhausted {
|
|
(
|
|
"exhausted",
|
|
Some("额度耗尽"),
|
|
Some("额度窗口已耗尽".to_string()),
|
|
)
|
|
} else {
|
|
("ok", None, last_error)
|
|
};
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "windsurf",
|
|
"code": code,
|
|
"label": label,
|
|
"reason": reason,
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": plan_type,
|
|
"allowed_models_count": allowed_models_count,
|
|
"rate_limit": rate_limit.unwrap_or(Value::Null),
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn build_antigravity_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "antigravity")?;
|
|
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"));
|
|
let is_forbidden = metadata
|
|
.get("is_forbidden")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
== Some(true);
|
|
let forbidden_reason = metadata
|
|
.get("forbidden_reason")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned);
|
|
let mut windows = provider_quota_model_bucket(metadata)
|
|
.map(|models| {
|
|
models
|
|
.iter()
|
|
.filter_map(|(model_name, item)| {
|
|
antigravity_model_quota_window_snapshot(
|
|
model_name,
|
|
item.as_object()?,
|
|
observed_at_unix_secs,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
let grouped_observed_at_unix_secs =
|
|
provider_quota_timestamp_unix_secs(metadata.get("quota_groups_updated_at"))
|
|
.or(observed_at_unix_secs);
|
|
windows.extend(antigravity_grouped_quota_window_snapshots(
|
|
metadata,
|
|
grouped_observed_at_unix_secs,
|
|
));
|
|
|
|
if windows.is_empty() && observed_at_unix_secs.is_none() && !is_forbidden {
|
|
return None;
|
|
}
|
|
|
|
let usage_ratio = quota_windows_usage_ratio(&windows);
|
|
let reset_seconds = quota_windows_min_reset_seconds(&windows);
|
|
let reset_at = quota_windows_min_reset_at(&windows);
|
|
let exhausted = !is_forbidden && quota_windows_all_exhausted(&windows);
|
|
let reason = if is_forbidden {
|
|
forbidden_reason
|
|
} else if exhausted {
|
|
Some("所有模型额度已耗尽".to_string())
|
|
} else {
|
|
None
|
|
};
|
|
let label = if is_forbidden {
|
|
Some("访问受限")
|
|
} else if exhausted {
|
|
Some("额度耗尽")
|
|
} else {
|
|
None
|
|
};
|
|
let code = if is_forbidden {
|
|
"forbidden"
|
|
} else if exhausted {
|
|
"exhausted"
|
|
} else {
|
|
"ok"
|
|
};
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "antigravity",
|
|
"code": code,
|
|
"label": label,
|
|
"reason": reason,
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": serde_json::Value::Null,
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn build_grok_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "grok")?;
|
|
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"));
|
|
let inferred_pool_tier = grok_pool_tier_from_quota_bucket(metadata);
|
|
let pool_tier = provider_quota_metadata_string(metadata, &["pool_tier", "tier"])
|
|
.or_else(|| inferred_pool_tier.map(ToOwned::to_owned));
|
|
let plan_type = provider_quota_metadata_string(metadata, &["plan_type", "plan"])
|
|
.or_else(|| pool_tier.clone());
|
|
let supported_windows = grok_supported_quota_windows_for_tier(pool_tier.as_deref());
|
|
let windows = provider_quota_model_bucket(metadata)
|
|
.map(|models| {
|
|
models
|
|
.iter()
|
|
.filter_map(|(model_name, item)| {
|
|
if !supported_windows
|
|
.iter()
|
|
.any(|(quota_key, _)| *quota_key == model_name.as_str())
|
|
{
|
|
return None;
|
|
}
|
|
model_quota_window_snapshot(
|
|
model_name,
|
|
item.as_object()?,
|
|
observed_at_unix_secs,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if windows.is_empty() && observed_at_unix_secs.is_none() {
|
|
return None;
|
|
}
|
|
|
|
let usage_ratio = quota_windows_usage_ratio(&windows);
|
|
let reset_seconds = quota_windows_min_reset_seconds(&windows);
|
|
let reset_at = quota_windows_min_reset_at(&windows);
|
|
let exhausted = quota_windows_all_exhausted(&windows);
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "grok",
|
|
"code": if exhausted { "exhausted" } else { "ok" },
|
|
"label": if exhausted { Some("额度耗尽") } else { None::<&str> },
|
|
"reason": if exhausted {
|
|
Some("所有 Grok 模式额度已耗尽")
|
|
} else {
|
|
None::<&str>
|
|
},
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": plan_type,
|
|
"pool_tier": pool_tier,
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn gemini_cli_plan_type(metadata: &Map<String, Value>) -> Option<String> {
|
|
provider_quota_metadata_string(metadata, &["plan_type", "tier", "plan"]).or_else(|| {
|
|
provider_quota_metadata_string_by_paths(
|
|
metadata,
|
|
&[
|
|
&["paidTier", "id"],
|
|
&["paidTier", "tierType"],
|
|
&["paidTier", "name"],
|
|
&["currentTier", "id"],
|
|
&["currentTier", "tierType"],
|
|
&["currentTier", "name"],
|
|
],
|
|
)
|
|
})
|
|
}
|
|
|
|
fn gemini_cli_credits_status_snapshot(metadata: &Map<String, Value>) -> Option<Value> {
|
|
let remaining = provider_quota_metadata_number_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "remaining"],
|
|
&["credits", "remainingCredits"],
|
|
&["credits", "available"],
|
|
&["credits", "availableCredits"],
|
|
&["credits", "balance"],
|
|
&["remainingCredits"],
|
|
&["availableCredits"],
|
|
&["paidTier", "remainingCredits"],
|
|
&["paidTier", "availableCredits"],
|
|
&["currentTier", "remainingCredits"],
|
|
&["currentTier", "availableCredits"],
|
|
],
|
|
);
|
|
let balance = provider_quota_metadata_number_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "balance"],
|
|
&["credits", "remaining"],
|
|
&["credits", "available"],
|
|
&["paidTier", "availableCredits"],
|
|
&["currentTier", "availableCredits"],
|
|
&["availableCredits"],
|
|
&["remainingCredits"],
|
|
],
|
|
)
|
|
.or(remaining);
|
|
let consumed = provider_quota_metadata_number_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "consumed"],
|
|
&["credits", "consumedCredits"],
|
|
&["consumedCredits"],
|
|
&["paidTier", "consumedCredits"],
|
|
&["currentTier", "consumedCredits"],
|
|
],
|
|
);
|
|
let total = provider_quota_metadata_number_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "total"],
|
|
&["credits", "totalCredits"],
|
|
&["totalCredits"],
|
|
&["paidTier", "totalCredits"],
|
|
&["currentTier", "totalCredits"],
|
|
],
|
|
);
|
|
let unlimited = provider_quota_metadata_bool_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "unlimited"],
|
|
&["credits_unlimited"],
|
|
&["paidTier", "unlimited"],
|
|
&["currentTier", "unlimited"],
|
|
],
|
|
);
|
|
let explicit_has_credits = provider_quota_metadata_bool_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "has_credits"],
|
|
&["has_credits"],
|
|
&["paidTier", "hasCredits"],
|
|
&["currentTier", "hasCredits"],
|
|
],
|
|
);
|
|
let trace_id = provider_quota_metadata_string_by_paths(
|
|
metadata,
|
|
&[
|
|
&["credits", "trace_id"],
|
|
&["credits", "traceId"],
|
|
&["trace_id"],
|
|
&["traceId"],
|
|
],
|
|
);
|
|
let updated_at = provider_quota_metadata_value_by_path(metadata, &["credits", "updated_at"])
|
|
.or_else(|| provider_quota_metadata_value_by_path(metadata, &["credits", "updatedAt"]))
|
|
.or_else(|| metadata.get("updated_at"))
|
|
.and_then(|value| provider_quota_timestamp_unix_secs(Some(value)));
|
|
|
|
if remaining.is_none()
|
|
&& balance.is_none()
|
|
&& consumed.is_none()
|
|
&& total.is_none()
|
|
&& unlimited.is_none()
|
|
&& explicit_has_credits.is_none()
|
|
&& trace_id.is_none()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let has_credits = explicit_has_credits
|
|
.or_else(|| unlimited.filter(|value| *value))
|
|
.or_else(|| remaining.or(balance).map(|value| value > 0.0));
|
|
let mut credits = Map::new();
|
|
credits.insert("has_credits".to_string(), json!(has_credits));
|
|
credits.insert("balance".to_string(), json!(balance));
|
|
credits.insert("remaining".to_string(), json!(remaining.or(balance)));
|
|
credits.insert("consumed".to_string(), json!(consumed));
|
|
credits.insert("total".to_string(), json!(total));
|
|
credits.insert("unlimited".to_string(), json!(unlimited));
|
|
credits.insert("trace_id".to_string(), json!(trace_id));
|
|
credits.insert("updated_at".to_string(), json!(updated_at));
|
|
Some(Value::Object(credits))
|
|
}
|
|
|
|
fn build_gemini_cli_quota_status_snapshot(
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let metadata = provider_quota_metadata_bucket(upstream_metadata, "gemini_cli")?;
|
|
let credits = gemini_cli_credits_status_snapshot(metadata);
|
|
let plan_type = gemini_cli_plan_type(metadata);
|
|
let observed_at_unix_secs = provider_quota_timestamp_unix_secs(metadata.get("updated_at"))
|
|
.or_else(|| {
|
|
credits
|
|
.as_ref()
|
|
.and_then(|value| value.get("updated_at"))
|
|
.and_then(|value| provider_quota_timestamp_unix_secs(Some(value)))
|
|
});
|
|
let windows = provider_quota_model_bucket(metadata)
|
|
.map(|models| {
|
|
models
|
|
.iter()
|
|
.filter_map(|(model_name, item)| {
|
|
model_quota_window_snapshot(
|
|
model_name,
|
|
item.as_object()?,
|
|
observed_at_unix_secs,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
if windows.is_empty()
|
|
&& observed_at_unix_secs.is_none()
|
|
&& credits.is_none()
|
|
&& plan_type.is_none()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let usage_ratio = quota_windows_usage_ratio(&windows);
|
|
let active_exhausted_windows = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.filter(|window| {
|
|
window
|
|
.get("is_exhausted")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
.or_else(|| {
|
|
window
|
|
.get("used_ratio")
|
|
.and_then(Value::as_f64)
|
|
.map(|value| value >= 1.0 - 1e-6)
|
|
})
|
|
.unwrap_or(false)
|
|
})
|
|
.filter(|window| {
|
|
provider_quota_timestamp_unix_secs(window.get("reset_at"))
|
|
.zip(observed_at_unix_secs)
|
|
.map(|(reset_at, observed_at)| reset_at > observed_at)
|
|
.unwrap_or(true)
|
|
})
|
|
.count();
|
|
let exhausted = !windows.is_empty() && active_exhausted_windows == windows.len();
|
|
let cooling = active_exhausted_windows > 0;
|
|
let reset_seconds = if cooling {
|
|
quota_windows_min_reset_seconds(&windows)
|
|
} else {
|
|
None
|
|
};
|
|
let reset_at = if cooling {
|
|
quota_windows_min_reset_at(&windows)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Some(json!({
|
|
"version": 2,
|
|
"provider_type": "gemini_cli",
|
|
"code": if exhausted {
|
|
"exhausted"
|
|
} else if cooling {
|
|
"cooldown"
|
|
} else {
|
|
"ok"
|
|
},
|
|
"label": if cooling { Some("冷却中") } else { None::<&str> },
|
|
"reason": if exhausted {
|
|
Some("所有模型均处于冷却中")
|
|
} else {
|
|
None::<&str>
|
|
},
|
|
"freshness": "fresh",
|
|
"source": source,
|
|
"observed_at": observed_at_unix_secs,
|
|
"exhausted": exhausted,
|
|
"usage_ratio": usage_ratio,
|
|
"updated_at": observed_at_unix_secs,
|
|
"reset_at": reset_at,
|
|
"reset_seconds": reset_seconds,
|
|
"plan_type": plan_type,
|
|
"credits": credits,
|
|
"windows": windows,
|
|
}))
|
|
}
|
|
|
|
fn build_codex_reset_credits_status_snapshot(
|
|
metadata: &Map<String, Value>,
|
|
observed_at_unix_secs: Option<u64>,
|
|
) -> Option<Value> {
|
|
let reset_credits = metadata.get("reset_credits").and_then(Value::as_object)?;
|
|
let available_count = reset_credits
|
|
.get("available_count")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64);
|
|
let updated_at = reset_credits
|
|
.get("updated_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.or(observed_at_unix_secs);
|
|
let detail_source = reset_credits
|
|
.get("detail_source")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty());
|
|
let detail_status = reset_credits
|
|
.get("detail_status")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty());
|
|
let detail_error = reset_credits
|
|
.get("detail_error")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty());
|
|
|
|
let mut credits = reset_credits
|
|
.get("credits")
|
|
.and_then(Value::as_array)
|
|
.into_iter()
|
|
.flatten()
|
|
.filter_map(|item| {
|
|
let object = item.as_object()?;
|
|
let expires_at = object
|
|
.get("expires_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)?;
|
|
let display_key = object
|
|
.get("display_key")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())?;
|
|
let mut out = Map::new();
|
|
if let Some(id) = object
|
|
.get("id")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
{
|
|
out.insert("id".to_string(), json!(id));
|
|
}
|
|
out.insert("display_key".to_string(), json!(display_key));
|
|
if let Some(status) = object
|
|
.get("status")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
{
|
|
out.insert("status".to_string(), json!(status));
|
|
}
|
|
if let Some(granted_at) = object
|
|
.get("granted_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
{
|
|
out.insert("granted_at".to_string(), json!(granted_at));
|
|
}
|
|
out.insert("expires_at".to_string(), json!(expires_at));
|
|
if let Some(observed_at) = observed_at_unix_secs {
|
|
out.insert(
|
|
"remaining_seconds".to_string(),
|
|
json!(expires_at.saturating_sub(observed_at)),
|
|
);
|
|
}
|
|
Some(Value::Object(out))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
credits.sort_by_key(|item| {
|
|
item.get("expires_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.unwrap_or(u64::MAX)
|
|
});
|
|
|
|
if available_count.is_none()
|
|
&& updated_at.is_none()
|
|
&& detail_source.is_none()
|
|
&& detail_status.is_none()
|
|
&& detail_error.is_none()
|
|
&& credits.is_empty()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
let mut out = Map::new();
|
|
if let Some(value) = available_count {
|
|
out.insert("available_count".to_string(), json!(value));
|
|
}
|
|
if let Some(value) = updated_at {
|
|
out.insert("updated_at".to_string(), json!(value));
|
|
}
|
|
if let Some(value) = detail_source {
|
|
out.insert("detail_source".to_string(), json!(value));
|
|
}
|
|
if let Some(value) = detail_status {
|
|
out.insert("detail_status".to_string(), json!(value));
|
|
}
|
|
if let Some(value) = detail_error {
|
|
out.insert("detail_error".to_string(), json!(value));
|
|
}
|
|
out.insert("credits".to_string(), Value::Array(credits));
|
|
Some(Value::Object(out))
|
|
}
|
|
|
|
pub(crate) fn sync_provider_key_quota_status_snapshot(
|
|
status_snapshot: Option<&Value>,
|
|
provider_type: &str,
|
|
upstream_metadata: Option<&Value>,
|
|
source: &str,
|
|
) -> Option<Value> {
|
|
let normalized_provider_type = provider_type.trim().to_ascii_lowercase();
|
|
let mut quota = match normalized_provider_type.as_str() {
|
|
"codex" => build_codex_quota_status_snapshot(upstream_metadata, source),
|
|
"kiro" => build_kiro_quota_status_snapshot(upstream_metadata, source),
|
|
"chatgpt_web" => build_chatgpt_web_quota_status_snapshot(upstream_metadata, source),
|
|
"windsurf" => build_windsurf_quota_status_snapshot(upstream_metadata, source),
|
|
"antigravity" => build_antigravity_quota_status_snapshot(upstream_metadata, source),
|
|
"grok" => build_grok_quota_status_snapshot(upstream_metadata, source),
|
|
"gemini_cli" => build_gemini_cli_quota_status_snapshot(upstream_metadata, source),
|
|
_ => None,
|
|
}?;
|
|
if normalized_provider_type == "codex" {
|
|
preserve_quota_window_usage_state(status_snapshot, &mut quota);
|
|
}
|
|
|
|
let default_snapshot = default_provider_key_status_snapshot();
|
|
let mut snapshot = provider_key_status_snapshot_object(status_snapshot)
|
|
.or_else(|| default_snapshot.as_object().cloned())
|
|
.unwrap_or_default();
|
|
snapshot.insert("quota".to_string(), quota);
|
|
Some(Value::Object(snapshot))
|
|
}
|
|
|
|
fn quota_snapshot_has_materialized_data(
|
|
quota_snapshot: Option<&Map<String, Value>>,
|
|
provider_type: &str,
|
|
) -> bool {
|
|
let Some(quota_snapshot) = quota_snapshot else {
|
|
return false;
|
|
};
|
|
|
|
let normalized_provider_type = provider_type.trim().to_ascii_lowercase();
|
|
let snapshot_provider_type = quota_snapshot
|
|
.get("provider_type")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.unwrap_or_default()
|
|
.to_ascii_lowercase();
|
|
if !snapshot_provider_type.is_empty() && snapshot_provider_type != normalized_provider_type {
|
|
return false;
|
|
}
|
|
|
|
if normalized_provider_type == "windsurf"
|
|
&& windsurf_quota_snapshot_has_stale_cooldown(quota_snapshot)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if quota_snapshot
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.is_some_and(|windows| !windows.is_empty())
|
|
{
|
|
return true;
|
|
}
|
|
if quota_snapshot
|
|
.get("credits")
|
|
.is_some_and(|credits| !credits.is_null())
|
|
{
|
|
return true;
|
|
}
|
|
if quota_snapshot
|
|
.get("reset_credits")
|
|
.is_some_and(|reset_credits| !reset_credits.is_null())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
quota_snapshot
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.is_some_and(|code| {
|
|
!code.is_empty()
|
|
&& !code.eq_ignore_ascii_case("unknown")
|
|
&& !code.eq_ignore_ascii_case("ok")
|
|
})
|
|
}
|
|
|
|
fn codex_upstream_metadata_is_at_least_as_fresh(
|
|
quota_snapshot: Option<&Map<String, Value>>,
|
|
upstream_metadata: Option<&Value>,
|
|
) -> bool {
|
|
let Some(metadata) = provider_quota_metadata_bucket(upstream_metadata, "codex") else {
|
|
return false;
|
|
};
|
|
let Some(metadata_updated_at) = metadata
|
|
.get("updated_at")
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
else {
|
|
return false;
|
|
};
|
|
let snapshot_updated_at = quota_snapshot.and_then(|quota| {
|
|
quota
|
|
.get("updated_at")
|
|
.or_else(|| quota.get("observed_at"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
});
|
|
|
|
snapshot_updated_at.is_none_or(|updated_at| metadata_updated_at >= updated_at)
|
|
}
|
|
|
|
fn windsurf_quota_snapshot_has_stale_cooldown(quota_snapshot: &Map<String, Value>) -> bool {
|
|
let code = quota_snapshot
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.map(str::trim)
|
|
.unwrap_or_default();
|
|
if !code.eq_ignore_ascii_case("cooldown") {
|
|
return false;
|
|
}
|
|
|
|
let rate_limit = quota_snapshot.get("rate_limit").and_then(Value::as_object);
|
|
let retry_after_ms = rate_limit
|
|
.and_then(|rate_limit| {
|
|
rate_limit
|
|
.get("retry_after_ms")
|
|
.or_else(|| rate_limit.get("retryAfterMs"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
})
|
|
.unwrap_or(0);
|
|
if retry_after_ms > 0 {
|
|
return false;
|
|
}
|
|
|
|
let has_positive_rate_limit_reset = quota_snapshot
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.is_some_and(|windows| {
|
|
windows.iter().filter_map(Value::as_object).any(|window| {
|
|
window
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|code| code.eq_ignore_ascii_case("rate_limit"))
|
|
&& window
|
|
.get("reset_seconds")
|
|
.or_else(|| window.get("reset_at"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
|
.is_some_and(|value| value > 0)
|
|
})
|
|
});
|
|
if has_positive_rate_limit_reset {
|
|
return false;
|
|
}
|
|
|
|
let exhausted = quota_snapshot
|
|
.get("exhausted")
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
.unwrap_or(false);
|
|
let has_capacity = rate_limit
|
|
.and_then(|rate_limit| rate_limit.get("has_capacity"))
|
|
.and_then(admin_provider_quota_pure::coerce_json_bool)
|
|
.unwrap_or(false);
|
|
|
|
has_capacity || !exhausted
|
|
}
|
|
|
|
pub(crate) fn provider_key_status_snapshot_payload(
|
|
key: &StoredProviderCatalogKey,
|
|
provider_type: &str,
|
|
) -> serde_json::Value {
|
|
let status_snapshot = key
|
|
.status_snapshot
|
|
.as_ref()
|
|
.filter(|value| value.is_object());
|
|
let quota_snapshot = status_snapshot
|
|
.and_then(Value::as_object)
|
|
.and_then(|snapshot| snapshot.get("quota"))
|
|
.and_then(Value::as_object);
|
|
let refresh_codex_snapshot = provider_type.trim().eq_ignore_ascii_case("codex")
|
|
&& codex_upstream_metadata_is_at_least_as_fresh(
|
|
quota_snapshot,
|
|
key.upstream_metadata.as_ref(),
|
|
);
|
|
|
|
let payload = if quota_snapshot_has_materialized_data(quota_snapshot, provider_type)
|
|
&& !refresh_codex_snapshot
|
|
{
|
|
status_snapshot
|
|
.cloned()
|
|
.unwrap_or_else(default_provider_key_status_snapshot)
|
|
} else {
|
|
sync_provider_key_quota_status_snapshot(
|
|
status_snapshot,
|
|
provider_type,
|
|
key.upstream_metadata.as_ref(),
|
|
"catalog_fallback",
|
|
)
|
|
.or_else(|| status_snapshot.cloned())
|
|
.unwrap_or_else(default_provider_key_status_snapshot)
|
|
};
|
|
|
|
let mut snapshot = provider_key_status_snapshot_object(Some(&payload))
|
|
.or_else(|| default_provider_key_status_snapshot().as_object().cloned())
|
|
.unwrap_or_default();
|
|
snapshot.insert(
|
|
"oauth".to_string(),
|
|
build_provider_key_oauth_status_snapshot(key),
|
|
);
|
|
snapshot.insert(
|
|
"account".to_string(),
|
|
build_provider_key_account_status_snapshot(key, provider_type),
|
|
);
|
|
Value::Object(snapshot)
|
|
}
|
|
|
|
pub(crate) fn provider_key_health_summary(
|
|
key: &StoredProviderCatalogKey,
|
|
) -> (
|
|
f64,
|
|
i64,
|
|
Option<String>,
|
|
bool,
|
|
serde_json::Map<String, serde_json::Value>,
|
|
) {
|
|
provider_key_health_summary_with_circuit_predicate(key, |value| {
|
|
value
|
|
.get("open")
|
|
.and_then(serde_json::Value::as_bool)
|
|
.unwrap_or(false)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn provider_key_health_summary_at(
|
|
key: &StoredProviderCatalogKey,
|
|
now_unix_secs: u64,
|
|
) -> (
|
|
f64,
|
|
i64,
|
|
Option<String>,
|
|
bool,
|
|
serde_json::Map<String, serde_json::Value>,
|
|
) {
|
|
provider_key_health_summary_with_circuit_predicate(key, |value| {
|
|
provider_key_circuit_payload_is_active_open_at(value, now_unix_secs)
|
|
})
|
|
}
|
|
|
|
fn provider_key_health_summary_with_circuit_predicate(
|
|
key: &StoredProviderCatalogKey,
|
|
circuit_is_open: impl Fn(&serde_json::Value) -> bool,
|
|
) -> (
|
|
f64,
|
|
i64,
|
|
Option<String>,
|
|
bool,
|
|
serde_json::Map<String, serde_json::Value>,
|
|
) {
|
|
let health_by_format = key
|
|
.health_by_format
|
|
.as_ref()
|
|
.and_then(serde_json::Value::as_object)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
let circuit_by_format = key
|
|
.circuit_breaker_by_format
|
|
.as_ref()
|
|
.and_then(serde_json::Value::as_object)
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
let mut min_health_score = 1.0_f64;
|
|
let mut max_consecutive = 0_i64;
|
|
let mut last_failure_at: Option<String> = None;
|
|
for value in health_by_format.values() {
|
|
let score = value
|
|
.get("health_score")
|
|
.and_then(serde_json::Value::as_f64)
|
|
.unwrap_or(1.0);
|
|
min_health_score = min_health_score.min(score);
|
|
let consecutive = value
|
|
.get("consecutive_failures")
|
|
.and_then(serde_json::Value::as_i64)
|
|
.unwrap_or(0);
|
|
max_consecutive = max_consecutive.max(consecutive);
|
|
if let Some(last_failure) = value
|
|
.get("last_failure_at")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(ToOwned::to_owned)
|
|
{
|
|
if last_failure_at
|
|
.as_ref()
|
|
.is_none_or(|current| last_failure > *current)
|
|
{
|
|
last_failure_at = Some(last_failure);
|
|
}
|
|
}
|
|
}
|
|
|
|
let any_circuit_open = circuit_by_format.values().any(circuit_is_open);
|
|
|
|
(
|
|
if health_by_format.is_empty() {
|
|
1.0
|
|
} else {
|
|
min_health_score
|
|
},
|
|
max_consecutive,
|
|
last_failure_at,
|
|
any_circuit_open,
|
|
circuit_by_format,
|
|
)
|
|
}
|
|
|
|
fn normalize_catalog_oauth_plan_type(value: &str, provider_type: &str) -> Option<String> {
|
|
let mut normalized = value.trim().to_string();
|
|
if normalized.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let provider_type = provider_type.trim().to_ascii_lowercase();
|
|
if !provider_type.is_empty() && normalized.to_ascii_lowercase().starts_with(&provider_type) {
|
|
normalized = normalized[provider_type.len()..]
|
|
.trim_matches(|ch: char| [' ', ':', '-', '_'].contains(&ch))
|
|
.to_string();
|
|
}
|
|
|
|
let normalized = normalized.trim().to_ascii_lowercase();
|
|
if normalized.is_empty() {
|
|
None
|
|
} else {
|
|
Some(normalized)
|
|
}
|
|
}
|
|
|
|
fn catalog_oauth_plan_type_from_source(
|
|
source: &serde_json::Map<String, serde_json::Value>,
|
|
provider_type: &str,
|
|
fields: &[&str],
|
|
) -> Option<String> {
|
|
for field in fields {
|
|
let Some(value) = source.get(*field).and_then(serde_json::Value::as_str) else {
|
|
continue;
|
|
};
|
|
if let Some(normalized) = normalize_catalog_oauth_plan_type(value, provider_type) {
|
|
return Some(normalized);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn derive_catalog_oauth_plan_type(
|
|
key: &StoredProviderCatalogKey,
|
|
provider_type: &str,
|
|
auth_config: Option<&serde_json::Map<String, serde_json::Value>>,
|
|
) -> Option<String> {
|
|
if !provider_key_auth_semantics(key, provider_type).oauth_managed() {
|
|
return None;
|
|
}
|
|
|
|
let provider_type_key = provider_type.trim().to_ascii_lowercase();
|
|
if let Some(upstream_metadata) = key
|
|
.upstream_metadata
|
|
.as_ref()
|
|
.and_then(serde_json::Value::as_object)
|
|
{
|
|
let provider_bucket = if provider_type_key.is_empty() {
|
|
None
|
|
} else {
|
|
upstream_metadata
|
|
.get(&provider_type_key)
|
|
.and_then(serde_json::Value::as_object)
|
|
};
|
|
for source in provider_bucket
|
|
.into_iter()
|
|
.chain(std::iter::once(upstream_metadata))
|
|
{
|
|
if let Some(plan_type) = catalog_oauth_plan_type_from_source(
|
|
source,
|
|
provider_type,
|
|
&[
|
|
"plan_type",
|
|
"tier",
|
|
"subscription_title",
|
|
"subscription_plan",
|
|
"plan",
|
|
],
|
|
) {
|
|
return Some(plan_type);
|
|
}
|
|
}
|
|
}
|
|
|
|
auth_config.and_then(|source| {
|
|
catalog_oauth_plan_type_from_source(
|
|
source,
|
|
provider_type,
|
|
&["plan_type", "tier", "plan", "subscription_plan"],
|
|
)
|
|
})
|
|
}
|
|
|
|
pub(crate) fn build_admin_provider_key_response(
|
|
state: &AppState,
|
|
key: &StoredProviderCatalogKey,
|
|
provider_type: &str,
|
|
api_formats: &[String],
|
|
now_unix_secs: u64,
|
|
) -> serde_json::Value {
|
|
let request_count = u64::from(key.request_count.unwrap_or(0));
|
|
let success_count = u64::from(key.success_count.unwrap_or(0));
|
|
let error_count = u64::from(key.error_count.unwrap_or(0));
|
|
let total_response_time_ms = key.total_response_time_ms.unwrap_or(0) as f64;
|
|
let success_rate = if request_count > 0 {
|
|
success_count as f64 / request_count as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
let avg_response_time_ms = if success_count > 0 {
|
|
total_response_time_ms / success_count as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
let auth_semantics = provider_key_auth_semantics(key, provider_type);
|
|
let auth_config = parse_catalog_auth_config_json(state, key);
|
|
let oauth_organizations = if auth_semantics.can_show_oauth_metadata() {
|
|
auth_config
|
|
.as_ref()
|
|
.and_then(|config| config.get("organizations"))
|
|
.and_then(serde_json::Value::as_array)
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
let oauth_temporary = auth_semantics.can_show_oauth_metadata()
|
|
&& auth_config
|
|
.as_ref()
|
|
.and_then(|config| config.get("access_token_import_temporary"))
|
|
.and_then(serde_json::Value::as_bool)
|
|
.unwrap_or(false);
|
|
let oauth_header_auth = auth_semantics.oauth_managed()
|
|
&& provider_key_auth_config_uses_header_authorization(auth_config.as_ref());
|
|
let agent_identity =
|
|
provider_key_auth_config_is_agent_identity(provider_type, auth_config.as_ref());
|
|
let oauth_plan_type = derive_catalog_oauth_plan_type(key, provider_type, auth_config.as_ref());
|
|
let (
|
|
health_score,
|
|
consecutive_failures,
|
|
last_failure_at,
|
|
circuit_breaker_open,
|
|
circuit_by_format,
|
|
) = provider_key_health_summary_at(key, now_unix_secs);
|
|
let circuit_sample = circuit_by_format
|
|
.values()
|
|
.find(|value| provider_key_circuit_payload_is_active_open_at(value, now_unix_secs))
|
|
.or_else(|| circuit_by_format.values().next());
|
|
let is_adaptive = key.rpm_limit.is_none();
|
|
let effective_limit = if is_adaptive {
|
|
key.learned_rpm_limit
|
|
} else {
|
|
key.rpm_limit
|
|
};
|
|
let mut payload = serde_json::Map::new();
|
|
payload.insert("id".to_string(), json!(key.id));
|
|
payload.insert("provider_id".to_string(), json!(key.provider_id));
|
|
payload.insert(
|
|
"api_formats".to_string(),
|
|
serde_json::Value::Array(
|
|
api_formats
|
|
.iter()
|
|
.cloned()
|
|
.map(serde_json::Value::String)
|
|
.collect(),
|
|
),
|
|
);
|
|
payload.insert(
|
|
"api_key_masked".to_string(),
|
|
json!(masked_catalog_api_key_for_provider(
|
|
state,
|
|
key,
|
|
provider_type,
|
|
)),
|
|
);
|
|
payload.insert("api_key_plain".to_string(), serde_json::Value::Null);
|
|
payload.insert("auth_type".to_string(), json!(key.auth_type));
|
|
payload.insert(
|
|
"auth_type_by_format".to_string(),
|
|
json!(key.auth_type_by_format),
|
|
);
|
|
payload.insert(
|
|
"allow_auth_channel_mismatch_formats".to_string(),
|
|
json!(key.allow_auth_channel_mismatch_formats),
|
|
);
|
|
payload.insert(
|
|
"credential_kind".to_string(),
|
|
json!(auth_semantics.credential_kind().as_str()),
|
|
);
|
|
payload.insert(
|
|
"runtime_auth_kind".to_string(),
|
|
json!(auth_semantics.runtime_auth_kind().as_str()),
|
|
);
|
|
payload.insert(
|
|
"oauth_managed".to_string(),
|
|
json!(auth_semantics.oauth_managed()),
|
|
);
|
|
payload.insert("agent_identity".to_string(), json!(agent_identity));
|
|
payload.insert(
|
|
"can_refresh_oauth".to_string(),
|
|
json!(provider_key_can_refresh_oauth(
|
|
auth_semantics,
|
|
provider_type,
|
|
auth_config.as_ref()
|
|
)),
|
|
);
|
|
payload.insert(
|
|
"can_export_oauth".to_string(),
|
|
json!(provider_key_can_export_oauth(
|
|
auth_semantics,
|
|
provider_type,
|
|
auth_config.as_ref()
|
|
)),
|
|
);
|
|
payload.insert(
|
|
"can_edit_oauth".to_string(),
|
|
json!(auth_semantics.can_edit_oauth()),
|
|
);
|
|
payload.insert("oauth_header_auth".to_string(), json!(oauth_header_auth));
|
|
payload.insert("name".to_string(), json!(key.name));
|
|
payload.insert("rate_multipliers".to_string(), json!(key.rate_multipliers));
|
|
payload.insert(
|
|
"internal_priority".to_string(),
|
|
json!(key.internal_priority),
|
|
);
|
|
payload.insert(
|
|
"global_priority_by_format".to_string(),
|
|
json!(key.global_priority_by_format),
|
|
);
|
|
payload.insert("rpm_limit".to_string(), json!(key.rpm_limit));
|
|
payload.insert("concurrent_limit".to_string(), json!(key.concurrent_limit));
|
|
payload.insert(
|
|
"allowed_models".to_string(),
|
|
serde_json::Value::Array(
|
|
json_string_list(key.allowed_models.as_ref())
|
|
.into_iter()
|
|
.map(serde_json::Value::String)
|
|
.collect(),
|
|
),
|
|
);
|
|
payload.insert("capabilities".to_string(), json!(key.capabilities));
|
|
payload.insert(
|
|
"oauth_expires_at".to_string(),
|
|
json!(auth_semantics
|
|
.can_show_oauth_metadata()
|
|
.then_some(key.expires_at_unix_secs)
|
|
.flatten()),
|
|
);
|
|
payload.insert(
|
|
"oauth_email".to_string(),
|
|
if auth_semantics.can_show_oauth_metadata() {
|
|
auth_config
|
|
.as_ref()
|
|
.and_then(|config| config.get("email"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null)
|
|
} else {
|
|
serde_json::Value::Null
|
|
},
|
|
);
|
|
payload.insert("oauth_plan_type".to_string(), json!(oauth_plan_type));
|
|
payload.insert(
|
|
"oauth_account_id".to_string(),
|
|
if auth_semantics.can_show_oauth_metadata() {
|
|
auth_config
|
|
.as_ref()
|
|
.and_then(|config| config.get("account_id"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null)
|
|
} else {
|
|
serde_json::Value::Null
|
|
},
|
|
);
|
|
payload.insert(
|
|
"oauth_account_name".to_string(),
|
|
if auth_semantics.can_show_oauth_metadata() {
|
|
auth_config
|
|
.as_ref()
|
|
.and_then(|config| config.get("account_name"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null)
|
|
} else {
|
|
serde_json::Value::Null
|
|
},
|
|
);
|
|
payload.insert(
|
|
"oauth_account_user_id".to_string(),
|
|
if auth_semantics.can_show_oauth_metadata() {
|
|
auth_config
|
|
.as_ref()
|
|
.and_then(|config| config.get("account_user_id"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null)
|
|
} else {
|
|
serde_json::Value::Null
|
|
},
|
|
);
|
|
payload.insert(
|
|
"oauth_organizations".to_string(),
|
|
serde_json::Value::Array(oauth_organizations),
|
|
);
|
|
payload.insert("oauth_temporary".to_string(), json!(oauth_temporary));
|
|
payload.insert(
|
|
"oauth_invalid_at".to_string(),
|
|
json!(auth_semantics
|
|
.can_show_oauth_metadata()
|
|
.then_some(key.oauth_invalid_at_unix_secs)
|
|
.flatten()),
|
|
);
|
|
payload.insert(
|
|
"oauth_invalid_reason".to_string(),
|
|
json!(auth_semantics
|
|
.can_show_oauth_metadata()
|
|
.then_some(key.oauth_invalid_reason.clone())
|
|
.flatten()),
|
|
);
|
|
payload.insert(
|
|
"status_snapshot".to_string(),
|
|
provider_key_status_snapshot_payload(key, provider_type),
|
|
);
|
|
payload.insert(
|
|
"cache_ttl_minutes".to_string(),
|
|
json!(key.cache_ttl_minutes),
|
|
);
|
|
payload.insert(
|
|
"max_probe_interval_minutes".to_string(),
|
|
json!(key.max_probe_interval_minutes),
|
|
);
|
|
payload.insert("health_by_format".to_string(), json!(key.health_by_format));
|
|
payload.insert(
|
|
"circuit_breaker_by_format".to_string(),
|
|
json!(key.circuit_breaker_by_format),
|
|
);
|
|
payload.insert("health_score".to_string(), json!(health_score));
|
|
payload.insert(
|
|
"consecutive_failures".to_string(),
|
|
json!(consecutive_failures),
|
|
);
|
|
payload.insert("last_failure_at".to_string(), json!(last_failure_at));
|
|
payload.insert(
|
|
"circuit_breaker_open".to_string(),
|
|
json!(circuit_breaker_open),
|
|
);
|
|
payload.insert(
|
|
"circuit_breaker_open_at".to_string(),
|
|
circuit_sample
|
|
.and_then(|value| value.get("open_at"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null),
|
|
);
|
|
payload.insert(
|
|
"next_probe_at".to_string(),
|
|
circuit_sample
|
|
.and_then(|value| value.get("next_probe_at"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null),
|
|
);
|
|
payload.insert(
|
|
"half_open_until".to_string(),
|
|
circuit_sample
|
|
.and_then(|value| value.get("half_open_until"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null),
|
|
);
|
|
payload.insert(
|
|
"half_open_successes".to_string(),
|
|
json!(circuit_sample
|
|
.and_then(|value| value.get("half_open_successes"))
|
|
.and_then(serde_json::Value::as_i64)
|
|
.unwrap_or(0)),
|
|
);
|
|
payload.insert(
|
|
"half_open_failures".to_string(),
|
|
json!(circuit_sample
|
|
.and_then(|value| value.get("half_open_failures"))
|
|
.and_then(serde_json::Value::as_i64)
|
|
.unwrap_or(0)),
|
|
);
|
|
payload.insert(
|
|
"request_results_window".to_string(),
|
|
circuit_sample
|
|
.and_then(|value| value.get("request_results_window"))
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null),
|
|
);
|
|
payload.insert("request_count".to_string(), json!(request_count));
|
|
payload.insert("success_count".to_string(), json!(success_count));
|
|
payload.insert("error_count".to_string(), json!(error_count));
|
|
payload.insert("success_rate".to_string(), json!(success_rate));
|
|
payload.insert(
|
|
"avg_response_time_ms".to_string(),
|
|
json!(avg_response_time_ms),
|
|
);
|
|
payload.insert("is_active".to_string(), json!(key.is_active));
|
|
payload.insert("is_adaptive".to_string(), json!(is_adaptive));
|
|
payload.insert(
|
|
"learned_rpm_limit".to_string(),
|
|
json!(key.learned_rpm_limit),
|
|
);
|
|
payload.insert("effective_limit".to_string(), json!(effective_limit));
|
|
payload.insert(
|
|
"utilization_samples".to_string(),
|
|
json!(key.utilization_samples),
|
|
);
|
|
payload.insert(
|
|
"last_probe_increase_at".to_string(),
|
|
json!(key
|
|
.last_probe_increase_at_unix_secs
|
|
.and_then(unix_secs_to_rfc3339)),
|
|
);
|
|
payload.insert(
|
|
"concurrent_429_count".to_string(),
|
|
json!(key.concurrent_429_count),
|
|
);
|
|
payload.insert("rpm_429_count".to_string(), json!(key.rpm_429_count));
|
|
payload.insert(
|
|
"last_429_at".to_string(),
|
|
json!(key.last_429_at_unix_secs.and_then(unix_secs_to_rfc3339)),
|
|
);
|
|
payload.insert("last_429_type".to_string(), json!(key.last_429_type));
|
|
payload.insert("note".to_string(), json!(key.note));
|
|
payload.insert(
|
|
"auto_fetch_models".to_string(),
|
|
json!(key.auto_fetch_models),
|
|
);
|
|
payload.insert(
|
|
"last_models_fetch_at".to_string(),
|
|
json!(key
|
|
.last_models_fetch_at_unix_secs
|
|
.and_then(unix_secs_to_rfc3339)),
|
|
);
|
|
payload.insert(
|
|
"last_models_fetch_error".to_string(),
|
|
json!(key.last_models_fetch_error),
|
|
);
|
|
payload.insert("locked_models".to_string(), json!(key.locked_models));
|
|
payload.insert(
|
|
"model_include_patterns".to_string(),
|
|
json!(key.model_include_patterns),
|
|
);
|
|
payload.insert(
|
|
"model_exclude_patterns".to_string(),
|
|
json!(key.model_exclude_patterns),
|
|
);
|
|
payload.insert(
|
|
"upstream_metadata".to_string(),
|
|
json!(key.upstream_metadata),
|
|
);
|
|
payload.insert("proxy".to_string(), json!(key.proxy));
|
|
payload.insert("fingerprint".to_string(), json!(key.fingerprint));
|
|
payload.insert(
|
|
"last_used_at".to_string(),
|
|
json!(key.last_used_at_unix_secs.and_then(unix_secs_to_rfc3339)),
|
|
);
|
|
payload.insert(
|
|
"created_at".to_string(),
|
|
json!(unix_secs_to_rfc3339(
|
|
key.created_at_unix_ms.unwrap_or(now_unix_secs)
|
|
)),
|
|
);
|
|
payload.insert(
|
|
"updated_at".to_string(),
|
|
json!(unix_secs_to_rfc3339(
|
|
key.updated_at_unix_secs.unwrap_or(now_unix_secs)
|
|
)),
|
|
);
|
|
serde_json::Value::Object(payload)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn sample_catalog_key() -> StoredProviderCatalogKey {
|
|
let encrypted_api_key =
|
|
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-test-123")
|
|
.expect("api key ciphertext should build");
|
|
StoredProviderCatalogKey::new(
|
|
"key-test".to_string(),
|
|
"provider-test".to_string(),
|
|
"default".to_string(),
|
|
"api_key".to_string(),
|
|
None,
|
|
true,
|
|
)
|
|
.expect("key should build")
|
|
.with_transport_fields(
|
|
Some(json!(["openai:chat"])),
|
|
encrypted_api_key,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.expect("key transport should build")
|
|
}
|
|
|
|
#[test]
|
|
fn responses_key_scope_covers_search_in_one_direction() {
|
|
let mut responses_key = sample_catalog_key();
|
|
responses_key.api_formats = Some(json!(["openai:responses"]));
|
|
assert!(provider_catalog_key_supports_format(
|
|
&responses_key,
|
|
"codex",
|
|
"openai:search",
|
|
));
|
|
|
|
let mut search_key = sample_catalog_key();
|
|
search_key.api_formats = Some(json!(["openai:search"]));
|
|
assert!(!provider_catalog_key_supports_format(
|
|
&search_key,
|
|
"codex",
|
|
"openai:responses",
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn masked_catalog_api_key_handles_unicode_plaintext_without_panicking() {
|
|
let state = AppState::new().expect("gateway should build");
|
|
let encrypted_api_key =
|
|
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "测试-密钥-1234567890")
|
|
.expect("api key ciphertext should build");
|
|
let key = StoredProviderCatalogKey::new(
|
|
"key-unicode".to_string(),
|
|
"provider-test".to_string(),
|
|
"default".to_string(),
|
|
"api_key".to_string(),
|
|
None,
|
|
true,
|
|
)
|
|
.expect("key should build")
|
|
.with_transport_fields(
|
|
Some(json!(["openai:chat"])),
|
|
encrypted_api_key,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.expect("key transport should build");
|
|
|
|
let masked = masked_catalog_api_key(&state, &key);
|
|
assert!(masked.contains("***"));
|
|
assert_ne!(masked, "***ERROR***");
|
|
}
|
|
|
|
#[test]
|
|
fn provider_aware_mask_labels_agent_identity_without_exposing_placeholder() {
|
|
let state = AppState::new().expect("gateway should build");
|
|
let encrypted_placeholder =
|
|
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "__placeholder__")
|
|
.expect("placeholder ciphertext should build");
|
|
let encrypted_auth_config = encrypt_python_fernet_plaintext(
|
|
DEVELOPMENT_ENCRYPTION_KEY,
|
|
r#"{"provider_type":"codex","auth_mode":"agentIdentity","agent_runtime_id":"runtime-1","agent_private_key":"base64-private-key","task_id":"task-1"}"#,
|
|
)
|
|
.expect("auth config ciphertext should build");
|
|
let key = StoredProviderCatalogKey::new(
|
|
"key-agent".to_string(),
|
|
"provider-codex".to_string(),
|
|
"agent".to_string(),
|
|
"oauth".to_string(),
|
|
None,
|
|
true,
|
|
)
|
|
.expect("key should build")
|
|
.with_transport_fields(
|
|
Some(json!(["openai:responses"])),
|
|
encrypted_placeholder,
|
|
Some(encrypted_auth_config),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.expect("key transport should build");
|
|
|
|
assert_eq!(
|
|
masked_catalog_api_key_for_provider(&state, &key, "codex"),
|
|
"[Agent Identity]"
|
|
);
|
|
assert!(!masked_catalog_api_key_for_provider(&state, &key, "codex").contains("placeholder"));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_missing_quota_from_upstream_metadata() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"updated_at": 1_775_553_285u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 55.0,
|
|
"primary_reset_at": 1_900_000_000u64,
|
|
"secondary_used_percent": 12.5,
|
|
"secondary_reset_at": 1_900_500_000u64,
|
|
"has_credits": true,
|
|
"credits_balance": 42.0
|
|
}
|
|
}));
|
|
|
|
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("provider_type"), Some(&json!("codex")));
|
|
assert_eq!(quota.get("plan_type"), Some(&json!("plus")));
|
|
assert_eq!(quota.get("updated_at"), Some(&json!(1_775_553_285u64)));
|
|
assert_eq!(quota.get("reset_at"), Some(&json!(1_900_000_000u64)));
|
|
assert_eq!(
|
|
quota
|
|
.get("credits")
|
|
.and_then(Value::as_object)
|
|
.and_then(|credits| credits.get("balance")),
|
|
Some(&json!(42.0))
|
|
);
|
|
assert_eq!(
|
|
quota.get("windows").and_then(Value::as_array).map(Vec::len),
|
|
Some(2usize)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_codex_reset_credits() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"updated_at": 1_775_553_285u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 55.0,
|
|
"primary_reset_at": 1_900_000_000u64,
|
|
"has_credits": true,
|
|
"credits_balance": 42.0,
|
|
"reset_credits": {
|
|
"available_count": 2,
|
|
"updated_at": 1_775_553_285u64,
|
|
"detail_source": "wham_readonly",
|
|
"detail_status": "available",
|
|
"credits": [
|
|
{
|
|
"id": "bbbbbbbb-1111-2222-3333-444444444444",
|
|
"display_key": "bbbbbbbb",
|
|
"status": "available",
|
|
"expires_at": 1_775_900_000u64
|
|
},
|
|
{
|
|
"id": "aaaaaaaa-1111-2222-3333-444444444444",
|
|
"display_key": "aaaaaaaa",
|
|
"status": "available",
|
|
"expires_at": 1_775_700_000u64
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}));
|
|
|
|
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("exhausted"), Some(&json!(false)));
|
|
assert_eq!(
|
|
payload.pointer("/quota/reset_credits/available_count"),
|
|
Some(&json!(2u64))
|
|
);
|
|
assert_eq!(
|
|
payload.pointer("/quota/reset_credits/credits/0/display_key"),
|
|
Some(&json!("aaaaaaaa"))
|
|
);
|
|
assert_eq!(
|
|
payload.pointer("/quota/reset_credits/credits/0/remaining_seconds"),
|
|
Some(&json!(146_715u64))
|
|
);
|
|
assert_eq!(
|
|
quota
|
|
.get("credits")
|
|
.and_then(Value::as_object)
|
|
.and_then(|credits| credits.get("balance")),
|
|
Some(&json!(42.0))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_codex_spark_windows() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"updated_at": 1_775_553_285u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 55.0,
|
|
"primary_reset_at": 1_900_000_000u64,
|
|
"secondary_used_percent": 12.5,
|
|
"secondary_reset_at": 1_900_500_000u64,
|
|
"spark_primary_used_percent": 40.0,
|
|
"spark_primary_reset_at": 1_900_100_000u64,
|
|
"spark_primary_window_minutes": 300u64,
|
|
"spark_secondary_used_percent": 5.0,
|
|
"spark_secondary_reset_at": 1_900_600_000u64,
|
|
"spark_secondary_window_minutes": 10_080u64
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
let spark_5h = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("spark_5h")))
|
|
.expect("Spark 5H window should exist");
|
|
let spark_weekly = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("spark_weekly")))
|
|
.expect("Spark weekly window should exist");
|
|
|
|
assert_eq!(windows.len(), 4);
|
|
assert_eq!(spark_5h.get("label"), Some(&json!("Spark 5H")));
|
|
assert_eq!(spark_5h.get("remaining_ratio"), Some(&json!(0.6)));
|
|
assert_eq!(spark_weekly.get("label"), Some(&json!("Spark 周")));
|
|
assert_eq!(spark_weekly.get("remaining_ratio"), Some(&json!(0.95)));
|
|
}
|
|
|
|
#[test]
|
|
fn codex_wham_snapshot_does_not_duplicate_normalized_spark_windows() {
|
|
let codex = admin_provider_quota_pure::parse_codex_wham_usage_response(
|
|
&json!({
|
|
"plan_type": "plus",
|
|
"rate_limit": {
|
|
"primary_window": {"used_percent": 25.0},
|
|
"secondary_window": {"used_percent": 10.0}
|
|
},
|
|
"additional_rate_limits": [{
|
|
"limit_name": "GPT-5.3-Codex-Spark",
|
|
"rate_limit": {
|
|
"primary_window": {
|
|
"used_percent": 40.0,
|
|
"limit_window_seconds": 18_000
|
|
},
|
|
"secondary_window": {
|
|
"used_percent": 5.0,
|
|
"limit_window_seconds": 604_800
|
|
}
|
|
}
|
|
}]
|
|
}),
|
|
1_777_000_000,
|
|
)
|
|
.expect("Codex WHAM quota should parse");
|
|
let upstream_metadata = json!({"codex": codex});
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
None,
|
|
"codex",
|
|
Some(&upstream_metadata),
|
|
"refresh_api",
|
|
)
|
|
.expect("Codex quota snapshot should sync");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
let spark_codes = windows
|
|
.iter()
|
|
.filter_map(|window| window.get("code").and_then(Value::as_str))
|
|
.filter(|code| code.starts_with("spark_"))
|
|
.collect::<Vec<_>>();
|
|
|
|
assert_eq!(spark_codes, vec!["spark_5h", "spark_weekly"]);
|
|
}
|
|
|
|
#[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();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"updated_at": 1_775_553_285u64,
|
|
"primary_used_percent": 55.0,
|
|
"primary_reset_after_seconds": 3_600u64
|
|
}
|
|
}));
|
|
|
|
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");
|
|
let window = quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.and_then(|windows| windows.first())
|
|
.and_then(Value::as_object)
|
|
.expect("quota window should exist");
|
|
|
|
assert_eq!(quota.get("reset_at"), Some(&json!(1_775_556_885u64)));
|
|
assert_eq!(quota.get("reset_seconds"), Some(&json!(3_600u64)));
|
|
assert_eq!(window.get("reset_at"), Some(&json!(1_775_556_885u64)));
|
|
assert_eq!(window.get("reset_seconds"), Some(&json!(3_600u64)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_chatgpt_web_image_quota() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"chatgpt_web": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"plan_type": "free",
|
|
"image_quota_remaining": 24.0,
|
|
"image_quota_reset_at": 1_778_157_172u64
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "chatgpt_web");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let window = quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.and_then(|windows| windows.first())
|
|
.and_then(Value::as_object)
|
|
.expect("image quota window should exist");
|
|
|
|
assert_eq!(quota.get("provider_type"), Some(&json!("chatgpt_web")));
|
|
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
|
assert_eq!(quota.get("plan_type"), Some(&json!("free")));
|
|
assert_eq!(quota.get("reset_at"), Some(&json!(1_778_157_172u64)));
|
|
assert_eq!(quota.get("usage_ratio"), Some(&json!(0.0)));
|
|
assert_eq!(window.get("code"), Some(&json!("image_gen")));
|
|
assert_eq!(window.get("remaining_value"), Some(&json!(24.0)));
|
|
assert_eq!(window.get("limit_value"), Some(&json!(24.0)));
|
|
assert_eq!(window.get("used_value"), Some(&json!(0.0)));
|
|
assert_eq!(window.get("remaining_ratio"), Some(&json!(1.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_ignores_chatgpt_web_legacy_free_25_limit() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"chatgpt_web": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"plan_type": "free",
|
|
"image_quota_remaining": 19.0,
|
|
"image_quota_total": 25.0
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "chatgpt_web");
|
|
let window = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.and_then(|quota| quota.get("windows"))
|
|
.and_then(Value::as_array)
|
|
.and_then(|windows| windows.first())
|
|
.and_then(Value::as_object)
|
|
.expect("image quota window should exist");
|
|
|
|
assert_eq!(window.get("remaining_value"), Some(&json!(19.0)));
|
|
assert_eq!(window.get("limit_value"), Some(&json!(19.0)));
|
|
assert_eq!(window.get("used_value"), Some(&json!(0.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_grok_model_quota() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"grok": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"pool_tier": "heavy",
|
|
"plan_type": "heavy",
|
|
"quota_by_model": {
|
|
"quota_auto": {
|
|
"display_name": "auto",
|
|
"remaining_fraction": 0.4,
|
|
"used_percent": 60.0,
|
|
"remaining": 60.0,
|
|
"total": 150.0,
|
|
"reset_at": 1_778_157_172u64,
|
|
"is_exhausted": false
|
|
},
|
|
"quota_heavy": {
|
|
"display_name": "heavy",
|
|
"remaining_fraction": 0.0,
|
|
"used_percent": 100.0,
|
|
"reset_at": 1_778_157_172u64,
|
|
"is_exhausted": true
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "grok");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let windows = quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.expect("grok quota windows should exist");
|
|
|
|
assert_eq!(quota.get("provider_type"), Some(&json!("grok")));
|
|
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
|
assert_eq!(quota.get("plan_type"), Some(&json!("heavy")));
|
|
assert_eq!(quota.get("pool_tier"), Some(&json!("heavy")));
|
|
assert_eq!(quota.get("exhausted"), Some(&json!(false)));
|
|
assert_eq!(quota.get("usage_ratio"), Some(&json!(1.0)));
|
|
assert_eq!(quota.get("reset_at"), Some(&json!(1_778_157_172u64)));
|
|
assert_eq!(windows.len(), 2);
|
|
assert!(windows.iter().any(|window| {
|
|
window
|
|
.get("code")
|
|
.and_then(Value::as_str)
|
|
.is_some_and(|code| code == "model:quota_auto")
|
|
}));
|
|
let auto = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("model:quota_auto")))
|
|
.expect("auto quota window should exist");
|
|
assert_eq!(auto.get("remaining_value"), Some(&json!(60.0)));
|
|
assert_eq!(auto.get("limit_value"), Some(&json!(150.0)));
|
|
assert_eq!(auto.get("used_value"), Some(&json!(90.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_gemini_cli_account_credits() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"gemini_cli": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"plan_type": "g1-pro-tier",
|
|
"paidTier": {
|
|
"availableCredits": 123.5,
|
|
"consumedCredits": 7.0,
|
|
"totalCredits": 200.0
|
|
},
|
|
"quota_by_model": {
|
|
"gemini-2.5-pro": {
|
|
"display_name": "Gemini 2.5 Pro",
|
|
"remaining_fraction": 0.75,
|
|
"is_exhausted": false
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "gemini_cli");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let credits = quota
|
|
.get("credits")
|
|
.and_then(Value::as_object)
|
|
.expect("credits snapshot should exist");
|
|
let windows = quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.expect("Gemini CLI model windows should exist");
|
|
|
|
assert_eq!(quota.get("provider_type"), Some(&json!("gemini_cli")));
|
|
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
|
assert_eq!(quota.get("plan_type"), Some(&json!("g1-pro-tier")));
|
|
assert_eq!(quota.get("updated_at"), Some(&json!(1_778_067_246u64)));
|
|
assert_eq!(credits.get("remaining"), Some(&json!(123.5)));
|
|
assert_eq!(credits.get("balance"), Some(&json!(123.5)));
|
|
assert_eq!(credits.get("consumed"), Some(&json!(7.0)));
|
|
assert_eq!(credits.get("total"), Some(&json!(200.0)));
|
|
assert_eq!(credits.get("has_credits"), Some(&json!(true)));
|
|
assert_eq!(windows.len(), 1);
|
|
assert_eq!(windows[0].get("remaining_ratio"), Some(&json!(0.75)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_windsurf_daily_and_weekly_quota() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"windsurf": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"plan_name": "Pro",
|
|
"daily_remaining_percent": 40.0,
|
|
"weekly_remaining_percent": 65.0,
|
|
"daily_reset_at": 1_778_100_000u64,
|
|
"weekly_reset_at": 1_778_600_000u64,
|
|
"prompt_used": 12.0,
|
|
"prompt_limit": 100.0,
|
|
"prompt_remaining": 88.0,
|
|
"flex_used": 3.0,
|
|
"flex_limit": 10.0,
|
|
"flex_remaining": 7.0,
|
|
"allowed_models_count": 82
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let windows = quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.expect("windsurf quota windows should exist");
|
|
let daily = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("daily")))
|
|
.expect("daily quota window should exist");
|
|
let weekly = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("weekly")))
|
|
.expect("weekly quota window should exist");
|
|
|
|
assert_eq!(quota.get("provider_type"), Some(&json!("windsurf")));
|
|
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
|
assert_eq!(quota.get("plan_type"), Some(&json!("Pro")));
|
|
assert_eq!(quota.get("usage_ratio"), Some(&json!(0.6)));
|
|
assert_eq!(quota.get("reset_at"), Some(&json!(1_778_100_000u64)));
|
|
assert_eq!(daily.get("remaining_ratio"), Some(&json!(0.4)));
|
|
assert_eq!(daily.get("used_ratio"), Some(&json!(0.6)));
|
|
assert_eq!(daily.get("reset_seconds"), Some(&json!(32_754u64)));
|
|
assert_eq!(weekly.get("remaining_ratio"), Some(&json!(0.65)));
|
|
assert_eq!(weekly.get("used_ratio"), Some(&json!(0.35)));
|
|
assert_eq!(weekly.get("reset_seconds"), Some(&json!(532_754u64)));
|
|
assert_eq!(quota.get("allowed_models_count"), Some(&json!(82)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_treats_windsurf_rate_limit_as_cooldown() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"windsurf": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"daily_remaining_percent": 80.0,
|
|
"rate_limit": {
|
|
"limited": true,
|
|
"retry_after_ms": 60_001u64,
|
|
"message": "slow down"
|
|
},
|
|
"last_error": "slow down"
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let rate_window = quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.and_then(|windows| {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("rate_limit")))
|
|
})
|
|
.expect("rate limit window should exist");
|
|
|
|
assert_eq!(quota.get("code"), Some(&json!("cooldown")));
|
|
assert_eq!(quota.get("exhausted"), Some(&json!(false)));
|
|
assert_eq!(quota.get("reset_seconds"), Some(&json!(61u64)));
|
|
assert_eq!(rate_window.get("is_exhausted"), Some(&json!(false)));
|
|
assert_eq!(rate_window.get("reset_seconds"), Some(&json!(61u64)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_keeps_windsurf_capacity_probe_without_retry_after_ok() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"windsurf": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"daily_remaining_percent": 100.0,
|
|
"weekly_remaining_percent": 100.0,
|
|
"rate_limit": {
|
|
"limited": true,
|
|
"has_capacity": false,
|
|
"messages_remaining": 0.0,
|
|
"max_messages": 100.0
|
|
}
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let has_rate_limit_window =
|
|
quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.is_some_and(|windows| {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.any(|window| window.get("code") == Some(&json!("rate_limit")))
|
|
});
|
|
|
|
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
|
assert_eq!(quota.get("label"), Some(&Value::Null));
|
|
assert_eq!(quota.get("exhausted"), Some(&json!(false)));
|
|
assert_eq!(
|
|
payload.pointer("/quota/rate_limit/limited"),
|
|
Some(&json!(true))
|
|
);
|
|
assert!(!has_rate_limit_window);
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_refreshes_stale_windsurf_cooldown_when_probe_has_capacity(
|
|
) {
|
|
let mut key = sample_catalog_key();
|
|
key.status_snapshot = Some(json!({
|
|
"quota": {
|
|
"version": 2,
|
|
"provider_type": "windsurf",
|
|
"code": "cooldown",
|
|
"label": "冷却中",
|
|
"exhausted": false,
|
|
"windows": [
|
|
{
|
|
"code": "daily",
|
|
"unit": "percent",
|
|
"label": "日",
|
|
"scope": "account",
|
|
"remaining_ratio": 0.99,
|
|
"is_exhausted": false
|
|
},
|
|
{
|
|
"code": "rate_limit",
|
|
"unit": "count",
|
|
"label": "速率",
|
|
"scope": "account",
|
|
"is_exhausted": false,
|
|
"reset_seconds": null
|
|
}
|
|
],
|
|
"rate_limit": {
|
|
"limited": true,
|
|
"has_capacity": true,
|
|
"messages_remaining": -1,
|
|
"max_messages": -1
|
|
}
|
|
}
|
|
}));
|
|
key.upstream_metadata = Some(json!({
|
|
"windsurf": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"daily_remaining_percent": 99.0,
|
|
"weekly_remaining_percent": 100.0,
|
|
"allowed_models_count": 118,
|
|
"rate_limit": {
|
|
"limited": true,
|
|
"has_capacity": true,
|
|
"messages_remaining": -1,
|
|
"max_messages": -1
|
|
}
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "windsurf");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
let has_rate_limit_window =
|
|
quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.is_some_and(|windows| {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.any(|window| window.get("code") == Some(&json!("rate_limit")))
|
|
});
|
|
|
|
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
|
assert_eq!(quota.get("label"), Some(&Value::Null));
|
|
assert_eq!(quota.get("allowed_models_count"), Some(&json!(118u64)));
|
|
assert!(!has_rate_limit_window);
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_marks_windsurf_banned_and_quarantined_blocking() {
|
|
let mut banned_key = sample_catalog_key();
|
|
banned_key.upstream_metadata = Some(json!({
|
|
"windsurf": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"banned": true,
|
|
"reason": "forbidden"
|
|
}
|
|
}));
|
|
let banned_payload = provider_key_status_snapshot_payload(&banned_key, "windsurf");
|
|
|
|
assert_eq!(
|
|
banned_payload.pointer("/quota/code"),
|
|
Some(&json!("banned"))
|
|
);
|
|
assert_eq!(
|
|
banned_payload.pointer("/quota/exhausted"),
|
|
Some(&json!(true))
|
|
);
|
|
assert_eq!(
|
|
banned_payload.pointer("/account/code"),
|
|
Some(&json!("account_banned"))
|
|
);
|
|
assert_eq!(
|
|
banned_payload.pointer("/account/blocked"),
|
|
Some(&json!(true))
|
|
);
|
|
|
|
let mut quarantined_key = sample_catalog_key();
|
|
quarantined_key.upstream_metadata = Some(json!({
|
|
"windsurf": {
|
|
"updated_at": 1_778_067_246u64,
|
|
"quarantined": true
|
|
}
|
|
}));
|
|
let quarantined_payload =
|
|
provider_key_status_snapshot_payload(&quarantined_key, "windsurf");
|
|
|
|
assert_eq!(
|
|
quarantined_payload.pointer("/quota/code"),
|
|
Some(&json!("quarantined"))
|
|
);
|
|
assert_eq!(
|
|
quarantined_payload.pointer("/quota/exhausted"),
|
|
Some(&json!(true))
|
|
);
|
|
assert_eq!(
|
|
quarantined_payload.pointer("/account/code"),
|
|
Some(&json!("account_quarantined"))
|
|
);
|
|
assert_eq!(
|
|
quarantined_payload.pointer("/account/blocked"),
|
|
Some(&json!(true))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_preserves_existing_materialized_quota_snapshot() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"updated_at": 100u64,
|
|
"primary_used_percent": 100.0
|
|
}
|
|
}));
|
|
key.status_snapshot = Some(json!({
|
|
"oauth": {
|
|
"code": "none",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"expires_at": serde_json::Value::Null,
|
|
"invalid_at": serde_json::Value::Null,
|
|
"source": serde_json::Value::Null,
|
|
"requires_reauth": false,
|
|
"expiring_soon": false
|
|
},
|
|
"account": {
|
|
"code": "ok",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"blocked": false,
|
|
"source": serde_json::Value::Null,
|
|
"recoverable": false
|
|
},
|
|
"quota": {
|
|
"version": 2,
|
|
"provider_type": "codex",
|
|
"code": "ok",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"freshness": "fresh",
|
|
"source": "refresh_api",
|
|
"observed_at": 200u64,
|
|
"exhausted": false,
|
|
"usage_ratio": 0.25,
|
|
"updated_at": 200u64,
|
|
"reset_seconds": 3600u64,
|
|
"plan_type": "team",
|
|
"windows": [{
|
|
"code": "weekly",
|
|
"label": "周",
|
|
"scope": "account",
|
|
"unit": "percent",
|
|
"used_ratio": 0.25,
|
|
"remaining_ratio": 0.75,
|
|
"reset_at": 1_900_000_000u64,
|
|
"reset_seconds": 3600u64
|
|
}]
|
|
}
|
|
}));
|
|
|
|
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("updated_at"), Some(&json!(200u64)));
|
|
assert_eq!(quota.get("plan_type"), Some(&json!("team")));
|
|
assert_eq!(
|
|
quota
|
|
.get("windows")
|
|
.and_then(Value::as_array)
|
|
.and_then(|windows| windows.first())
|
|
.and_then(Value::as_object)
|
|
.and_then(|window| window.get("used_ratio")),
|
|
Some(&json!(0.25))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_restores_complete_codex_cache() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"updated_at": 200u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 89.0,
|
|
"primary_reset_at": 1_900_000_000u64,
|
|
"spark_primary_used_percent": 40.0,
|
|
"spark_primary_reset_at": 1_900_100_000u64,
|
|
"reset_credits": {
|
|
"available_count": 3,
|
|
"updated_at": 200u64,
|
|
"detail_status": "available",
|
|
"credits": []
|
|
}
|
|
}
|
|
}));
|
|
key.status_snapshot = Some(json!({
|
|
"quota": {
|
|
"version": 2,
|
|
"provider_type": "codex",
|
|
"updated_at": 200u64,
|
|
"windows": [{
|
|
"code": "weekly",
|
|
"used_ratio": 1.0,
|
|
"reset_at": 1_900_000_000u64
|
|
}]
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
|
|
assert_eq!(payload.pointer("/quota/plan_type"), Some(&json!("plus")));
|
|
assert_eq!(
|
|
payload.pointer("/quota/reset_credits/available_count"),
|
|
Some(&json!(3u64))
|
|
);
|
|
assert!(windows.iter().any(|window| window["code"] == "spark_5h"));
|
|
assert_eq!(
|
|
windows
|
|
.iter()
|
|
.find(|window| window["code"] == "weekly")
|
|
.and_then(|window| window.get("used_ratio")),
|
|
Some(&json!(0.89))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_preserves_codex_usage_state() {
|
|
let current_status_snapshot = json!({
|
|
"quota": {
|
|
"version": 2,
|
|
"provider_type": "codex",
|
|
"windows": [
|
|
{
|
|
"code": "weekly",
|
|
"usage_reset_at": 1_775_600_000u64,
|
|
"usage": {
|
|
"request_count": 3,
|
|
"total_tokens": 375,
|
|
"total_cost_usd": "0.60000000"
|
|
},
|
|
"reset_at": 1_900_000_000u64,
|
|
"window_minutes": 10_080u64
|
|
},
|
|
{
|
|
"code": "5h",
|
|
"usage_reset_at": 1_775_700_000u64,
|
|
"usage": {
|
|
"request_count": 2,
|
|
"total_tokens": 225,
|
|
"total_cost_usd": "0.30000000"
|
|
},
|
|
"reset_at": 1_900_500_000u64,
|
|
"window_minutes": 300u64
|
|
}
|
|
]
|
|
}
|
|
});
|
|
let upstream_metadata = json!({
|
|
"codex": {
|
|
"updated_at": 1_775_800_000u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 5.0,
|
|
"primary_reset_at": 1_900_000_000u64,
|
|
"primary_window_minutes": 10_080u64,
|
|
"secondary_used_percent": 1.0,
|
|
"secondary_reset_at": 1_900_500_000u64,
|
|
"secondary_window_minutes": 300u64
|
|
}
|
|
});
|
|
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
Some(¤t_status_snapshot),
|
|
"codex",
|
|
Some(&upstream_metadata),
|
|
"refresh_api",
|
|
)
|
|
.expect("quota snapshot should sync");
|
|
let windows = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.and_then(|quota| quota.get("windows"))
|
|
.and_then(Value::as_array)
|
|
.expect("quota windows should exist");
|
|
let weekly = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("weekly")))
|
|
.expect("weekly window should exist");
|
|
let five_h = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("5h")))
|
|
.expect("5h window should exist");
|
|
|
|
assert_eq!(weekly.get("usage_reset_at"), Some(&json!(1_775_600_000u64)));
|
|
assert_eq!(
|
|
weekly
|
|
.get("usage")
|
|
.and_then(|usage| usage.get("request_count")),
|
|
Some(&json!(3))
|
|
);
|
|
assert_eq!(
|
|
weekly
|
|
.get("usage")
|
|
.and_then(|usage| usage.get("total_tokens")),
|
|
Some(&json!(375))
|
|
);
|
|
assert_eq!(
|
|
weekly
|
|
.get("usage")
|
|
.and_then(|usage| usage.get("total_cost_usd")),
|
|
Some(&json!("0.60000000"))
|
|
);
|
|
assert_eq!(five_h.get("usage_reset_at"), Some(&json!(1_775_700_000u64)));
|
|
assert_eq!(
|
|
five_h
|
|
.get("usage")
|
|
.and_then(|usage| usage.get("request_count")),
|
|
Some(&json!(2))
|
|
);
|
|
assert_eq!(
|
|
five_h
|
|
.get("usage")
|
|
.and_then(|usage| usage.get("total_tokens")),
|
|
Some(&json!(225))
|
|
);
|
|
assert_eq!(
|
|
five_h
|
|
.get("usage")
|
|
.and_then(|usage| usage.get("total_cost_usd")),
|
|
Some(&json!("0.30000000"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_honors_codex_limit_signal() {
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
None,
|
|
"codex",
|
|
Some(&json!({
|
|
"codex": {
|
|
"updated_at": 1_775_800_000u64,
|
|
"allowed": false,
|
|
"limit_reached": true
|
|
}
|
|
})),
|
|
"websocket_response_body",
|
|
)
|
|
.expect("explicit Codex limit signal should build a quota snapshot");
|
|
|
|
assert_eq!(payload.pointer("/quota/code"), Some(&json!("exhausted")));
|
|
assert_eq!(payload.pointer("/quota/exhausted"), Some(&json!(true)));
|
|
assert_eq!(payload.pointer("/quota/allowed"), Some(&json!(false)));
|
|
assert_eq!(payload.pointer("/quota/limit_reached"), Some(&json!(true)));
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_drops_codex_usage_state_when_window_resets() {
|
|
let current_status_snapshot = json!({
|
|
"quota": {
|
|
"version": 2,
|
|
"provider_type": "codex",
|
|
"windows": [
|
|
{
|
|
"code": "weekly",
|
|
"usage_reset_at": 1_775_600_000u64,
|
|
"usage": {
|
|
"request_count": 3,
|
|
"total_tokens": 375,
|
|
"total_cost_usd": "0.60000000"
|
|
},
|
|
"reset_at": 1_900_000_000u64,
|
|
"window_minutes": 10_080u64
|
|
}
|
|
]
|
|
}
|
|
});
|
|
let upstream_metadata = json!({
|
|
"codex": {
|
|
"updated_at": 1_900_000_100u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 0.0,
|
|
"primary_reset_at": 1_960_480_100u64,
|
|
"primary_window_minutes": 10_080u64
|
|
}
|
|
});
|
|
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
Some(¤t_status_snapshot),
|
|
"codex",
|
|
Some(&upstream_metadata),
|
|
"refresh_api",
|
|
)
|
|
.expect("quota snapshot should sync");
|
|
let weekly = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist")
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("weekly")))
|
|
.expect("weekly window should exist");
|
|
|
|
assert_eq!(weekly.get("reset_at"), Some(&json!(1_960_480_100u64)));
|
|
assert!(weekly.get("usage_reset_at").is_none());
|
|
assert!(weekly.get("usage").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_defaults_codex_window_minutes() {
|
|
let upstream_metadata = json!({
|
|
"codex": {
|
|
"updated_at": 1_775_800_000u64,
|
|
"plan_type": "plus",
|
|
"primary_used_percent": 5.0,
|
|
"primary_reset_at": 1_900_000_000u64,
|
|
"secondary_used_percent": 1.0,
|
|
"secondary_reset_at": 1_900_500_000u64
|
|
}
|
|
});
|
|
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
None,
|
|
"codex",
|
|
Some(&upstream_metadata),
|
|
"response_headers",
|
|
)
|
|
.expect("quota snapshot should sync");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
let weekly = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("weekly")))
|
|
.expect("weekly window should exist");
|
|
let five_h = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("code") == Some(&json!("5h")))
|
|
.expect("5h window should exist");
|
|
|
|
assert_eq!(weekly.get("window_minutes"), Some(&json!(10_080u64)));
|
|
assert_eq!(five_h.get("window_minutes"), Some(&json!(300u64)));
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_labels_actual_monthly_window() {
|
|
let upstream_metadata = json!({
|
|
"codex": {
|
|
"updated_at": 1_784_287_450u64,
|
|
"plan_type": "team",
|
|
"primary_used_percent": 14.0,
|
|
"primary_reset_at": 1_786_915_122u64,
|
|
"primary_window_minutes": 43_800u64,
|
|
"secondary_used_percent": 0.0,
|
|
"secondary_reset_after_seconds": 0u64,
|
|
"secondary_window_minutes": 0u64
|
|
}
|
|
});
|
|
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
None,
|
|
"codex",
|
|
Some(&upstream_metadata),
|
|
"response_headers",
|
|
)
|
|
.expect("quota snapshot should sync");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
|
|
assert_eq!(windows.len(), 1);
|
|
assert_eq!(windows[0]["code"], json!("monthly"));
|
|
assert_eq!(windows[0]["label"], json!("月"));
|
|
assert_eq!(windows[0]["window_minutes"], json!(43_800u64));
|
|
assert_eq!(windows[0]["remaining_ratio"], json!(0.86));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_thin_ok_snapshot_from_upstream_metadata() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"antigravity": {
|
|
"updated_at": 1_775_553_285u64,
|
|
"quota_by_model": {
|
|
"gemini-2.5-pro": { "used_percent": 0.0 },
|
|
"gemini-2.5-flash": { "used_percent": 25.0 }
|
|
}
|
|
}
|
|
}));
|
|
key.status_snapshot = Some(json!({
|
|
"oauth": {
|
|
"code": "none",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"expires_at": serde_json::Value::Null,
|
|
"invalid_at": serde_json::Value::Null,
|
|
"source": serde_json::Value::Null,
|
|
"requires_reauth": false,
|
|
"expiring_soon": false
|
|
},
|
|
"account": {
|
|
"code": "ok",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"blocked": false,
|
|
"source": serde_json::Value::Null,
|
|
"recoverable": false
|
|
},
|
|
"quota": {
|
|
"version": 2,
|
|
"provider_type": "antigravity",
|
|
"code": "ok",
|
|
"label": serde_json::Value::Null,
|
|
"reason": serde_json::Value::Null,
|
|
"freshness": "fresh",
|
|
"source": "refresh_api",
|
|
"observed_at": 100u64,
|
|
"exhausted": false,
|
|
"usage_ratio": 0.0,
|
|
"updated_at": 100u64,
|
|
"reset_seconds": serde_json::Value::Null,
|
|
"plan_type": serde_json::Value::Null
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "antigravity");
|
|
let quota = payload
|
|
.get("quota")
|
|
.and_then(Value::as_object)
|
|
.expect("quota snapshot should be object");
|
|
|
|
assert_eq!(quota.get("provider_type"), Some(&json!("antigravity")));
|
|
assert_eq!(quota.get("updated_at"), Some(&json!(1_775_553_285u64)));
|
|
assert_eq!(
|
|
quota.get("windows").and_then(Value::as_array).map(Vec::len),
|
|
Some(2usize)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_labels_antigravity_models_by_model_id() {
|
|
let upstream_metadata = json!({
|
|
"antigravity": {
|
|
"updated_at": 1_775_553_285u64,
|
|
"quota_by_model": {
|
|
"gemini-3.5-flash-extra-low": {
|
|
"display_name": "Gemini 3.5 Flash (Low)",
|
|
"remaining_fraction": 1.0
|
|
},
|
|
"gemini-3.5-flash-low": {
|
|
"display_name": "Gemini 3.5 Flash (Medium)",
|
|
"remaining_fraction": 0.75
|
|
},
|
|
"gemini-3-flash-agent": {
|
|
"display_name": "Gemini 3.5 Flash (High)",
|
|
"remaining_fraction": 0.5
|
|
},
|
|
"gemini-2.5-flash": {
|
|
"display_name": "Gemini 3.1 Flash Lite",
|
|
"remaining_fraction": 0.4
|
|
},
|
|
"claude-sonnet-4-6": {
|
|
"display_name": "Claude Sonnet 4.6 (Thinking)",
|
|
"remaining_fraction": 0.3,
|
|
"reset_time": "2026-04-07T12:34:56Z"
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
None,
|
|
"antigravity",
|
|
Some(&upstream_metadata),
|
|
"refresh_api",
|
|
)
|
|
.expect("quota snapshot should sync");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
let label_for_model = |model: &str| {
|
|
windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("model") == Some(&json!(model)))
|
|
.and_then(|window| window.get("label"))
|
|
.cloned()
|
|
};
|
|
|
|
assert_eq!(
|
|
label_for_model("gemini-3.5-flash-extra-low"),
|
|
Some(json!("Gemini 3.5 Flash (Low)"))
|
|
);
|
|
assert_eq!(
|
|
label_for_model("gemini-3.5-flash-low"),
|
|
Some(json!("Gemini 3.5 Flash (Medium)"))
|
|
);
|
|
assert_eq!(
|
|
label_for_model("gemini-3-flash-agent"),
|
|
Some(json!("Gemini 3.5 Flash (High)"))
|
|
);
|
|
assert_eq!(
|
|
label_for_model("gemini-2.5-flash"),
|
|
Some(json!("Gemini 3.1 Flash Lite"))
|
|
);
|
|
assert_eq!(
|
|
label_for_model("claude-sonnet-4-6"),
|
|
Some(json!("Claude Sonnet 4.6 (Thinking)"))
|
|
);
|
|
let claude_window = windows
|
|
.iter()
|
|
.filter_map(Value::as_object)
|
|
.find(|window| window.get("model") == Some(&json!("claude-sonnet-4-6")))
|
|
.expect("Claude quota window should exist");
|
|
assert_eq!(
|
|
claude_window.get("reset_at"),
|
|
Some(&json!(1_775_565_296u64))
|
|
);
|
|
assert_eq!(claude_window.get("reset_seconds"), Some(&json!(12_011u64)));
|
|
}
|
|
|
|
#[test]
|
|
fn sync_provider_key_quota_status_snapshot_materializes_antigravity_group_windows() {
|
|
let upstream_metadata = json!({
|
|
"antigravity": {
|
|
"updated_at": 1_777_000_000u64,
|
|
"models": {
|
|
"gemini-3.7-flash-tiered": {
|
|
"remaining_fraction": 0.9
|
|
}
|
|
},
|
|
"quota_groups": [{
|
|
"display_name": "Claude and GPT models",
|
|
"description": "Shared quota",
|
|
"buckets": [{
|
|
"bucket_id": "3p-5h",
|
|
"window": "5h",
|
|
"remaining_fraction": 0.25,
|
|
"reset_time": "2026-05-05T05:00:00Z",
|
|
"display_name": "5 hour"
|
|
}, {
|
|
"bucket_id": "3p-weekly",
|
|
"window": "weekly",
|
|
"remaining_fraction": 0.8,
|
|
"reset_time": "2026-05-11T00:00:00Z"
|
|
}]
|
|
}]
|
|
}
|
|
});
|
|
|
|
let payload = sync_provider_key_quota_status_snapshot(
|
|
None,
|
|
"antigravity",
|
|
Some(&upstream_metadata),
|
|
"refresh_api",
|
|
)
|
|
.expect("Antigravity quota snapshot should sync");
|
|
let windows = payload["quota"]["windows"]
|
|
.as_array()
|
|
.expect("quota windows should exist");
|
|
let five_hour = windows
|
|
.iter()
|
|
.find(|window| window["code"] == "group:0:3p-5h")
|
|
.expect("5h grouped quota window should exist");
|
|
let weekly = windows
|
|
.iter()
|
|
.find(|window| window["code"] == "group:0:3p-weekly")
|
|
.expect("weekly grouped quota window should exist");
|
|
|
|
assert_eq!(windows.len(), 3);
|
|
assert_eq!(five_hour["scope"], json!("quota_group"));
|
|
assert_eq!(five_hour["bucket_id"], json!("3p-5h"));
|
|
assert_eq!(five_hour["label"], json!("Claude and GPT models · 5 hour"));
|
|
assert_eq!(
|
|
five_hour["quota_group_label"],
|
|
json!("Claude and GPT models")
|
|
);
|
|
assert_eq!(five_hour["remaining_ratio"], json!(0.25));
|
|
assert_eq!(five_hour["used_ratio"], json!(0.75));
|
|
assert!(five_hour.get("model").is_none());
|
|
assert_eq!(weekly["window"], json!("weekly"));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_account_block_from_oauth_invalid_reason() {
|
|
let mut key = sample_catalog_key();
|
|
key.oauth_invalid_reason = Some("[ACCOUNT_BLOCK] account has been deactivated".to_string());
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
|
let account = payload
|
|
.get("account")
|
|
.and_then(Value::as_object)
|
|
.expect("account snapshot should be object");
|
|
|
|
assert_eq!(account.get("code"), Some(&json!("account_disabled")));
|
|
assert_eq!(account.get("label"), Some(&json!("账号停用")));
|
|
assert_eq!(
|
|
account.get("reason"),
|
|
Some(&json!("account has been deactivated"))
|
|
);
|
|
assert_eq!(account.get("blocked"), Some(&json!(true)));
|
|
assert_eq!(account.get("source"), Some(&json!("oauth_invalid")));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_upgrades_deleted_agent_runtime_to_invalid() {
|
|
let mut key = sample_catalog_key();
|
|
key.auth_type = "oauth".to_string();
|
|
key.oauth_invalid_at_unix_secs = Some(1_784_728_663);
|
|
key.oauth_invalid_reason =
|
|
Some("[REQUEST_FAILED] Agent runtime has been deleted.".to_string());
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
|
let oauth = payload
|
|
.get("oauth")
|
|
.and_then(Value::as_object)
|
|
.expect("oauth snapshot should be object");
|
|
|
|
assert_eq!(oauth.get("code"), Some(&json!("invalid")));
|
|
assert_eq!(oauth.get("label"), Some(&json!("已失效")));
|
|
assert_eq!(oauth.get("invalid_at"), Some(&json!(1_784_728_663u64)));
|
|
assert_eq!(oauth.get("requires_reauth"), Some(&json!(true)));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_key_status_snapshot_payload_backfills_workspace_deactivated_from_metadata() {
|
|
let mut key = sample_catalog_key();
|
|
key.upstream_metadata = Some(json!({
|
|
"codex": {
|
|
"account_disabled": true,
|
|
"reason": "deactivated_workspace"
|
|
}
|
|
}));
|
|
|
|
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
|
let account = payload
|
|
.get("account")
|
|
.and_then(Value::as_object)
|
|
.expect("account snapshot should be object");
|
|
|
|
assert_eq!(account.get("code"), Some(&json!("workspace_deactivated")));
|
|
assert_eq!(account.get("label"), Some(&json!("工作区停用")));
|
|
assert_eq!(account.get("blocked"), Some(&json!(true)));
|
|
assert_eq!(account.get("source"), Some(&json!("metadata")));
|
|
}
|
|
}
|