mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(pool): 引入多维评分调度策略与账号状态检测
- 新增 multi_score 调度模式,支持 LRU/延迟/健康度/剩余额度多维加权评分 - 新增调度预设维度系统(free_team_first, quota_balanced, recent_refresh, single_account),支持有序对象列表配置格式并兼容旧字符串列表 - 新增 account_state 模块,统一账号封禁/受限检测逻辑,替代分散在 routes 中的判断代码 - 新增 health_cache 模块和 latency 采样(redis_ops.record_latency / batch_get_latency_avgs) - RequestDispatcher 返回 ttfb_ms,PoolManager.on_request_success 记录延迟样本 - 前端:PoolConfigDialog 替换为 PoolSchedulingDialog,支持预设维度可视化配置;号池管理页增加调度模式标签与账号异常 Badge 显示 - 提取前端 accountBlock 工具函数,ProviderDetailDrawer 复用统一判断 - scheduling_dimensions 增加 account_state 和 latency 维度评估 - 补充 account_state、health_cache、multi_score 策略、preset 维度、redis latency 等测试
This commit is contained in:
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
from src.services.provider.pool.config import (
|
||||
PoolConfig,
|
||||
SchedulingPreset,
|
||||
ScoringWeights,
|
||||
UnschedulableRule,
|
||||
parse_pool_config,
|
||||
)
|
||||
@@ -21,6 +23,14 @@ def test_parse_pool_config_returns_defaults_for_empty_advanced() -> None:
|
||||
assert cfg.sticky_session_ttl_seconds == 3600
|
||||
assert cfg.load_threshold_percent == 80
|
||||
assert cfg.lru_enabled is True
|
||||
assert cfg.scheduling_mode == "lru"
|
||||
assert cfg.scoring_weights == ScoringWeights()
|
||||
# Default: only LRU preset enabled
|
||||
assert len(cfg.scheduling_presets) == 1
|
||||
assert cfg.scheduling_presets[0].preset == "lru"
|
||||
assert cfg.scheduling_presets[0].enabled is True
|
||||
assert cfg.latency_window_seconds == 3600
|
||||
assert cfg.latency_sample_limit == 50
|
||||
assert cfg.cost_window_seconds == 18000
|
||||
assert cfg.cost_limit_per_key_tokens is None
|
||||
assert cfg.cost_soft_threshold_percent == 80
|
||||
@@ -31,13 +41,24 @@ def test_parse_pool_config_returns_defaults_for_empty_advanced() -> None:
|
||||
assert cfg.unschedulable_rules == []
|
||||
|
||||
|
||||
def test_parse_pool_config_overrides_values() -> None:
|
||||
def test_parse_pool_config_overrides_values_legacy_string_list() -> None:
|
||||
"""Legacy string-list format with scheduling_mode/lru_enabled."""
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"sticky_session_ttl_seconds": 7200,
|
||||
"load_threshold_percent": 90,
|
||||
"lru_enabled": False,
|
||||
"scheduling_mode": "multi_score",
|
||||
"scheduling_presets": ["free_team_first", "recent_refresh", "free_team_first"],
|
||||
"scoring_weights": {
|
||||
"lru": 0.1,
|
||||
"latency": 0.5,
|
||||
"health": 0.2,
|
||||
"cost_remaining": 0.2,
|
||||
},
|
||||
"latency_window_seconds": 7200,
|
||||
"latency_sample_limit": 80,
|
||||
"cost_window_seconds": 36000,
|
||||
"cost_limit_per_key_tokens": 100000,
|
||||
"cost_soft_threshold_percent": 70,
|
||||
@@ -52,6 +73,20 @@ def test_parse_pool_config_overrides_values() -> None:
|
||||
assert cfg.sticky_session_ttl_seconds == 7200
|
||||
assert cfg.load_threshold_percent == 90
|
||||
assert cfg.lru_enabled is False
|
||||
assert cfg.scheduling_mode == "multi_score"
|
||||
# Legacy string list → SchedulingPreset objects, deduped
|
||||
preset_names = tuple(p.preset for p in cfg.scheduling_presets)
|
||||
assert "free_team_first" in preset_names
|
||||
assert "recent_refresh" in preset_names
|
||||
assert cfg.scoring_weights == ScoringWeights(
|
||||
lru=0.1,
|
||||
latency=0.5,
|
||||
health=0.2,
|
||||
cost_remaining=0.2,
|
||||
)
|
||||
assert cfg.latency_window_seconds == 7200
|
||||
assert cfg.latency_sample_limit == 80
|
||||
assert "multi_score" in cfg.strategies
|
||||
assert cfg.cost_window_seconds == 36000
|
||||
assert cfg.cost_limit_per_key_tokens == 100000
|
||||
assert cfg.cost_soft_threshold_percent == 70
|
||||
@@ -61,6 +96,114 @@ def test_parse_pool_config_overrides_values() -> None:
|
||||
assert cfg.health_policy_enabled is False
|
||||
|
||||
|
||||
def test_parse_pool_config_new_object_list_format() -> None:
|
||||
"""New object-list format: [{preset, enabled, mode}]."""
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_presets": [
|
||||
{"preset": "lru", "enabled": True},
|
||||
{"preset": "free_team_first", "enabled": True, "mode": "free_only"},
|
||||
{"preset": "quota_balanced", "enabled": False},
|
||||
{"preset": "recent_refresh", "enabled": True},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert len(cfg.scheduling_presets) == 4
|
||||
|
||||
lru = cfg.scheduling_presets[0]
|
||||
assert lru.preset == "lru"
|
||||
assert lru.enabled is True
|
||||
|
||||
ftf = cfg.scheduling_presets[1]
|
||||
assert ftf.preset == "free_team_first"
|
||||
assert ftf.enabled is True
|
||||
assert ftf.mode == "free_only"
|
||||
|
||||
qb = cfg.scheduling_presets[2]
|
||||
assert qb.preset == "quota_balanced"
|
||||
assert qb.enabled is False
|
||||
|
||||
rr = cfg.scheduling_presets[3]
|
||||
assert rr.preset == "recent_refresh"
|
||||
assert rr.enabled is True
|
||||
|
||||
# Derived fields: lru enabled, non-lru enabled → multi_score
|
||||
assert cfg.lru_enabled is True
|
||||
assert cfg.scheduling_mode == "multi_score"
|
||||
|
||||
|
||||
def test_parse_pool_config_new_format_lru_only() -> None:
|
||||
"""When only LRU is enabled, scheduling_mode should be 'lru'."""
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_presets": [
|
||||
{"preset": "lru", "enabled": True},
|
||||
{"preset": "quota_balanced", "enabled": False},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.lru_enabled is True
|
||||
assert cfg.scheduling_mode == "lru"
|
||||
|
||||
|
||||
def test_parse_pool_config_new_format_lru_disabled() -> None:
|
||||
"""LRU disabled, other presets enabled → multi_score + lru_enabled=False."""
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_presets": [
|
||||
{"preset": "lru", "enabled": False},
|
||||
{"preset": "quota_balanced", "enabled": True},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.lru_enabled is False
|
||||
assert cfg.scheduling_mode == "multi_score"
|
||||
|
||||
|
||||
def test_parse_pool_config_new_format_free_team_mode_validation() -> None:
|
||||
"""Invalid mode falls back to 'both'."""
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_presets": [
|
||||
{"preset": "free_team_first", "enabled": True, "mode": "invalid_mode"},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
ftf = [p for p in cfg.scheduling_presets if p.preset == "free_team_first"][0]
|
||||
assert ftf.mode == "both"
|
||||
|
||||
|
||||
def test_parse_pool_config_new_format_dedup_presets() -> None:
|
||||
"""Duplicate presets in object list should be deduplicated."""
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_presets": [
|
||||
{"preset": "lru", "enabled": True},
|
||||
{"preset": "lru", "enabled": False},
|
||||
{"preset": "quota_balanced", "enabled": True},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
lru_presets = [p for p in cfg.scheduling_presets if p.preset == "lru"]
|
||||
assert len(lru_presets) == 1
|
||||
assert lru_presets[0].enabled is True # first occurrence wins
|
||||
|
||||
|
||||
def test_parse_pool_config_parses_unschedulable_rules() -> None:
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
@@ -97,6 +240,47 @@ def test_parse_pool_config_handles_invalid_types_gracefully() -> None:
|
||||
assert cfg.cost_limit_per_key_tokens is None # default for opt_int
|
||||
|
||||
|
||||
def test_parse_pool_config_invalid_scheduling_mode_falls_back_to_lru() -> None:
|
||||
cfg = parse_pool_config({"pool_advanced": {"scheduling_mode": "unknown"}})
|
||||
assert cfg is not None
|
||||
assert cfg.scheduling_mode == "lru"
|
||||
|
||||
|
||||
def test_parse_pool_config_scoring_weights_invalid_values_are_clamped() -> None:
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_mode": "multi_score",
|
||||
"scoring_weights": {
|
||||
"lru": 2.0,
|
||||
"latency": -1.0,
|
||||
"health": "bad",
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
assert cfg.scoring_weights.lru == 1.0
|
||||
assert cfg.scoring_weights.latency == 0.0
|
||||
assert cfg.scoring_weights.health == 0.2
|
||||
|
||||
|
||||
def test_parse_pool_config_invalid_scheduling_presets_are_ignored() -> None:
|
||||
cfg = parse_pool_config(
|
||||
{
|
||||
"pool_advanced": {
|
||||
"scheduling_mode": "multi_score",
|
||||
"scheduling_presets": ["quota_balanced", "unknown", 123, "single_account"],
|
||||
}
|
||||
}
|
||||
)
|
||||
assert cfg is not None
|
||||
preset_names = tuple(p.preset for p in cfg.scheduling_presets if p.preset != "lru")
|
||||
assert "quota_balanced" in preset_names
|
||||
assert "single_account" in preset_names
|
||||
assert "unknown" not in preset_names
|
||||
|
||||
|
||||
def test_pool_config_is_frozen() -> None:
|
||||
cfg = PoolConfig()
|
||||
try:
|
||||
@@ -104,3 +288,12 @@ def test_pool_config_is_frozen() -> None:
|
||||
assert False, "Should have raised FrozenInstanceError"
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
def test_scheduling_preset_is_frozen() -> None:
|
||||
preset = SchedulingPreset(preset="lru", enabled=True)
|
||||
try:
|
||||
preset.enabled = False # type: ignore[misc]
|
||||
assert False, "Should have raised FrozenInstanceError"
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user