mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor: 拆分调度器为独立子模块,增强 Sub2API 多认证方式支持
调度器重构: - 将 CacheAwareScheduler 拆分为 candidate_builder、candidate_sorter、 concurrency_checker、restriction_checker、scheduling_config、schemas、utils 等独立模块 - 删除旧的 _candidate_builder.py 和 _candidate_sorter.py - 新增调度并发拒绝 Prometheus 指标 Sub2API 架构增强: - 支持账号密码登录和 Refresh Token 两种认证方式 - 实现 JWT 自动刷新和 Token Rotation 持久化 - 前端 ProviderAuthDialog 支持多认证方式切换和 credentials_schema 动态渲染 - 验证接口返回 updated_credentials 以同步轮换后的 token 其他改进: - 并发管理器增加 RPM guard 和动态预留逻辑 - RequestCandidate 支持 mark_skipped 附加 extra_data - TaskService 增强健壮性 - 补充相关单元测试和契约测试
This commit is contained in:
201
tests/unit/test_candidate_sorter_keep_priority_on_conversion.py
Normal file
201
tests/unit/test_candidate_sorter_keep_priority_on_conversion.py
Normal file
@@ -0,0 +1,201 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.services.cache.candidate_sorter import CandidateSorter
|
||||
from src.services.cache.scheduling_config import SchedulingConfig
|
||||
from src.services.cache.schemas import ProviderCandidate
|
||||
|
||||
|
||||
def _make_candidate(
|
||||
*,
|
||||
key_id: str,
|
||||
global_priority: int,
|
||||
needs_conversion: bool,
|
||||
provider_keep_priority_on_conversion: bool,
|
||||
) -> ProviderCandidate:
|
||||
provider = SimpleNamespace(
|
||||
id=f"p_{key_id}",
|
||||
name=f"prov_{key_id}",
|
||||
provider_priority=1,
|
||||
keep_priority_on_conversion=provider_keep_priority_on_conversion,
|
||||
)
|
||||
endpoint = SimpleNamespace(id=f"e_{key_id}")
|
||||
key = SimpleNamespace(
|
||||
id=key_id,
|
||||
internal_priority=1,
|
||||
global_priority_by_format={"openai:chat": global_priority},
|
||||
)
|
||||
return ProviderCandidate(
|
||||
provider=provider,
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
needs_conversion=needs_conversion,
|
||||
provider_api_format="openai:chat",
|
||||
) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def test_priority_sort_global_key_does_not_demote_when_global_keep_priority_enabled() -> None:
|
||||
db = MagicMock()
|
||||
config = SchedulingConfig(
|
||||
priority_mode=SchedulingConfig.PRIORITY_MODE_GLOBAL_KEY,
|
||||
scheduling_mode=SchedulingConfig.SCHEDULING_MODE_FIXED_ORDER,
|
||||
)
|
||||
sorter = CandidateSorter(config)
|
||||
|
||||
exact = _make_candidate(
|
||||
key_id="k_exact",
|
||||
global_priority=10,
|
||||
needs_conversion=False,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
demoted = _make_candidate(
|
||||
key_id="k_demote",
|
||||
global_priority=1,
|
||||
needs_conversion=True,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
|
||||
# 全局 keep_priority_on_conversion=True:不做 needs_conversion 降级分组,纯按 global_priority 排序
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=True,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([exact, demoted], db, None, "openai:chat")
|
||||
|
||||
assert [c.key.id for c in result] == ["k_demote", "k_exact"]
|
||||
|
||||
|
||||
def test_priority_sort_global_key_demotes_convertible_when_global_keep_priority_disabled() -> None:
|
||||
db = MagicMock()
|
||||
config = SchedulingConfig(
|
||||
priority_mode=SchedulingConfig.PRIORITY_MODE_GLOBAL_KEY,
|
||||
scheduling_mode=SchedulingConfig.SCHEDULING_MODE_FIXED_ORDER,
|
||||
)
|
||||
sorter = CandidateSorter(config)
|
||||
|
||||
exact = _make_candidate(
|
||||
key_id="k_exact",
|
||||
global_priority=10,
|
||||
needs_conversion=False,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
demoted = _make_candidate(
|
||||
key_id="k_demote",
|
||||
global_priority=1,
|
||||
needs_conversion=True,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
|
||||
# 全局 keep_priority_on_conversion=False:需要降级的 convertible 候选整体排后
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=False,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([exact, demoted], db, None, "openai:chat")
|
||||
|
||||
assert [c.key.id for c in result] == ["k_exact", "k_demote"]
|
||||
|
||||
|
||||
def test_priority_sort_global_key_provider_keep_priority_overrides_demotion_group() -> None:
|
||||
db = MagicMock()
|
||||
config = SchedulingConfig(
|
||||
priority_mode=SchedulingConfig.PRIORITY_MODE_GLOBAL_KEY,
|
||||
scheduling_mode=SchedulingConfig.SCHEDULING_MODE_FIXED_ORDER,
|
||||
)
|
||||
sorter = CandidateSorter(config)
|
||||
|
||||
exact = _make_candidate(
|
||||
key_id="k_exact",
|
||||
global_priority=10,
|
||||
needs_conversion=False,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
convertible_keep = _make_candidate(
|
||||
key_id="k_keep",
|
||||
global_priority=1,
|
||||
needs_conversion=True,
|
||||
provider_keep_priority_on_conversion=True,
|
||||
)
|
||||
convertible_demote = _make_candidate(
|
||||
key_id="k_demote",
|
||||
global_priority=0,
|
||||
needs_conversion=True,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=False,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort(
|
||||
[exact, convertible_demote, convertible_keep],
|
||||
db,
|
||||
None,
|
||||
"openai:chat",
|
||||
)
|
||||
|
||||
assert [c.key.id for c in result] == ["k_keep", "k_exact", "k_demote"]
|
||||
|
||||
|
||||
def test_priority_sort_provider_mode_demotes_convertible_when_global_keep_priority_disabled() -> (
|
||||
None
|
||||
):
|
||||
db = MagicMock()
|
||||
config = SchedulingConfig(
|
||||
priority_mode=SchedulingConfig.PRIORITY_MODE_PROVIDER,
|
||||
scheduling_mode=SchedulingConfig.SCHEDULING_MODE_FIXED_ORDER,
|
||||
)
|
||||
sorter = CandidateSorter(config)
|
||||
|
||||
exact = _make_candidate(
|
||||
key_id="k_exact",
|
||||
global_priority=10,
|
||||
needs_conversion=False,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
demoted = _make_candidate(
|
||||
key_id="k_demote",
|
||||
global_priority=1,
|
||||
needs_conversion=True,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=False,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([demoted, exact], db, None, "openai:chat")
|
||||
|
||||
assert [c.key.id for c in result] == ["k_exact", "k_demote"]
|
||||
|
||||
|
||||
def test_priority_sort_provider_mode_does_not_demote_when_global_keep_priority_enabled() -> None:
|
||||
db = MagicMock()
|
||||
config = SchedulingConfig(
|
||||
priority_mode=SchedulingConfig.PRIORITY_MODE_PROVIDER,
|
||||
scheduling_mode=SchedulingConfig.SCHEDULING_MODE_FIXED_ORDER,
|
||||
)
|
||||
sorter = CandidateSorter(config)
|
||||
|
||||
exact = _make_candidate(
|
||||
key_id="k_exact",
|
||||
global_priority=10,
|
||||
needs_conversion=False,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
demoted = _make_candidate(
|
||||
key_id="k_demote",
|
||||
global_priority=1,
|
||||
needs_conversion=True,
|
||||
provider_keep_priority_on_conversion=False,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"src.services.cache.candidate_sorter.SystemConfigService.is_keep_priority_on_conversion",
|
||||
return_value=True,
|
||||
):
|
||||
result = sorter._apply_priority_mode_sort([demoted, exact], db, None, "openai:chat")
|
||||
|
||||
assert [c.key.id for c in result] == ["k_demote", "k_exact"]
|
||||
40
tests/unit/test_request_candidate_mark_skipped.py
Normal file
40
tests/unit/test_request_candidate_mark_skipped.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from src.services.request.candidate import RequestCandidateService
|
||||
|
||||
|
||||
def test_mark_candidate_skipped_merges_extra_data_and_sets_fields() -> None:
|
||||
candidate = SimpleNamespace(
|
||||
status="available",
|
||||
skip_reason=None,
|
||||
finished_at=None,
|
||||
status_code=None,
|
||||
concurrent_requests=None,
|
||||
extra_data={"needs_conversion": True},
|
||||
)
|
||||
|
||||
query = MagicMock()
|
||||
query.filter.return_value.first.return_value = candidate
|
||||
|
||||
db = MagicMock()
|
||||
db.query.return_value = query
|
||||
|
||||
RequestCandidateService.mark_candidate_skipped(
|
||||
db=db,
|
||||
candidate_id="c1",
|
||||
skip_reason="并发限制",
|
||||
status_code=429,
|
||||
concurrent_requests=12,
|
||||
extra_data={"concurrency_denied": True},
|
||||
)
|
||||
|
||||
assert candidate.status == "skipped"
|
||||
assert candidate.skip_reason == "并发限制"
|
||||
assert candidate.status_code == 429
|
||||
assert candidate.concurrent_requests == 12
|
||||
assert candidate.extra_data == {"needs_conversion": True, "concurrency_denied": True}
|
||||
assert candidate.finished_at is not None
|
||||
db.flush.assert_called_once()
|
||||
Reference in New Issue
Block a user