mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(pool,admin): 拆分调度维度、重构调度 UI 与增强配置导入导出
调度维度: - 新增 cache_affinity/free_first/team_first/plus_first/load_balance 五个独立维度 - 将 free_team_first 标记为 hidden,保留后向兼容但不再显示 - Registry 新增 hidden 属性,_helpers 新增 plus_only 优先级评分 前端调度对话框: - 分配模式(互斥组)独立为按钮组选择,策略调度保留拖拽排序 - 默认调度从 LRU 轮转改为缓存亲和 - 提取 buildPresetListItem/insertMissingByPreferredOrder 消除重复代码 配置导入导出: - 导出时 api_formats 支持规范化、去重与 None 回退到 Provider 端点 - 导入时兼容 supported_endpoints 别名与历史 None 语义 - 新增 test_admin_system_key_formats 单元测试
This commit is contained in:
@@ -910,6 +910,67 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
||||
"cookie",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_api_formats(raw_formats: Any) -> list[str]:
|
||||
"""规范化 api_formats 为 endpoint signature 列表。"""
|
||||
from src.core.api_format.signature import normalize_signature_key
|
||||
|
||||
if not isinstance(raw_formats, list):
|
||||
return []
|
||||
|
||||
normalized: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in raw_formats:
|
||||
if not isinstance(raw, str):
|
||||
continue
|
||||
value = raw.strip()
|
||||
if not value:
|
||||
continue
|
||||
try:
|
||||
fmt = normalize_signature_key(value)
|
||||
except Exception:
|
||||
continue
|
||||
if fmt in seen:
|
||||
continue
|
||||
seen.add(fmt)
|
||||
normalized.append(fmt)
|
||||
return normalized
|
||||
|
||||
def _resolve_export_key_api_formats(
|
||||
self, raw_formats: Any, provider_endpoint_formats: list[str]
|
||||
) -> list[str]:
|
||||
"""导出 Key 时解析支持端点:
|
||||
|
||||
- 优先使用 Key 自身 api_formats(规范化后)
|
||||
- 当 api_formats 为 None(历史语义:全支持)时回退为 Provider 端点列表
|
||||
- 当 api_formats 显式为空列表时保留空列表
|
||||
"""
|
||||
normalized = self._normalize_api_formats(raw_formats)
|
||||
if normalized:
|
||||
return normalized
|
||||
|
||||
if raw_formats is None:
|
||||
return list(provider_endpoint_formats)
|
||||
return []
|
||||
|
||||
def _collect_provider_endpoint_formats(self, endpoints: list[Any]) -> list[str]:
|
||||
"""收集 Provider 下所有 endpoint signature(去重后排序)。"""
|
||||
normalized: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for ep in endpoints:
|
||||
raw = getattr(ep, "api_format", None)
|
||||
if hasattr(raw, "value"):
|
||||
raw = raw.value
|
||||
fmt_list = self._normalize_api_formats([raw])
|
||||
if not fmt_list:
|
||||
continue
|
||||
fmt = fmt_list[0]
|
||||
if fmt in seen:
|
||||
continue
|
||||
seen.add(fmt)
|
||||
normalized.append(fmt)
|
||||
return sorted(normalized)
|
||||
|
||||
def _decrypt_provider_config(self, config: dict, crypto_service: Any) -> dict:
|
||||
"""解密 Provider config 中的 provider_ops credentials"""
|
||||
if not config:
|
||||
@@ -959,6 +1020,7 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
||||
db.query(ProviderEndpoint).filter(ProviderEndpoint.provider_id == provider.id).all()
|
||||
)
|
||||
endpoints_data = [ep.to_export_dict() for ep in endpoints]
|
||||
provider_endpoint_formats = self._collect_provider_endpoint_formats(endpoints)
|
||||
|
||||
# 导出 Provider Keys(按 provider_id 归属,包含 api_formats)
|
||||
keys = (
|
||||
@@ -970,6 +1032,13 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
||||
keys_data = []
|
||||
for key in keys:
|
||||
key_data = key.to_export_dict()
|
||||
key_formats = self._resolve_export_key_api_formats(
|
||||
key_data.get("api_formats"),
|
||||
provider_endpoint_formats,
|
||||
)
|
||||
# 保持现有字段名 api_formats,并补充可读别名 supported_endpoints。
|
||||
key_data["api_formats"] = key_formats
|
||||
key_data["supported_endpoints"] = list(key_formats)
|
||||
# 解密 API Key
|
||||
try:
|
||||
key_data["api_key"] = crypto_service.decrypt(key.api_key)
|
||||
@@ -1122,6 +1191,29 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
||||
"cookie",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _extract_import_key_api_formats(
|
||||
key_data: dict[str, Any], endpoint_formats: set[str]
|
||||
) -> list[str]:
|
||||
"""导入 Key 时提取 api_formats(兼容历史字段与旧语义)。"""
|
||||
raw_formats = key_data.get("api_formats")
|
||||
if isinstance(raw_formats, list):
|
||||
if raw_formats:
|
||||
return raw_formats
|
||||
legacy_formats = key_data.get("supported_endpoints")
|
||||
if isinstance(legacy_formats, list) and legacy_formats:
|
||||
return legacy_formats
|
||||
return []
|
||||
|
||||
legacy_formats = key_data.get("supported_endpoints")
|
||||
if isinstance(legacy_formats, list) and legacy_formats:
|
||||
return legacy_formats
|
||||
|
||||
# 兼容历史数据:api_formats=None 代表支持 Provider 的全部端点。
|
||||
if raw_formats is None and endpoint_formats:
|
||||
return sorted(endpoint_formats)
|
||||
return []
|
||||
|
||||
def _encrypt_provider_config(self, config: dict, crypto_service: Any) -> dict:
|
||||
"""加密 Provider config 中的 provider_ops credentials"""
|
||||
if not config:
|
||||
@@ -1436,8 +1528,8 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
||||
stats["keys"]["skipped"] += 1
|
||||
continue
|
||||
|
||||
raw_formats = key_data.get("api_formats") or []
|
||||
if not isinstance(raw_formats, list) or len(raw_formats) == 0:
|
||||
raw_formats = self._extract_import_key_api_formats(key_data, endpoint_formats)
|
||||
if len(raw_formats) == 0:
|
||||
stats["errors"].append(
|
||||
f"跳过无 api_formats 的 Key (Provider: {prov_data['name']})"
|
||||
)
|
||||
|
||||
@@ -5,14 +5,19 @@ Importing this package registers all built-in preset dimensions.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import cache_affinity # noqa: F401
|
||||
from . import cost_first # noqa: F401
|
||||
from . import free_first # noqa: F401
|
||||
from . import free_team_first # noqa: F401
|
||||
from . import health_first # noqa: F401
|
||||
from . import latency_first # noqa: F401
|
||||
from . import load_balance # noqa: F401
|
||||
from . import plus_first # noqa: F401
|
||||
from . import priority_first # noqa: F401
|
||||
from . import quota_balanced # noqa: F401
|
||||
from . import recent_refresh # noqa: F401
|
||||
from . import single_account # noqa: F401
|
||||
from . import team_first # noqa: F401
|
||||
from .registry import (
|
||||
PresetDimensionBase,
|
||||
PresetDimensionMeta,
|
||||
|
||||
@@ -219,7 +219,10 @@ def extract_health_score(key_obj: Any) -> float | None:
|
||||
|
||||
|
||||
def plan_priority_score(plan_type: str | None, mode: str | None = None) -> float:
|
||||
"""Score a key based on plan type and free_team_first mode."""
|
||||
"""Score a key based on plan type and scheduling mode.
|
||||
|
||||
Lower score = higher priority.
|
||||
"""
|
||||
|
||||
effective_mode = (mode or "both").strip().lower()
|
||||
if effective_mode == "free_only":
|
||||
@@ -232,6 +235,11 @@ def plan_priority_score(plan_type: str | None, mode: str | None = None) -> float
|
||||
return 0.0
|
||||
if plan_type == "free":
|
||||
return 0.5
|
||||
elif effective_mode == "plus_only":
|
||||
if plan_type in {"plus", "pro"}:
|
||||
return 0.0
|
||||
if plan_type in {"enterprise", "business"}:
|
||||
return 0.3
|
||||
else:
|
||||
# "both" or unrecognized -> original behavior
|
||||
if plan_type in {"free", "team"}:
|
||||
|
||||
45
src/services/provider/pool/dimensions/cache_affinity.py
Normal file
45
src/services/provider/pool/dimensions/cache_affinity.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""cache_affinity preset dimension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._helpers import rank_descending
|
||||
from .registry import PresetDimensionBase, register_preset_dimension
|
||||
|
||||
|
||||
class CacheAffinityDimension(PresetDimensionBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "cache_affinity"
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return "缓存亲和"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "优先复用最近使用过的 Key,利用 Prompt Caching"
|
||||
|
||||
@property
|
||||
def mutex_group(self) -> str | None:
|
||||
return "distribution_mode"
|
||||
|
||||
@property
|
||||
def evidence_hint(self) -> str | None:
|
||||
return "依据 LRU 时间戳(最近使用优先,与 LRU 轮转相反)"
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
key_id: str,
|
||||
all_key_ids: list[str],
|
||||
keys_by_id: dict[str, Any],
|
||||
lru_scores: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
mode: str | None,
|
||||
) -> float:
|
||||
return rank_descending(key_id, lru_scores, all_key_ids)
|
||||
|
||||
|
||||
register_preset_dimension(CacheAffinityDimension())
|
||||
51
src/services/provider/pool/dimensions/free_first.py
Normal file
51
src/services/provider/pool/dimensions/free_first.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""free_first preset dimension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._helpers import extract_plan_type, plan_priority_score, rank_ascending
|
||||
from .registry import PresetDimensionBase, register_preset_dimension
|
||||
|
||||
|
||||
class FreeFirstDimension(PresetDimensionBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "free_first"
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return "Free 优先"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "优先消耗 Free 账号(依赖 plan_type)"
|
||||
|
||||
@property
|
||||
def evidence_hint(self) -> str | None:
|
||||
return "依据 plan_type(Free 账号优先调度)"
|
||||
|
||||
@property
|
||||
def providers(self) -> tuple[str, ...]:
|
||||
return ("codex", "kiro")
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
key_id: str,
|
||||
all_key_ids: list[str],
|
||||
keys_by_id: dict[str, Any],
|
||||
lru_scores: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
mode: str | None,
|
||||
) -> float:
|
||||
plan_scores = {
|
||||
kid: plan_priority_score(extract_plan_type(keys_by_id.get(kid)), "free_only")
|
||||
for kid in all_key_ids
|
||||
}
|
||||
if len(set(plan_scores.values())) <= 1:
|
||||
return rank_ascending(key_id, lru_scores, all_key_ids)
|
||||
return rank_ascending(key_id, plan_scores, all_key_ids)
|
||||
|
||||
|
||||
register_preset_dimension(FreeFirstDimension())
|
||||
@@ -37,6 +37,10 @@ class FreeTeamFirstDimension(PresetDimensionBase):
|
||||
def default_mode(self) -> str | None:
|
||||
return "both"
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
return True
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
|
||||
45
src/services/provider/pool/dimensions/load_balance.py
Normal file
45
src/services/provider/pool/dimensions/load_balance.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""load_balance preset dimension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from .registry import PresetDimensionBase, register_preset_dimension
|
||||
|
||||
|
||||
class LoadBalanceDimension(PresetDimensionBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "load_balance"
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return "负载均衡"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "随机分散 Key 使用,均匀分摊负载"
|
||||
|
||||
@property
|
||||
def mutex_group(self) -> str | None:
|
||||
return "distribution_mode"
|
||||
|
||||
@property
|
||||
def evidence_hint(self) -> str | None:
|
||||
return "每次随机分值,实现完全均匀分散"
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
key_id: str,
|
||||
all_key_ids: list[str],
|
||||
keys_by_id: dict[str, Any],
|
||||
lru_scores: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
mode: str | None,
|
||||
) -> float:
|
||||
return random.random()
|
||||
|
||||
|
||||
register_preset_dimension(LoadBalanceDimension())
|
||||
51
src/services/provider/pool/dimensions/plus_first.py
Normal file
51
src/services/provider/pool/dimensions/plus_first.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""plus_first preset dimension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._helpers import extract_plan_type, plan_priority_score, rank_ascending
|
||||
from .registry import PresetDimensionBase, register_preset_dimension
|
||||
|
||||
|
||||
class PlusFirstDimension(PresetDimensionBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "plus_first"
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return "Plus 优先"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "优先消耗 Plus/Pro 账号(依赖 plan_type)"
|
||||
|
||||
@property
|
||||
def evidence_hint(self) -> str | None:
|
||||
return "依据 plan_type(Plus/Pro 账号优先调度)"
|
||||
|
||||
@property
|
||||
def providers(self) -> tuple[str, ...]:
|
||||
return ("codex", "kiro")
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
key_id: str,
|
||||
all_key_ids: list[str],
|
||||
keys_by_id: dict[str, Any],
|
||||
lru_scores: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
mode: str | None,
|
||||
) -> float:
|
||||
plan_scores = {
|
||||
kid: plan_priority_score(extract_plan_type(keys_by_id.get(kid)), "plus_only")
|
||||
for kid in all_key_ids
|
||||
}
|
||||
if len(set(plan_scores.values())) <= 1:
|
||||
return rank_ascending(key_id, lru_scores, all_key_ids)
|
||||
return rank_ascending(key_id, plan_scores, all_key_ids)
|
||||
|
||||
|
||||
register_preset_dimension(PlusFirstDimension())
|
||||
@@ -76,6 +76,16 @@ class PresetDimensionBase(ABC):
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
"""If True, this dimension is excluded from API metadata listings.
|
||||
|
||||
The dimension remains functional for backward compatibility but
|
||||
will not appear in the scheduling dialog.
|
||||
"""
|
||||
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def compute_metric(
|
||||
self,
|
||||
@@ -170,6 +180,10 @@ def register_preset_dimension(dim: PresetDimensionBase) -> None:
|
||||
raw = str(self._wrapped.evidence_hint or "").strip()
|
||||
return raw or None
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
return self._wrapped.hidden
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
@@ -223,6 +237,8 @@ def get_preset_dimension_metas() -> list[PresetDimensionMeta]:
|
||||
|
||||
metas: list[PresetDimensionMeta] = []
|
||||
for dim in get_all_preset_dimensions():
|
||||
if dim.hidden:
|
||||
continue
|
||||
metas.append(
|
||||
PresetDimensionMeta(
|
||||
name=dim.name,
|
||||
|
||||
51
src/services/provider/pool/dimensions/team_first.py
Normal file
51
src/services/provider/pool/dimensions/team_first.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""team_first preset dimension."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._helpers import extract_plan_type, plan_priority_score, rank_ascending
|
||||
from .registry import PresetDimensionBase, register_preset_dimension
|
||||
|
||||
|
||||
class TeamFirstDimension(PresetDimensionBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "team_first"
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return "Team 优先"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "优先消耗 Team 账号(依赖 plan_type)"
|
||||
|
||||
@property
|
||||
def evidence_hint(self) -> str | None:
|
||||
return "依据 plan_type(Team 账号优先调度)"
|
||||
|
||||
@property
|
||||
def providers(self) -> tuple[str, ...]:
|
||||
return ("codex", "kiro")
|
||||
|
||||
def compute_metric(
|
||||
self,
|
||||
*,
|
||||
key_id: str,
|
||||
all_key_ids: list[str],
|
||||
keys_by_id: dict[str, Any],
|
||||
lru_scores: dict[str, Any],
|
||||
context: dict[str, Any],
|
||||
mode: str | None,
|
||||
) -> float:
|
||||
plan_scores = {
|
||||
kid: plan_priority_score(extract_plan_type(keys_by_id.get(kid)), "team_only")
|
||||
for kid in all_key_ids
|
||||
}
|
||||
if len(set(plan_scores.values())) <= 1:
|
||||
return rank_ascending(key_id, lru_scores, all_key_ids)
|
||||
return rank_ascending(key_id, plan_scores, all_key_ids)
|
||||
|
||||
|
||||
register_preset_dimension(TeamFirstDimension())
|
||||
Reference in New Issue
Block a user