mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
fix(gateway): expire provider key circuit cooldowns
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::provider_key_auth::provider_key_effective_api_formats;
|
||||
use aether_scheduler_core::count_recent_rpm_requests_for_provider_key_since;
|
||||
use aether_scheduler_core::{
|
||||
count_recent_rpm_requests_for_provider_key_since,
|
||||
provider_key_circuit_payload_is_active_open_at,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -18,6 +21,10 @@ pub(crate) async fn build_admin_key_health_payload(
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|mut keys| keys.drain(..).next())?;
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or_default();
|
||||
let provider = state
|
||||
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&key.provider_id))
|
||||
.await
|
||||
@@ -81,10 +88,10 @@ pub(crate) async fn build_admin_key_health_payload(
|
||||
.and_then(|value| value.get("last_failure_at"))
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
payload["circuit_breaker_open"] = json!(circuit_data
|
||||
.and_then(|value| value.get("open"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false));
|
||||
payload["circuit_breaker_open"] =
|
||||
json!(circuit_data.is_some_and(
|
||||
|value| provider_key_circuit_payload_is_active_open_at(value, now_unix_secs)
|
||||
));
|
||||
payload["circuit_breaker_open_at"] = circuit_data
|
||||
.and_then(|value| value.get("open_at"))
|
||||
.cloned()
|
||||
@@ -168,11 +175,9 @@ pub(crate) async fn build_admin_key_health_payload(
|
||||
.reduce(f64::min)
|
||||
.unwrap_or(1.0);
|
||||
let any_circuit_open = formats_payload.values().any(|value| {
|
||||
value
|
||||
.get("circuit_breaker")
|
||||
.and_then(|circuit| circuit.get("open"))
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
value.get("circuit_breaker").is_some_and(|circuit| {
|
||||
provider_key_circuit_payload_is_active_open_at(circuit, now_unix_secs)
|
||||
})
|
||||
});
|
||||
|
||||
payload["key_health_score"] = json!(key_health_score);
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::handlers::public::{api_format_display_name, build_public_health_timel
|
||||
use crate::handlers::shared::unix_ms_to_rfc3339;
|
||||
use crate::provider_key_auth::provider_key_effective_api_formats;
|
||||
use aether_data_contracts::repository::candidates::PublicHealthTimelineBucket;
|
||||
use aether_scheduler_core::{is_provider_key_circuit_open, provider_key_health_score};
|
||||
use aether_scheduler_core::{
|
||||
any_provider_key_circuit_open_at, is_provider_key_circuit_open_at, provider_key_health_score,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -99,7 +101,9 @@ pub(crate) async fn build_admin_endpoint_health_status_payload(
|
||||
.entry(api_format.clone())
|
||||
.or_default()
|
||||
.insert(key.id.clone());
|
||||
if key.is_active && !is_provider_key_circuit_open(&key, &api_format) {
|
||||
if key.is_active
|
||||
&& !is_provider_key_circuit_open_at(&key, &api_format, now_unix_secs)
|
||||
{
|
||||
let key_health_score =
|
||||
provider_key_health_score(&key, &api_format).unwrap_or(1.0);
|
||||
active_keys_by_format
|
||||
@@ -229,6 +233,10 @@ pub(crate) async fn build_admin_health_summary_payload(
|
||||
return None;
|
||||
}
|
||||
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or_default();
|
||||
let providers = state
|
||||
.list_provider_catalog_providers(false)
|
||||
.await
|
||||
@@ -286,20 +294,7 @@ pub(crate) async fn build_admin_health_summary_payload(
|
||||
.count();
|
||||
let circuit_open_keys = keys
|
||||
.iter()
|
||||
.filter(|key| {
|
||||
key.circuit_breaker_by_format
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.map(|formats| {
|
||||
formats.values().any(|circuit| {
|
||||
circuit
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.filter(|key| any_provider_key_circuit_open_at(key, now_unix_secs))
|
||||
.count();
|
||||
|
||||
Some(json!({
|
||||
|
||||
@@ -8,10 +8,12 @@ use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use aether_scheduler_core::{
|
||||
is_provider_key_circuit_open, matches_model_mapping, provider_key_health_score,
|
||||
is_provider_key_circuit_open_at, matches_model_mapping,
|
||||
provider_key_circuit_payload_is_active_open_at, provider_key_health_score,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) async fn build_admin_global_model_routing_payload(
|
||||
@@ -86,6 +88,10 @@ pub(crate) async fn build_admin_global_model_routing_payload(
|
||||
.flatten()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
let now_unix_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_secs())
|
||||
.unwrap_or_default();
|
||||
|
||||
let global_model_mappings = global_model
|
||||
.config
|
||||
@@ -163,9 +169,11 @@ pub(crate) async fn build_admin_global_model_routing_payload(
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|(api_format, value)| {
|
||||
value.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.filter(|is_open| *is_open)
|
||||
provider_key_circuit_payload_is_active_open_at(
|
||||
value,
|
||||
now_unix_secs,
|
||||
)
|
||||
.then_some(())
|
||||
.map(|_| api_format.clone())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -188,7 +196,7 @@ pub(crate) async fn build_admin_global_model_routing_payload(
|
||||
"effective_rpm": effective_rpm,
|
||||
"allowed_models": allowed_models,
|
||||
"health_score": provider_key_health_score(key, &endpoint.api_format),
|
||||
"circuit_breaker_open": is_provider_key_circuit_open(key, &endpoint.api_format),
|
||||
"circuit_breaker_open": is_provider_key_circuit_open_at(key, &endpoint.api_format, now_unix_secs),
|
||||
"circuit_breaker_formats": circuit_breaker_formats,
|
||||
"next_probe_at": next_probe_at,
|
||||
});
|
||||
|
||||
+8
-7
@@ -1,6 +1,6 @@
|
||||
use super::super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{provider_key_health_summary, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::shared::{provider_key_health_summary_at, unix_secs_to_rfc3339};
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::{
|
||||
provider_catalog::StoredProviderCatalogKey, usage::UsageMonitoringErrorListQuery,
|
||||
@@ -99,7 +99,7 @@ pub(super) async fn build_admin_monitoring_resilience_snapshot(
|
||||
last_failure_at,
|
||||
circuit_breaker_open,
|
||||
circuit_by_format,
|
||||
) = provider_key_health_summary(key);
|
||||
) = provider_key_health_summary_at(key, now.timestamp().max(0) as u64);
|
||||
if health_score < 0.8 {
|
||||
degraded_keys += 1;
|
||||
}
|
||||
@@ -110,11 +110,12 @@ pub(super) async fn build_admin_monitoring_resilience_snapshot(
|
||||
let open_formats = circuit_by_format
|
||||
.iter()
|
||||
.filter_map(|(api_format, value)| {
|
||||
value
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.filter(|open| *open)
|
||||
.map(|_| api_format.clone())
|
||||
aether_scheduler_core::provider_key_circuit_payload_is_active_open_at(
|
||||
value,
|
||||
now.timestamp().max(0) as u64,
|
||||
)
|
||||
.then_some(())
|
||||
.map(|_| api_format.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -1237,7 +1237,8 @@ async fn admin_monitoring_circuit_history_returns_local_payload() {
|
||||
"openai:chat": {
|
||||
"open": true,
|
||||
"open_at": "2026-03-30T12:00:00+00:00",
|
||||
"next_probe_at": "2026-03-30T12:05:00+00:00",
|
||||
"next_probe_at": "2099-03-30T12:05:00+00:00",
|
||||
"recovery_seconds": 300,
|
||||
"reason": "错误率过高"
|
||||
}
|
||||
})),
|
||||
|
||||
@@ -13,6 +13,7 @@ use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::StoredProviderApiKeyWindowUsageSummary;
|
||||
use aether_scheduler_core::provider_key_circuit_payload_is_active_open_at;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -921,19 +922,14 @@ fn admin_pool_health_score(key: &StoredProviderCatalogKey) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_pool_circuit_breaker_open(key: &StoredProviderCatalogKey) -> bool {
|
||||
fn admin_pool_circuit_breaker_open(key: &StoredProviderCatalogKey, now_unix_secs: u64) -> bool {
|
||||
key.circuit_breaker_by_format
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.map(|formats| {
|
||||
formats
|
||||
.values()
|
||||
.filter_map(serde_json::Value::as_object)
|
||||
.any(|item| {
|
||||
item.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.any(|item| provider_key_circuit_payload_is_active_open_at(item, now_unix_secs))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -1032,7 +1028,7 @@ pub(super) fn build_admin_pool_key_payload(
|
||||
.as_ref()
|
||||
.and_then(|_| runtime.cooldown_ttl_by_key.get(&key.id).copied());
|
||||
let health_score = admin_pool_health_score(key);
|
||||
let circuit_breaker_open = admin_pool_circuit_breaker_open(key);
|
||||
let circuit_breaker_open = admin_pool_circuit_breaker_open(key, now_unix_secs);
|
||||
let auth_semantics = provider_key_auth_semantics(key, provider_type);
|
||||
let account_quota_exhausted = pool_config
|
||||
.as_ref()
|
||||
|
||||
@@ -68,6 +68,7 @@ use aether_model_fetch::{
|
||||
aggregate_models_for_cache, fetch_models_from_transports, json_string_list,
|
||||
preset_models_for_provider, selected_models_fetch_endpoints,
|
||||
};
|
||||
use aether_scheduler_core::provider_key_circuit_payload_is_active_open_at;
|
||||
use axum::{
|
||||
body::{to_bytes, Body},
|
||||
http::{self, HeaderMap, HeaderName, HeaderValue},
|
||||
@@ -920,6 +921,7 @@ fn provider_query_test_key_sort_key(
|
||||
provider_type: &str,
|
||||
key: &StoredProviderCatalogKey,
|
||||
endpoint_api_format: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> (u8, u8, i32, u64, i32) {
|
||||
let quota_exhausted =
|
||||
admin_provider_pool_pure::admin_pool_key_account_quota_exhausted(key, provider_type);
|
||||
@@ -928,10 +930,7 @@ fn provider_query_test_key_sort_key(
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|value| value.get(endpoint_api_format))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|value| value.get("open"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|value| provider_key_circuit_payload_is_active_open_at(value, now_unix_secs));
|
||||
let health_score = key
|
||||
.health_by_format
|
||||
.as_ref()
|
||||
@@ -1390,6 +1389,7 @@ async fn provider_query_build_kiro_test_candidates(
|
||||
provider_query_key_supports_endpoint(key, &provider.provider_type, &endpoint.api_format)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let now_unix_secs = current_unix_ms() / 1000;
|
||||
|
||||
let candidates = if test_mode.eq_ignore_ascii_case("pool") {
|
||||
if let Some(pool_config) =
|
||||
@@ -1411,6 +1411,7 @@ async fn provider_query_build_kiro_test_candidates(
|
||||
provider.provider_type.as_str(),
|
||||
key,
|
||||
&endpoint.api_format,
|
||||
now_unix_secs,
|
||||
)
|
||||
});
|
||||
keys.into_iter()
|
||||
@@ -1428,6 +1429,7 @@ async fn provider_query_build_kiro_test_candidates(
|
||||
provider.provider_type.as_str(),
|
||||
key,
|
||||
&endpoint.api_format,
|
||||
now_unix_secs,
|
||||
)
|
||||
});
|
||||
keys.into_iter()
|
||||
|
||||
@@ -13,7 +13,8 @@ pub(crate) use crate::handlers::shared::{
|
||||
effective_catalog_encryption_key, encrypt_catalog_secret_with_fallbacks, json_string_list,
|
||||
masked_catalog_api_key, normalize_json_array, normalize_json_object, normalize_string_list,
|
||||
parse_catalog_auth_config_json, provider_catalog_key_supports_format,
|
||||
provider_key_health_summary, provider_key_status_snapshot_payload, query_param_bool,
|
||||
query_param_optional_bool, query_param_value, take_secret_prefix, take_secret_suffix,
|
||||
unix_secs_to_rfc3339, OFFICIAL_EXTERNAL_MODEL_PROVIDERS,
|
||||
provider_key_health_summary, provider_key_health_summary_at,
|
||||
provider_key_status_snapshot_payload, query_param_bool, query_param_optional_bool,
|
||||
query_param_value, take_secret_prefix, take_secret_suffix, unix_secs_to_rfc3339,
|
||||
OFFICIAL_EXTERNAL_MODEL_PROVIDERS,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::enabled_key_capability_short_names;
|
||||
use crate::handlers::shared::unix_secs_to_rfc3339;
|
||||
use crate::provider_key_auth::provider_key_effective_api_formats;
|
||||
use crate::AppState;
|
||||
use aether_scheduler_core::provider_key_circuit_payload_is_active_open_at;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -173,10 +174,10 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
.get("health_score")
|
||||
.and_then(serde_json::Value::as_f64)
|
||||
.unwrap_or(1.0),
|
||||
"circuit_breaker_open": format_circuit
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false),
|
||||
"circuit_breaker_open": provider_key_circuit_payload_is_active_open_at(
|
||||
&format_circuit,
|
||||
now_unix_secs,
|
||||
),
|
||||
"last_used_at": key.last_used_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"created_at": unix_secs_to_rfc3339(key.created_at_unix_ms.unwrap_or(now_unix_secs)),
|
||||
"updated_at": unix_secs_to_rfc3339(key.updated_at_unix_secs.unwrap_or(now_unix_secs)),
|
||||
|
||||
@@ -13,6 +13,7 @@ use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKe
|
||||
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};
|
||||
@@ -1778,6 +1779,39 @@ pub(crate) fn provider_key_health_summary(
|
||||
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
|
||||
@@ -1820,12 +1854,7 @@ pub(crate) fn provider_key_health_summary(
|
||||
}
|
||||
}
|
||||
|
||||
let any_circuit_open = circuit_by_format.values().any(|value| {
|
||||
value
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let any_circuit_open = circuit_by_format.values().any(circuit_is_open);
|
||||
|
||||
(
|
||||
if health_by_format.is_empty() {
|
||||
@@ -1974,15 +2003,10 @@ pub(crate) fn build_admin_provider_key_response(
|
||||
last_failure_at,
|
||||
circuit_breaker_open,
|
||||
circuit_by_format,
|
||||
) = provider_key_health_summary(key);
|
||||
) = provider_key_health_summary_at(key, now_unix_secs);
|
||||
let circuit_sample = circuit_by_format
|
||||
.values()
|
||||
.find(|value| {
|
||||
value
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.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 {
|
||||
|
||||
@@ -26,8 +26,9 @@ pub(crate) use self::catalog::{
|
||||
default_provider_key_status_snapshot, effective_catalog_encryption_key,
|
||||
encrypt_catalog_secret_with_fallbacks, masked_catalog_api_key, parse_catalog_auth_config_json,
|
||||
provider_catalog_key_supports_format, provider_key_health_summary,
|
||||
provider_key_status_snapshot_payload, sync_provider_key_oauth_status_snapshot,
|
||||
sync_provider_key_quota_status_snapshot, take_secret_prefix, take_secret_suffix,
|
||||
provider_key_health_summary_at, provider_key_status_snapshot_payload,
|
||||
sync_provider_key_oauth_status_snapshot, sync_provider_key_quota_status_snapshot,
|
||||
take_secret_prefix, take_secret_suffix,
|
||||
};
|
||||
pub(crate) use self::email_templates::{
|
||||
admin_email_template_definition, admin_email_template_html_key,
|
||||
|
||||
Reference in New Issue
Block a user