2026-04-17 23:05:49 +08:00
|
|
|
use crate::handlers::admin::provider::shared::support::{
|
|
|
|
|
AdminProviderPoolConfig, AdminProviderPoolSchedulingPreset, AdminProviderPoolUnschedulableRule,
|
|
|
|
|
};
|
2026-05-12 09:08:10 +08:00
|
|
|
use aether_ai_serving::{PoolMemberScoreRules, PoolMemberScoreWeights};
|
2026-04-07 12:15:19 +08:00
|
|
|
use serde_json::{Map, Value};
|
|
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
const POOL_ALLOWED_SCHEDULING_PRESETS: &[&str] = &[
|
|
|
|
|
"lru",
|
|
|
|
|
"cache_affinity",
|
|
|
|
|
"load_balance",
|
|
|
|
|
"single_account",
|
|
|
|
|
"priority_first",
|
|
|
|
|
"free_first",
|
|
|
|
|
"team_first",
|
|
|
|
|
"plus_first",
|
2026-04-24 13:29:05 +08:00
|
|
|
"pro_first",
|
2026-04-24 10:05:55 +08:00
|
|
|
"health_first",
|
|
|
|
|
"latency_first",
|
|
|
|
|
"cost_first",
|
|
|
|
|
"quota_balanced",
|
|
|
|
|
"recent_refresh",
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-07 12:15:19 +08:00
|
|
|
fn json_u64(value: &Value) -> Option<u64> {
|
|
|
|
|
value
|
|
|
|
|
.as_u64()
|
|
|
|
|
.or_else(|| value.as_i64().and_then(|raw| u64::try_from(raw).ok()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 09:08:10 +08:00
|
|
|
fn json_f64(value: &Value) -> Option<f64> {
|
|
|
|
|
value.as_f64().or_else(|| {
|
|
|
|
|
value
|
|
|
|
|
.as_str()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
.and_then(|value| value.parse::<f64>().ok())
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn pool_score_weight(object: &Map<String, Value>, names: &[&str], current: f64) -> f64 {
|
|
|
|
|
names
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|name| {
|
|
|
|
|
object
|
|
|
|
|
.get(*name)
|
|
|
|
|
.and_then(json_f64)
|
|
|
|
|
.filter(|value| value.is_finite() && *value >= 0.0)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_pool_score_weights(
|
|
|
|
|
raw_weights: Option<&Map<String, Value>>,
|
|
|
|
|
current: PoolMemberScoreWeights,
|
|
|
|
|
) -> PoolMemberScoreWeights {
|
|
|
|
|
let Some(raw_weights) = raw_weights else {
|
|
|
|
|
return current;
|
|
|
|
|
};
|
|
|
|
|
PoolMemberScoreWeights {
|
|
|
|
|
manual_priority: pool_score_weight(
|
|
|
|
|
raw_weights,
|
|
|
|
|
&["manual_priority", "priority", "internal_priority"],
|
|
|
|
|
current.manual_priority,
|
|
|
|
|
),
|
|
|
|
|
health: pool_score_weight(raw_weights, &["health"], current.health),
|
|
|
|
|
probe_freshness: pool_score_weight(
|
|
|
|
|
raw_weights,
|
|
|
|
|
&["probe_freshness", "freshness", "probe"],
|
|
|
|
|
current.probe_freshness,
|
|
|
|
|
),
|
|
|
|
|
quota_remaining: pool_score_weight(
|
|
|
|
|
raw_weights,
|
|
|
|
|
&["quota_remaining", "quota", "quota_available"],
|
|
|
|
|
current.quota_remaining,
|
|
|
|
|
),
|
|
|
|
|
latency: pool_score_weight(raw_weights, &["latency"], current.latency),
|
|
|
|
|
cost_lru: pool_score_weight(
|
|
|
|
|
raw_weights,
|
|
|
|
|
&["cost_lru", "cost_remaining", "cost", "lru"],
|
|
|
|
|
current.cost_lru,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_pool_score_rules(pool_advanced: &Map<String, Value>) -> PoolMemberScoreRules {
|
|
|
|
|
let mut rules = PoolMemberScoreRules::default();
|
|
|
|
|
|
|
|
|
|
for key in ["score_weights", "pool_score_weights", "scoring_weights"] {
|
|
|
|
|
rules.weights = parse_pool_score_weights(
|
|
|
|
|
pool_advanced.get(key).and_then(Value::as_object),
|
|
|
|
|
rules.weights,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(score_rules) = pool_advanced
|
|
|
|
|
.get("score_rules")
|
|
|
|
|
.or_else(|| pool_advanced.get("pool_score_rules"))
|
|
|
|
|
.and_then(Value::as_object)
|
|
|
|
|
{
|
|
|
|
|
rules.weights = parse_pool_score_weights(
|
|
|
|
|
score_rules.get("weights").and_then(Value::as_object),
|
|
|
|
|
rules.weights,
|
|
|
|
|
);
|
|
|
|
|
if let Some(ttl_seconds) = score_rules
|
|
|
|
|
.get("probe_freshness_ttl_seconds")
|
|
|
|
|
.or_else(|| score_rules.get("score_probe_freshness_ttl_seconds"))
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
{
|
|
|
|
|
rules.probe_freshness_ttl_seconds = ttl_seconds.min(7 * 24 * 3600);
|
|
|
|
|
}
|
|
|
|
|
if let Some(cap) = score_rules
|
|
|
|
|
.get("unschedulable_score_cap")
|
|
|
|
|
.or_else(|| score_rules.get("hard_state_score_cap"))
|
|
|
|
|
.and_then(json_f64)
|
|
|
|
|
.filter(|value| value.is_finite())
|
|
|
|
|
{
|
|
|
|
|
rules.unschedulable_score_cap = cap.clamp(0.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
if let Some(penalty) = score_rules
|
|
|
|
|
.get("probe_failure_penalty")
|
|
|
|
|
.and_then(json_f64)
|
|
|
|
|
.filter(|value| value.is_finite())
|
|
|
|
|
{
|
|
|
|
|
rules.probe_failure_penalty = penalty.clamp(0.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
if let Some(penalty) = score_rules
|
|
|
|
|
.get("request_failure_penalty")
|
|
|
|
|
.or_else(|| score_rules.get("runtime_failure_penalty"))
|
|
|
|
|
.and_then(json_f64)
|
|
|
|
|
.filter(|value| value.is_finite())
|
|
|
|
|
{
|
|
|
|
|
rules.request_failure_penalty = penalty.clamp(0.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
if let Some(threshold) = score_rules
|
|
|
|
|
.get("probe_failure_cooldown_threshold")
|
|
|
|
|
.or_else(|| score_rules.get("probe_failure_hard_state_threshold"))
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
{
|
|
|
|
|
rules.probe_failure_cooldown_threshold = threshold.min(100);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rules.effective()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
fn normalize_pool_preset_mode(preset: &str, raw_mode: Option<&Value>) -> Option<String> {
|
|
|
|
|
match preset {
|
2026-04-24 13:29:05 +08:00
|
|
|
"free_first" | "team_first" | "plus_first" | "pro_first" => {
|
2026-04-24 10:05:55 +08:00
|
|
|
let default_mode = match preset {
|
|
|
|
|
"free_first" => "free_only",
|
|
|
|
|
"team_first" => "team_only",
|
|
|
|
|
"plus_first" => "plus_only",
|
2026-04-24 13:29:05 +08:00
|
|
|
"pro_first" => "pro_only",
|
2026-04-24 10:05:55 +08:00
|
|
|
_ => unreachable!("preset covered by outer match"),
|
|
|
|
|
};
|
|
|
|
|
let normalized = raw_mode
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
.map(|value| value.to_ascii_lowercase())
|
|
|
|
|
.filter(|value| match preset {
|
|
|
|
|
"free_first" => value == "free_only",
|
|
|
|
|
"team_first" => value == "team_only",
|
|
|
|
|
"plus_first" => value == "plus_only",
|
2026-04-24 13:29:05 +08:00
|
|
|
"pro_first" => value == "pro_only",
|
2026-04-24 10:05:55 +08:00
|
|
|
_ => false,
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| default_mode.to_string());
|
|
|
|
|
Some(normalized)
|
2026-04-17 23:05:49 +08:00
|
|
|
}
|
2026-04-24 10:05:55 +08:00
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-17 23:05:49 +08:00
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
fn parse_object_style_pool_scheduling_presets(
|
|
|
|
|
presets: &[Value],
|
|
|
|
|
) -> Vec<AdminProviderPoolSchedulingPreset> {
|
|
|
|
|
let mut normalized = Vec::new();
|
|
|
|
|
let mut seen = std::collections::BTreeSet::new();
|
|
|
|
|
|
|
|
|
|
for item in presets {
|
2026-04-17 23:05:49 +08:00
|
|
|
let Some(object) = item.as_object() else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let Some(preset) = object
|
|
|
|
|
.get("preset")
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())
|
2026-04-24 10:05:55 +08:00
|
|
|
.map(|value| value.to_ascii_lowercase())
|
2026-04-17 23:05:49 +08:00
|
|
|
else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
2026-04-24 10:05:55 +08:00
|
|
|
if !POOL_ALLOWED_SCHEDULING_PRESETS.contains(&preset.as_str())
|
|
|
|
|
|| !seen.insert(preset.clone())
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-17 23:05:49 +08:00
|
|
|
normalized.push(AdminProviderPoolSchedulingPreset {
|
2026-04-24 10:05:55 +08:00
|
|
|
mode: normalize_pool_preset_mode(&preset, object.get("mode")),
|
|
|
|
|
preset,
|
2026-04-17 23:05:49 +08:00
|
|
|
enabled: object
|
|
|
|
|
.get("enabled")
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
.unwrap_or(true),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
if normalized.is_empty() {
|
|
|
|
|
vec![AdminProviderPoolSchedulingPreset {
|
2026-04-17 23:05:49 +08:00
|
|
|
preset: "lru".to_string(),
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: None,
|
2026-04-24 10:05:55 +08:00
|
|
|
}]
|
|
|
|
|
} else {
|
|
|
|
|
normalized
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_legacy_string_style_pool_scheduling_presets(
|
|
|
|
|
raw_pool_advanced: &Map<String, Value>,
|
|
|
|
|
presets: &[Value],
|
|
|
|
|
) -> Vec<AdminProviderPoolSchedulingPreset> {
|
|
|
|
|
let lru_enabled = raw_pool_advanced
|
|
|
|
|
.get("lru_enabled")
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
let mut normalized = vec![AdminProviderPoolSchedulingPreset {
|
|
|
|
|
preset: "lru".to_string(),
|
|
|
|
|
enabled: lru_enabled,
|
|
|
|
|
mode: None,
|
|
|
|
|
}];
|
|
|
|
|
let mut seen = std::collections::BTreeSet::from(["lru".to_string()]);
|
|
|
|
|
|
|
|
|
|
for item in presets {
|
|
|
|
|
let Some(preset) = item
|
|
|
|
|
.as_str()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())
|
|
|
|
|
.map(|value| value.to_ascii_lowercase())
|
|
|
|
|
else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
if preset == "lru"
|
|
|
|
|
|| !POOL_ALLOWED_SCHEDULING_PRESETS.contains(&preset.as_str())
|
|
|
|
|
|| !seen.insert(preset.clone())
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
normalized.push(AdminProviderPoolSchedulingPreset {
|
|
|
|
|
preset,
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: None,
|
2026-04-17 23:05:49 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
normalized
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
fn parse_pool_scheduling_presets_from_legacy_fields(
|
|
|
|
|
raw_pool_advanced: &Map<String, Value>,
|
|
|
|
|
) -> Vec<AdminProviderPoolSchedulingPreset> {
|
|
|
|
|
if raw_pool_advanced
|
|
|
|
|
.get("lru_enabled")
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
{
|
|
|
|
|
vec![AdminProviderPoolSchedulingPreset {
|
|
|
|
|
preset: "lru".to_string(),
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: None,
|
|
|
|
|
}]
|
|
|
|
|
} else {
|
|
|
|
|
vec![AdminProviderPoolSchedulingPreset {
|
|
|
|
|
preset: "cache_affinity".to_string(),
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: None,
|
|
|
|
|
}]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_pool_scheduling_presets(
|
|
|
|
|
raw_pool_advanced: &Map<String, Value>,
|
|
|
|
|
) -> Vec<AdminProviderPoolSchedulingPreset> {
|
|
|
|
|
match raw_pool_advanced
|
|
|
|
|
.get("scheduling_presets")
|
|
|
|
|
.and_then(Value::as_array)
|
|
|
|
|
{
|
|
|
|
|
Some(presets) if !presets.is_empty() => match presets.first() {
|
|
|
|
|
Some(Value::Object(_)) => parse_object_style_pool_scheduling_presets(presets),
|
|
|
|
|
Some(Value::String(_)) => {
|
|
|
|
|
parse_legacy_string_style_pool_scheduling_presets(raw_pool_advanced, presets)
|
|
|
|
|
}
|
|
|
|
|
_ => parse_pool_scheduling_presets_from_legacy_fields(raw_pool_advanced),
|
|
|
|
|
},
|
|
|
|
|
_ => parse_pool_scheduling_presets_from_legacy_fields(raw_pool_advanced),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:05:49 +08:00
|
|
|
fn parse_pool_unschedulable_rules(
|
|
|
|
|
raw_pool_advanced: &Map<String, Value>,
|
|
|
|
|
) -> Vec<AdminProviderPoolUnschedulableRule> {
|
|
|
|
|
raw_pool_advanced
|
|
|
|
|
.get("unschedulable_rules")
|
|
|
|
|
.and_then(Value::as_array)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flatten()
|
|
|
|
|
.filter_map(|item| {
|
|
|
|
|
let object = item.as_object()?;
|
|
|
|
|
let keyword = object
|
|
|
|
|
.get("keyword")
|
|
|
|
|
.and_then(Value::as_str)
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty())?;
|
|
|
|
|
Some(AdminProviderPoolUnschedulableRule {
|
|
|
|
|
keyword: keyword.to_string(),
|
|
|
|
|
duration_minutes: object
|
|
|
|
|
.get("duration_minutes")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(5),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn admin_provider_pool_lru_enabled(
|
|
|
|
|
scheduling_presets: &[AdminProviderPoolSchedulingPreset],
|
|
|
|
|
) -> bool {
|
|
|
|
|
scheduling_presets
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|item| item.enabled && item.preset.eq_ignore_ascii_case("lru"))
|
2026-04-07 12:15:19 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 14:06:49 +08:00
|
|
|
pub(crate) fn admin_provider_pool_cache_affinity_enabled(
|
|
|
|
|
pool_config: &AdminProviderPoolConfig,
|
|
|
|
|
) -> bool {
|
|
|
|
|
let mut seen = std::collections::BTreeSet::new();
|
|
|
|
|
for item in &pool_config.scheduling_presets {
|
|
|
|
|
let preset = item.preset.trim().to_ascii_lowercase();
|
|
|
|
|
if preset.is_empty() || !seen.insert(preset.clone()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if !item.enabled {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if matches!(
|
|
|
|
|
preset.as_str(),
|
|
|
|
|
"lru" | "cache_affinity" | "load_balance" | "single_account"
|
|
|
|
|
) {
|
|
|
|
|
return preset == "cache_affinity";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 12:15:19 +08:00
|
|
|
pub(crate) fn admin_provider_pool_config(
|
|
|
|
|
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
|
|
|
|
|
) -> Option<AdminProviderPoolConfig> {
|
2026-04-17 23:05:49 +08:00
|
|
|
admin_provider_pool_config_from_config_value(provider.config.as_ref())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(crate) fn admin_provider_pool_config_from_config_value(
|
|
|
|
|
config: Option<&serde_json::Value>,
|
|
|
|
|
) -> Option<AdminProviderPoolConfig> {
|
|
|
|
|
let raw_pool_advanced = config
|
2026-04-07 12:15:19 +08:00
|
|
|
.and_then(Value::as_object)
|
|
|
|
|
.and_then(|config| config.get("pool_advanced"))?;
|
|
|
|
|
|
|
|
|
|
let Some(pool_advanced) = raw_pool_advanced.as_object() else {
|
|
|
|
|
return Some(AdminProviderPoolConfig {
|
2026-04-24 10:05:55 +08:00
|
|
|
scheduling_presets: vec![AdminProviderPoolSchedulingPreset {
|
|
|
|
|
preset: "cache_affinity".to_string(),
|
|
|
|
|
enabled: true,
|
|
|
|
|
mode: None,
|
|
|
|
|
}],
|
2026-04-17 23:05:49 +08:00
|
|
|
unschedulable_rules: Vec::new(),
|
2026-04-07 12:15:19 +08:00
|
|
|
lru_enabled: false,
|
2026-04-17 12:57:06 +08:00
|
|
|
skip_exhausted_accounts: false,
|
2026-04-17 23:05:49 +08:00
|
|
|
sticky_session_ttl_seconds: 3600,
|
|
|
|
|
latency_window_seconds: 3600,
|
|
|
|
|
latency_sample_limit: 50,
|
2026-04-07 12:15:19 +08:00
|
|
|
cost_window_seconds: 18_000,
|
|
|
|
|
cost_limit_per_key_tokens: None,
|
2026-04-17 23:05:49 +08:00
|
|
|
rate_limit_cooldown_seconds: 300,
|
|
|
|
|
overload_cooldown_seconds: 30,
|
|
|
|
|
health_policy_enabled: true,
|
2026-04-26 23:48:31 +08:00
|
|
|
probing_enabled: false,
|
|
|
|
|
probing_interval_minutes: 10,
|
2026-05-12 01:46:22 +08:00
|
|
|
probe_concurrency: 4,
|
|
|
|
|
score_top_n: 128,
|
|
|
|
|
score_fallback_scan_limit: 1024,
|
2026-05-12 09:08:10 +08:00
|
|
|
score_rules: PoolMemberScoreRules::default(),
|
2026-04-17 23:05:49 +08:00
|
|
|
stream_timeout_threshold: 3,
|
|
|
|
|
stream_timeout_window_seconds: 1800,
|
|
|
|
|
stream_timeout_cooldown_seconds: 300,
|
2026-04-07 12:15:19 +08:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-17 23:05:49 +08:00
|
|
|
let scheduling_presets = parse_pool_scheduling_presets(pool_advanced);
|
|
|
|
|
let unschedulable_rules = parse_pool_unschedulable_rules(pool_advanced);
|
2026-05-12 09:08:10 +08:00
|
|
|
let score_rules = parse_pool_score_rules(pool_advanced);
|
2026-04-17 23:05:49 +08:00
|
|
|
|
2026-04-07 12:15:19 +08:00
|
|
|
Some(AdminProviderPoolConfig {
|
2026-04-24 10:05:55 +08:00
|
|
|
lru_enabled: admin_provider_pool_lru_enabled(&scheduling_presets),
|
2026-04-17 23:05:49 +08:00
|
|
|
scheduling_presets,
|
|
|
|
|
unschedulable_rules,
|
2026-04-17 12:57:06 +08:00
|
|
|
skip_exhausted_accounts: pool_advanced
|
|
|
|
|
.get("skip_exhausted_accounts")
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
.unwrap_or(false),
|
2026-04-17 23:05:49 +08:00
|
|
|
sticky_session_ttl_seconds: pool_advanced
|
|
|
|
|
.get("sticky_session_ttl_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.unwrap_or(3600),
|
|
|
|
|
latency_window_seconds: pool_advanced
|
|
|
|
|
.get("latency_window_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(3600),
|
|
|
|
|
latency_sample_limit: pool_advanced
|
|
|
|
|
.get("latency_sample_limit")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(50),
|
2026-04-07 12:15:19 +08:00
|
|
|
cost_window_seconds: pool_advanced
|
|
|
|
|
.get("cost_window_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(18_000),
|
|
|
|
|
cost_limit_per_key_tokens: pool_advanced
|
|
|
|
|
.get("cost_limit_per_key_tokens")
|
|
|
|
|
.and_then(json_u64),
|
2026-04-17 23:05:49 +08:00
|
|
|
rate_limit_cooldown_seconds: pool_advanced
|
|
|
|
|
.get("rate_limit_cooldown_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(300),
|
|
|
|
|
overload_cooldown_seconds: pool_advanced
|
|
|
|
|
.get("overload_cooldown_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(30),
|
|
|
|
|
health_policy_enabled: pool_advanced
|
|
|
|
|
.get("health_policy_enabled")
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
.unwrap_or(true),
|
2026-04-26 23:48:31 +08:00
|
|
|
probing_enabled: pool_advanced
|
|
|
|
|
.get("probing_enabled")
|
|
|
|
|
.and_then(Value::as_bool)
|
|
|
|
|
.unwrap_or(false),
|
|
|
|
|
probing_interval_minutes: pool_advanced
|
|
|
|
|
.get("probing_interval_minutes")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.map(|value| value.min(1440))
|
|
|
|
|
.unwrap_or(10),
|
2026-05-12 01:46:22 +08:00
|
|
|
probe_concurrency: pool_advanced
|
|
|
|
|
.get("probe_concurrency")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.map(|value| value.min(64))
|
|
|
|
|
.unwrap_or(4),
|
|
|
|
|
score_top_n: pool_advanced
|
|
|
|
|
.get("score_top_n")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.map(|value| value.min(4096))
|
|
|
|
|
.unwrap_or(128),
|
|
|
|
|
score_fallback_scan_limit: pool_advanced
|
|
|
|
|
.get("score_fallback_scan_limit")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.map(|value| value.min(50_000))
|
|
|
|
|
.unwrap_or(1024),
|
2026-05-12 09:08:10 +08:00
|
|
|
score_rules,
|
2026-04-17 23:05:49 +08:00
|
|
|
stream_timeout_threshold: pool_advanced
|
|
|
|
|
.get("stream_timeout_threshold")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(3),
|
|
|
|
|
stream_timeout_window_seconds: pool_advanced
|
|
|
|
|
.get("stream_timeout_window_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(1800),
|
|
|
|
|
stream_timeout_cooldown_seconds: pool_advanced
|
|
|
|
|
.get("stream_timeout_cooldown_seconds")
|
|
|
|
|
.and_then(json_u64)
|
|
|
|
|
.filter(|value| *value > 0)
|
|
|
|
|
.unwrap_or(300),
|
2026-04-07 12:15:19 +08:00
|
|
|
})
|
|
|
|
|
}
|
2026-04-17 12:57:06 +08:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-05-11 14:06:49 +08:00
|
|
|
use super::{
|
|
|
|
|
admin_provider_pool_cache_affinity_enabled, admin_provider_pool_config,
|
|
|
|
|
admin_provider_pool_config_from_config_value,
|
|
|
|
|
};
|
2026-04-17 12:57:06 +08:00
|
|
|
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
|
|
|
|
|
fn sample_provider(config: serde_json::Value) -> StoredProviderCatalogProvider {
|
|
|
|
|
StoredProviderCatalogProvider::new(
|
|
|
|
|
"provider-1".to_string(),
|
|
|
|
|
"provider-1".to_string(),
|
|
|
|
|
Some("https://example.com".to_string()),
|
|
|
|
|
"codex".to_string(),
|
|
|
|
|
)
|
|
|
|
|
.expect("provider should build")
|
|
|
|
|
.with_transport_fields(
|
|
|
|
|
true,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
Some(config),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn defaults_skip_exhausted_accounts_to_false() {
|
|
|
|
|
let provider = sample_provider(json!({ "pool_advanced": {} }));
|
|
|
|
|
let config = admin_provider_pool_config(&provider).expect("pool config should exist");
|
|
|
|
|
|
|
|
|
|
assert!(!config.skip_exhausted_accounts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parses_skip_exhausted_accounts_from_pool_advanced() {
|
|
|
|
|
let provider = sample_provider(json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"skip_exhausted_accounts": true,
|
|
|
|
|
"lru_enabled": true,
|
2026-04-17 23:05:49 +08:00
|
|
|
"sticky_session_ttl_seconds": 600,
|
|
|
|
|
"latency_window_seconds": 900,
|
|
|
|
|
"latency_sample_limit": 75,
|
2026-04-17 12:57:06 +08:00
|
|
|
"cost_window_seconds": 7200,
|
2026-04-17 23:05:49 +08:00
|
|
|
"cost_limit_per_key_tokens": 12000,
|
|
|
|
|
"rate_limit_cooldown_seconds": 420,
|
|
|
|
|
"overload_cooldown_seconds": 45,
|
|
|
|
|
"health_policy_enabled": false,
|
2026-04-26 23:48:31 +08:00
|
|
|
"probing_enabled": true,
|
|
|
|
|
"probing_interval_minutes": 20,
|
2026-05-12 01:46:22 +08:00
|
|
|
"probe_concurrency": 6,
|
|
|
|
|
"score_top_n": 256,
|
|
|
|
|
"score_fallback_scan_limit": 2048,
|
2026-05-12 09:08:10 +08:00
|
|
|
"score_rules": {
|
|
|
|
|
"weights": {
|
|
|
|
|
"manual_priority": 0.4,
|
|
|
|
|
"health": 0.2,
|
|
|
|
|
"probe_freshness": 0.2,
|
|
|
|
|
"quota_remaining": 0.1,
|
|
|
|
|
"latency": 0.05,
|
|
|
|
|
"cost_lru": 0.05
|
|
|
|
|
},
|
|
|
|
|
"probe_freshness_ttl_seconds": 1200,
|
|
|
|
|
"unschedulable_score_cap": 0.03,
|
|
|
|
|
"probe_failure_penalty": 0.08,
|
|
|
|
|
"request_failure_penalty": 0.01,
|
|
|
|
|
"probe_failure_cooldown_threshold": 2
|
|
|
|
|
},
|
2026-04-17 23:05:49 +08:00
|
|
|
"stream_timeout_threshold": 4,
|
|
|
|
|
"stream_timeout_window_seconds": 900,
|
|
|
|
|
"stream_timeout_cooldown_seconds": 180
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
let config = admin_provider_pool_config(&provider).expect("pool config should exist");
|
|
|
|
|
|
|
|
|
|
assert!(config.skip_exhausted_accounts);
|
|
|
|
|
assert!(config.lru_enabled);
|
2026-04-17 23:05:49 +08:00
|
|
|
assert_eq!(config.sticky_session_ttl_seconds, 600);
|
|
|
|
|
assert_eq!(config.latency_window_seconds, 900);
|
|
|
|
|
assert_eq!(config.latency_sample_limit, 75);
|
2026-04-17 12:57:06 +08:00
|
|
|
assert_eq!(config.cost_window_seconds, 7200);
|
|
|
|
|
assert_eq!(config.cost_limit_per_key_tokens, Some(12_000));
|
2026-04-17 23:05:49 +08:00
|
|
|
assert_eq!(config.rate_limit_cooldown_seconds, 420);
|
|
|
|
|
assert_eq!(config.overload_cooldown_seconds, 45);
|
|
|
|
|
assert!(!config.health_policy_enabled);
|
2026-04-26 23:48:31 +08:00
|
|
|
assert!(config.probing_enabled);
|
|
|
|
|
assert_eq!(config.probing_interval_minutes, 20);
|
2026-05-12 01:46:22 +08:00
|
|
|
assert_eq!(config.probe_concurrency, 6);
|
|
|
|
|
assert_eq!(config.score_top_n, 256);
|
|
|
|
|
assert_eq!(config.score_fallback_scan_limit, 2048);
|
2026-05-12 09:08:10 +08:00
|
|
|
assert_eq!(config.score_rules.weights.manual_priority, 0.4);
|
|
|
|
|
assert_eq!(config.score_rules.weights.health, 0.2);
|
|
|
|
|
assert_eq!(config.score_rules.probe_freshness_ttl_seconds, 1200);
|
|
|
|
|
assert_eq!(config.score_rules.unschedulable_score_cap, 0.03);
|
|
|
|
|
assert_eq!(config.score_rules.probe_failure_penalty, 0.08);
|
|
|
|
|
assert_eq!(config.score_rules.request_failure_penalty, 0.01);
|
|
|
|
|
assert_eq!(config.score_rules.probe_failure_cooldown_threshold, 2);
|
2026-04-17 23:05:49 +08:00
|
|
|
assert_eq!(config.stream_timeout_threshold, 4);
|
|
|
|
|
assert_eq!(config.stream_timeout_window_seconds, 900);
|
|
|
|
|
assert_eq!(config.stream_timeout_cooldown_seconds, 180);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 23:48:31 +08:00
|
|
|
#[test]
|
|
|
|
|
fn clamps_pool_quota_probe_interval_to_python_range() {
|
|
|
|
|
let provider = sample_provider(json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"probing_enabled": true,
|
|
|
|
|
"probing_interval_minutes": 2000,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
let config = admin_provider_pool_config(&provider).expect("pool config should exist");
|
|
|
|
|
assert_eq!(config.probing_interval_minutes, 1440);
|
|
|
|
|
|
|
|
|
|
let provider = sample_provider(json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"probing_enabled": true,
|
|
|
|
|
"probing_interval_minutes": 0,
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
let config = admin_provider_pool_config(&provider).expect("pool config should exist");
|
|
|
|
|
assert_eq!(config.probing_interval_minutes, 10);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 16:41:51 +08:00
|
|
|
#[test]
|
|
|
|
|
fn parses_zero_sticky_session_ttl_to_disable_sticky_sessions() {
|
|
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"sticky_session_ttl_seconds": 0
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.sticky_session_ttl_seconds, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 09:08:10 +08:00
|
|
|
#[test]
|
|
|
|
|
fn parses_legacy_pool_score_weights_from_pool_advanced() {
|
|
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scoring_weights": {
|
|
|
|
|
"manual_priority": 0,
|
|
|
|
|
"health": 2,
|
|
|
|
|
"probe": 1,
|
|
|
|
|
"quota_remaining": 0,
|
|
|
|
|
"latency": 0,
|
|
|
|
|
"cost_remaining": 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.score_rules.weights.health, 0.5);
|
|
|
|
|
assert_eq!(config.score_rules.weights.probe_freshness, 0.25);
|
|
|
|
|
assert_eq!(config.score_rules.weights.cost_lru, 0.25);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:05:49 +08:00
|
|
|
#[test]
|
|
|
|
|
fn parses_pool_config_from_generic_config_value() {
|
|
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [{"preset": "lru", "enabled": true}],
|
|
|
|
|
"cost_limit_per_key_tokens": 4096
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert!(config.lru_enabled);
|
|
|
|
|
assert_eq!(config.scheduling_presets.len(), 1);
|
|
|
|
|
assert_eq!(config.scheduling_presets[0].preset, "lru");
|
|
|
|
|
assert_eq!(config.cost_limit_per_key_tokens, Some(4096));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
#[test]
|
|
|
|
|
fn defaults_empty_pool_advanced_to_cache_affinity_preset() {
|
|
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert!(!config.lru_enabled);
|
|
|
|
|
assert_eq!(config.scheduling_presets.len(), 1);
|
|
|
|
|
assert_eq!(config.scheduling_presets[0].preset, "cache_affinity");
|
|
|
|
|
assert!(config.scheduling_presets[0].enabled);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:05:49 +08:00
|
|
|
#[test]
|
|
|
|
|
fn parses_object_style_scheduling_presets_with_modes() {
|
|
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "cache_affinity", "enabled": false},
|
2026-04-24 13:29:05 +08:00
|
|
|
{"preset": "plus_first", "enabled": true, "mode": "plus_only"},
|
|
|
|
|
{"preset": "pro_first", "enabled": true, "mode": "pro_only"}
|
2026-04-17 23:05:49 +08:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert!(!config.lru_enabled);
|
2026-04-24 13:29:05 +08:00
|
|
|
assert_eq!(config.scheduling_presets.len(), 3);
|
2026-04-17 23:05:49 +08:00
|
|
|
assert_eq!(config.scheduling_presets[0].preset, "cache_affinity");
|
|
|
|
|
assert!(!config.scheduling_presets[0].enabled);
|
|
|
|
|
assert_eq!(config.scheduling_presets[1].preset, "plus_first");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
config.scheduling_presets[1].mode.as_deref(),
|
|
|
|
|
Some("plus_only")
|
|
|
|
|
);
|
2026-04-24 13:29:05 +08:00
|
|
|
assert_eq!(config.scheduling_presets[2].preset, "pro_first");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
config.scheduling_presets[2].mode.as_deref(),
|
|
|
|
|
Some("pro_only")
|
|
|
|
|
);
|
2026-04-17 23:05:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 10:05:55 +08:00
|
|
|
#[test]
|
2026-04-24 13:29:05 +08:00
|
|
|
fn parses_legacy_string_style_scheduling_presets() {
|
2026-04-24 10:05:55 +08:00
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"lru_enabled": false,
|
|
|
|
|
"scheduling_presets": [
|
2026-04-24 13:29:05 +08:00
|
|
|
"free_first",
|
2026-04-24 10:05:55 +08:00
|
|
|
"recent_refresh",
|
2026-04-24 13:29:05 +08:00
|
|
|
"free_first"
|
2026-04-24 10:05:55 +08:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert!(!config.lru_enabled);
|
|
|
|
|
assert_eq!(config.scheduling_presets.len(), 3);
|
|
|
|
|
assert_eq!(config.scheduling_presets[0].preset, "lru");
|
|
|
|
|
assert!(!config.scheduling_presets[0].enabled);
|
2026-04-24 13:29:05 +08:00
|
|
|
assert_eq!(config.scheduling_presets[1].preset, "free_first");
|
2026-04-24 10:05:55 +08:00
|
|
|
assert_eq!(config.scheduling_presets[2].preset, "recent_refresh");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-24 13:29:05 +08:00
|
|
|
fn retired_free_team_first_preset_is_rejected() {
|
2026-04-24 10:05:55 +08:00
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
2026-04-24 13:29:05 +08:00
|
|
|
{"preset": "free_team_first", "enabled": true, "mode": "team_only"}
|
2026-04-24 10:05:55 +08:00
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.scheduling_presets.len(), 1);
|
2026-04-24 13:29:05 +08:00
|
|
|
assert_eq!(config.scheduling_presets[0].preset, "lru");
|
|
|
|
|
assert_eq!(config.scheduling_presets[0].mode, None);
|
2026-04-24 10:05:55 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:05:49 +08:00
|
|
|
#[test]
|
|
|
|
|
fn parses_unschedulable_rules_from_pool_advanced() {
|
|
|
|
|
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"unschedulable_rules": [
|
|
|
|
|
{"keyword": "suspended", "duration_minutes": 15},
|
|
|
|
|
{"keyword": "review_required"}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.unschedulable_rules.len(), 2);
|
|
|
|
|
assert_eq!(config.unschedulable_rules[0].keyword, "suspended");
|
|
|
|
|
assert_eq!(config.unschedulable_rules[0].duration_minutes, 15);
|
|
|
|
|
assert_eq!(config.unschedulable_rules[1].keyword, "review_required");
|
|
|
|
|
assert_eq!(config.unschedulable_rules[1].duration_minutes, 5);
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|
2026-05-11 14:06:49 +08:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cache_affinity_enabled_only_when_it_is_distribution_mode() {
|
|
|
|
|
let cache_affinity = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "cache_affinity", "enabled": true},
|
|
|
|
|
{"preset": "priority_first", "enabled": true}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
assert!(admin_provider_pool_cache_affinity_enabled(&cache_affinity));
|
|
|
|
|
|
|
|
|
|
let load_balance = admin_provider_pool_config_from_config_value(Some(&json!({
|
|
|
|
|
"pool_advanced": {
|
|
|
|
|
"scheduling_presets": [
|
|
|
|
|
{"preset": "load_balance", "enabled": true},
|
|
|
|
|
{"preset": "cache_affinity", "enabled": true}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.expect("pool config should parse");
|
|
|
|
|
assert!(!admin_provider_pool_cache_affinity_enabled(&load_balance));
|
|
|
|
|
}
|
2026-04-17 12:57:06 +08:00
|
|
|
}
|