feat(pool,scheduling): 号池调度维度、配额冷却机制与管理后台重构

- 新增 scheduling_dimensions 模块,为每个 Key 计算多维调度状态(手动/冷却/熔断/成本/健康)
- 新增 quota_cooldown 模块,统一判定 Key 的有效冷却原因
- Pool 管理后台 API 扩展 Key 详情字段(调度状态/维度/配额/OAuth 信息)
- 前端 Pool 管理页面重写,支持调度状态展示、批量清理封禁 Key
- Handler 基类增加请求调度元数据采集,stream telemetry 增强
- 请求时间线组件增强,支持 attempted 候选展示
- Kiro OAuth 凭证导入解析改进
- 新增 usage 表 provider_key 索引迁移
- 补充调度维度、配额冷却、候选枚举等单元测试

Closes #197

Co-authored-by: AAEE86 <33052466+AAEE86@users.noreply.github.com>
This commit is contained in:
fawney19
2026-03-03 09:22:20 +08:00
parent f787b1b02a
commit 11997c024e
40 changed files with 4419 additions and 823 deletions

View File

@@ -0,0 +1,82 @@
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import MagicMock
from src.api.admin.monitoring.trace import AdminGetRequestTraceAdapter
from src.services.request.candidate import RequestCandidateService
def _candidate(*, status: str, latency_ms: int | None = None) -> SimpleNamespace:
now = datetime.now(timezone.utc)
return SimpleNamespace(
id=f"cand-{status}",
request_id="req-1",
candidate_index=0,
retry_index=0,
provider_id=None,
endpoint_id=None,
key_id=None,
required_capabilities=None,
status=status,
skip_reason=None,
is_cached=False,
status_code=None,
error_type=None,
error_message=None,
latency_ms=latency_ms,
concurrent_requests=None,
extra_data=None,
created_at=now,
started_at=None,
finished_at=None,
)
def _context() -> SimpleNamespace:
return SimpleNamespace(
db=MagicMock(),
add_audit_metadata=lambda **_: None,
)
def test_trace_prefers_attempted_subset(monkeypatch: object) -> None:
candidates = [
_candidate(status="available"),
_candidate(status="unused"),
_candidate(status="failed", latency_ms=123),
]
monkeypatch.setattr(
RequestCandidateService,
"get_candidates_by_request_id",
lambda _db, _request_id: candidates,
)
adapter = AdminGetRequestTraceAdapter(request_id="req-1")
response = asyncio.run(adapter.handle(_context()))
assert response.total_candidates == 1
assert len(response.candidates) == 1
assert response.candidates[0].status == "failed"
assert response.total_latency_ms == 123
def test_trace_falls_back_to_all_when_no_attempted(monkeypatch: object) -> None:
candidates = [
_candidate(status="available"),
_candidate(status="unused"),
]
monkeypatch.setattr(
RequestCandidateService,
"get_candidates_by_request_id",
lambda _db, _request_id: candidates,
)
adapter = AdminGetRequestTraceAdapter(request_id="req-1")
response = asyncio.run(adapter.handle(_context()))
assert response.total_candidates == 2
assert len(response.candidates) == 2
assert {c.status for c in response.candidates} == {"available", "unused"}