feat(pool): 增加 OAuth 账号池管理功能

- 新增 pool manager / strategy / health_policy / cost_tracker / redis_ops 等核心模块
- 新增 pool admin API 路由与 schemas
- 新增 OAuth 账号类型解析 (oauth_plan)
- 前端增加 PoolManagement 页面、PoolConfigDialog、PoolImportDialog、PoolStatusCard 组件
- 补充 pool config / cost tracker / health policy / manager / strategy / trace 等测试
This commit is contained in:
fawney19
2026-02-27 13:58:58 +08:00
parent 579b5e4623
commit 76a6d0ce8e
35 changed files with 5761 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
"""Tests for pool scheduling trace dataclasses."""
from __future__ import annotations
from src.services.provider.pool.trace import PoolCandidateTrace, PoolSchedulingTrace
# ---------------------------------------------------------------------------
# PoolCandidateTrace.to_extra_data
# ---------------------------------------------------------------------------
class TestPoolCandidateTraceExtraData:
def test_selected_sticky(self) -> None:
ct = PoolCandidateTrace(key_id="k1", reason="sticky", sticky_hit=True)
data = ct.to_extra_data()
assert "pool_selection" in data
assert data["pool_selection"]["reason"] == "sticky"
assert data["pool_selection"]["sticky_hit"] is True
def test_selected_lru_with_cost(self) -> None:
ct = PoolCandidateTrace(
key_id="k2",
reason="lru",
lru_score=1234.5,
cost_window_usage=500,
cost_limit=1000,
)
data = ct.to_extra_data()
sel = data["pool_selection"]
assert sel["reason"] == "lru"
assert sel["lru_score"] == 1234.5
assert sel["cost_window_usage"] == 500
assert sel["cost_limit"] == 1000
assert "sticky_hit" not in sel
def test_selected_with_soft_threshold(self) -> None:
ct = PoolCandidateTrace(
key_id="k3",
reason="lru",
cost_window_usage=850,
cost_limit=1000,
cost_soft_threshold=True,
)
data = ct.to_extra_data()
assert data["pool_selection"]["cost_soft_threshold"] is True
def test_selected_random_minimal(self) -> None:
ct = PoolCandidateTrace(key_id="k4", reason="random")
data = ct.to_extra_data()
sel = data["pool_selection"]
assert sel == {"reason": "random"}
def test_skipped_cooldown(self) -> None:
ct = PoolCandidateTrace(
key_id="k5",
skipped=True,
skip_type="cooldown",
cooldown_reason="rate_limited_429",
cooldown_ttl=120,
)
data = ct.to_extra_data()
assert "pool_skip" in data
skip = data["pool_skip"]
assert skip["type"] == "cooldown"
assert skip["cooldown_reason"] == "rate_limited_429"
assert skip["cooldown_ttl"] == 120
def test_skipped_cost_exhausted(self) -> None:
ct = PoolCandidateTrace(
key_id="k6",
skipped=True,
skip_type="cost_exhausted",
cost_window_usage=2000,
)
data = ct.to_extra_data()
skip = data["pool_skip"]
assert skip["type"] == "cost_exhausted"
assert skip["cost_window_usage"] == 2000
assert "cooldown_reason" not in skip
def test_skipped_minimal(self) -> None:
ct = PoolCandidateTrace(key_id="k7", skipped=True, skip_type="upstream")
data = ct.to_extra_data()
skip = data["pool_skip"]
assert skip == {"type": "upstream"}
# ---------------------------------------------------------------------------
# PoolSchedulingTrace.build_summary
# ---------------------------------------------------------------------------
class TestPoolSchedulingTraceSummary:
def test_basic_summary(self) -> None:
trace = PoolSchedulingTrace(provider_id="prov-1", total_keys=5)
trace.candidate_traces = {
"k1": PoolCandidateTrace(key_id="k1", reason="sticky", sticky_hit=True),
"k2": PoolCandidateTrace(key_id="k2", reason="lru"),
"k3": PoolCandidateTrace(
key_id="k3", skipped=True, skip_type="cooldown", cooldown_reason="429"
),
"k4": PoolCandidateTrace(key_id="k4", skipped=True, skip_type="cost_exhausted"),
"k5": PoolCandidateTrace(
key_id="k5", skipped=True, skip_type="cooldown", cooldown_reason="500"
),
}
trace.sticky_session_used = True
summary = trace.build_summary(success_key_id="k1")
assert summary["enabled"] is True
assert summary["total_keys"] == 5
assert summary["attempted"] == 2
assert summary["skipped_cooldown"] == 2
assert summary["skipped_cost"] == 1
assert summary["sticky_session"] is True
assert summary["success_key_id"] == "k1"[:8]
assert summary["success_reason"] == "sticky"
def test_summary_no_success_key(self) -> None:
trace = PoolSchedulingTrace(provider_id="prov-2", total_keys=2)
trace.candidate_traces = {
"k1": PoolCandidateTrace(key_id="k1", reason="random"),
"k2": PoolCandidateTrace(key_id="k2", reason="lru"),
}
summary = trace.build_summary(success_key_id=None)
assert summary["attempted"] == 2
assert summary["skipped_cooldown"] == 0
assert summary["skipped_cost"] == 0
assert "success_key_id" not in summary
assert "success_reason" not in summary
def test_summary_all_skipped(self) -> None:
trace = PoolSchedulingTrace(provider_id="prov-3", total_keys=2)
trace.candidate_traces = {
"k1": PoolCandidateTrace(key_id="k1", skipped=True, skip_type="cooldown"),
"k2": PoolCandidateTrace(key_id="k2", skipped=True, skip_type="cost_exhausted"),
}
summary = trace.build_summary()
assert summary["attempted"] == 0
assert summary["skipped_cooldown"] == 1
assert summary["skipped_cost"] == 1