mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user