refactor: 全局适配 ApiFamily/EndpointKind 结构化标识体系

将新的 (ApiFamily, EndpointKind) / `family:kind` 签名体系应用到整个代码库:
- API Handlers: 所有 adapter/handler 使用新的签名格式
- Services: provider, model, usage, cache, auth 等服务层适配
- Database: ProviderEndpoint 新增 api_family/endpoint_kind 字段
- Frontend: Provider 管理、Usage 表格等组件适配
- Tests: 更新所有相关测试用例
This commit is contained in:
fawney19
2026-02-01 17:28:00 +08:00
parent c246ccfc91
commit 7b66505634
219 changed files with 4732 additions and 2545 deletions

View File

@@ -21,7 +21,6 @@ from sqlalchemy.orm import Session
from src.core.logger import logger
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint, RequestCandidate
# 缓存配置
CACHE_TTL_SECONDS = 30 # 缓存 30 秒
CACHE_KEY_PREFIX = "health:endpoint:"
@@ -31,6 +30,7 @@ def _get_redis_client() -> Any:
"""获取 Redis 客户端,失败返回 None"""
try:
from src.clients.redis_client import redis_client
return redis_client
except Exception:
return None
@@ -77,15 +77,19 @@ class EndpointHealthService:
# 批量查询所有密钥(通过 provider_id 关联)
all_keys = (
db.query(ProviderAPIKey)
.filter(ProviderAPIKey.provider_id.in_(all_provider_ids))
.all()
) if all_provider_ids else []
(
db.query(ProviderAPIKey)
.filter(ProviderAPIKey.provider_id.in_(all_provider_ids))
.all()
)
if all_provider_ids
else []
)
# 按 api_format 分组密钥(通过 api_formats 字段)
keys_by_format: dict[str, list[ProviderAPIKey]] = defaultdict(list)
for key in all_keys:
for fmt in (key.api_formats or []):
for fmt in key.api_formats or []:
keys_by_format[fmt].append(key)
# 按 API 格式聚合
@@ -157,11 +161,14 @@ class EndpointHealthService:
result = []
for api_format, stats in format_stats.items():
timeline_data = timeline_data_map.get(api_format, {
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
})
timeline_data = timeline_data_map.get(
api_format,
{
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
},
)
timeline = timeline_data["timeline"]
time_range_start = timeline_data.get("time_range_start")
time_range_end = timeline_data.get("time_range_end")
@@ -271,28 +278,22 @@ class EndpointHealthService:
final_statuses = ["success", "failed", "skipped"]
segment_expr = func.floor(
func.extract('epoch', RequestCandidate.created_at - start_time) / segment_seconds
).label('segment_idx')
func.extract("epoch", RequestCandidate.created_at - start_time) / segment_seconds
).label("segment_idx")
candidate_stats = (
db.query(
RequestCandidate.key_id,
segment_expr,
func.count(RequestCandidate.id).label('total_count'),
func.sum(
case(
(RequestCandidate.status == "success", 1),
else_=0
)
).label('success_count'),
func.sum(
case(
(RequestCandidate.status == "failed", 1),
else_=0
)
).label('failed_count'),
func.min(RequestCandidate.created_at).label('min_time'),
func.max(RequestCandidate.created_at).label('max_time'),
func.count(RequestCandidate.id).label("total_count"),
func.sum(case((RequestCandidate.status == "success", 1), else_=0)).label(
"success_count"
),
func.sum(case((RequestCandidate.status == "failed", 1), else_=0)).label(
"failed_count"
),
func.min(RequestCandidate.created_at).label("min_time"),
func.max(RequestCandidate.created_at).label("max_time"),
)
.filter(
RequestCandidate.key_id.in_(all_key_ids),
@@ -311,13 +312,17 @@ class EndpointHealthService:
key_to_format[key_id] = api_format
# 按 api_format 和 segment 聚合数据
format_segment_data: dict[str, dict[int, dict]] = defaultdict(lambda: defaultdict(lambda: {
"total": 0,
"success": 0,
"failed": 0,
"min_time": None,
"max_time": None,
}))
format_segment_data: dict[str, dict[int, dict]] = defaultdict(
lambda: defaultdict(
lambda: {
"total": 0,
"success": 0,
"failed": 0,
"min_time": None,
"max_time": None,
}
)
)
for row in candidate_stats:
key_id = row.key_id
@@ -454,24 +459,34 @@ class EndpointHealthService:
db, format_key_mapping, now, lookback_hours, segments
)
return result.get("_single", {
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
})
return result.get(
"_single",
{
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
},
)
@staticmethod
def _format_display_name(api_format: str) -> str:
"""格式化 API 格式的显示名称"""
format_names = {
"CLAUDE": "Claude API",
"CLAUDE_CLI": "Claude CLI",
"CLAUDE_COMPATIBLE": "Claude 兼容",
"OPENAI": "OpenAI API",
"OPENAI_CLI": "OpenAI CLI",
"OPENAI_COMPATIBLE": "OpenAI 兼容",
}
return format_names.get(api_format, api_format)
raw = str(api_format or "").strip()
normalized = raw.lower()
if ":" not in normalized:
return raw or api_format
fam, kind = normalized.split(":", 1)
fam_label = {"claude": "Claude", "openai": "OpenAI", "gemini": "Gemini"}.get(fam, fam)
kind_label = {
"chat": "",
"cli": "CLI",
"video": "Video",
"image": "Image",
}.get(kind, kind)
if not kind_label:
return fam_label
return f"{fam_label} {kind_label}"
@staticmethod
def _get_from_cache(key: str) -> list[dict[str, Any]] | None:

View File

@@ -230,9 +230,9 @@ class HealthMonitor:
if state == CircuitState.HALF_OPEN:
# 半开状态:记录成功
circuit_data["half_open_successes"] = int(
circuit_data.get("half_open_successes") or 0
) + 1
circuit_data["half_open_successes"] = (
int(circuit_data.get("half_open_successes") or 0) + 1
)
if circuit_data["half_open_successes"] >= cls.HALF_OPEN_SUCCESS_THRESHOLD:
# 达到成功阈值,关闭熔断器
@@ -357,9 +357,9 @@ class HealthMonitor:
if state == CircuitState.HALF_OPEN:
# 半开状态:记录失败
circuit_data["half_open_failures"] = int(
circuit_data.get("half_open_failures") or 0
) + 1
circuit_data["half_open_failures"] = (
int(circuit_data.get("half_open_failures") or 0) + 1
)
if circuit_data["half_open_failures"] >= cls.HALF_OPEN_FAILURE_THRESHOLD:
# 达到失败阈值,重新打开熔断器
@@ -581,9 +581,7 @@ class HealthMonitor:
return cls._get_status_from_circuit_data(circuit_data)
@classmethod
def _get_status_from_circuit_data(
cls, circuit_data: dict[str, Any]
) -> tuple[bool, str | None]:
def _get_status_from_circuit_data(cls, circuit_data: dict[str, Any]) -> tuple[bool, str | None]:
"""从熔断器数据获取状态描述"""
if not circuit_data.get("open"):
return True, None
@@ -662,9 +660,7 @@ class HealthMonitor:
result["health_score"] = float(health_data.get("health_score") or 1.0)
result["error_rate"] = cls._calculate_error_rate_from_window(window, now_ts)
result["window_size"] = len(valid_window)
result["consecutive_failures"] = int(
health_data.get("consecutive_failures") or 0
)
result["consecutive_failures"] = int(health_data.get("consecutive_failures") or 0)
result["last_failure_at"] = health_data.get("last_failure_at")
result["circuit_breaker"] = {
"state": cls._get_circuit_state_from_data(circuit_data, now),
@@ -678,7 +674,7 @@ class HealthMonitor:
else:
# 返回所有格式的健康度数据
formats_health = {}
for fmt in (key.api_formats or []):
for fmt in key.api_formats or []:
health_data = health_by_format.get(fmt, _default_health_data())
circuit_data = circuit_by_format.get(fmt, _default_circuit_data())
window = health_data.get("request_results_window") or []
@@ -688,9 +684,7 @@ class HealthMonitor:
"health_score": float(health_data.get("health_score") or 1.0),
"error_rate": cls._calculate_error_rate_from_window(window, now_ts),
"window_size": len(valid_window),
"consecutive_failures": int(
health_data.get("consecutive_failures") or 0
),
"consecutive_failures": int(health_data.get("consecutive_failures") or 0),
"last_failure_at": health_data.get("last_failure_at"),
"circuit_breaker": {
"state": cls._get_circuit_state_from_data(circuit_data, now),
@@ -701,9 +695,7 @@ class HealthMonitor:
"half_open_successes": int(
circuit_data.get("half_open_successes") or 0
),
"half_open_failures": int(
circuit_data.get("half_open_failures") or 0
),
"half_open_failures": int(circuit_data.get("half_open_failures") or 0),
},
}
@@ -711,9 +703,7 @@ class HealthMonitor:
# 计算整体健康度(取最低值)
if formats_health:
result["health_score"] = min(
h["health_score"] for h in formats_health.values()
)
result["health_score"] = min(h["health_score"] for h in formats_health.values())
result["any_circuit_open"] = any(
h["circuit_breaker"]["open"] for h in formats_health.values()
)
@@ -731,9 +721,7 @@ class HealthMonitor:
def get_endpoint_health(cls, db: Session, endpoint_id: str) -> dict[str, Any] | None:
"""获取 Endpoint 健康状态"""
try:
endpoint = (
db.query(ProviderEndpoint).filter(ProviderEndpoint.id == endpoint_id).first()
)
endpoint = db.query(ProviderEndpoint).filter(ProviderEndpoint.id == endpoint_id).first()
if not endpoint:
return None
@@ -782,6 +770,12 @@ class HealthMonitor:
db.rollback()
return False
@classmethod
def reset_open_circuit_count(cls) -> None:
"""重置进程级别的熔断计数器(批量恢复后调用)。"""
cls._open_circuit_keys = 0
health_open_circuits.set(0)
@classmethod
def manually_enable(cls, db: Session, key_id: str | None = None) -> bool:
"""手动启用 Key"""
@@ -915,18 +909,14 @@ class HealthMonitor:
# ==================== 便捷方法 ====================
@classmethod
def get_health_score(
cls, key: ProviderAPIKey, api_format: str | None = None
) -> float:
def get_health_score(cls, key: ProviderAPIKey, api_format: str | None = None) -> float:
"""获取指定格式的健康度分数"""
if not api_format:
# 返回所有格式中的最低健康度
health_by_format = key.health_by_format or {}
if not health_by_format:
return 1.0
return min(
float(h.get("health_score") or 1.0) for h in health_by_format.values()
)
return min(float(h.get("health_score") or 1.0) for h in health_by_format.values())
health_data = cls._get_health_data(key, api_format)
return float(health_data.get("health_score") or 1.0)