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

@@ -2,20 +2,20 @@
Key RPM 限制管理 API
"""
from typing import Any
from dataclasses import dataclass
from typing import Any
from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import ApiRequestPipeline
from src.core.exceptions import NotFoundException
from src.database import get_db
from src.models.database import ProviderAPIKey
from src.models.endpoint_models import KeyRpmStatusResponse
from src.services.rate_limit.concurrency_manager import get_concurrency_manager
from src.api.base.context import ApiRequestContext
router = APIRouter(tags=["RPM Control"])
pipeline = ApiRequestPipeline()

View File

@@ -4,16 +4,17 @@ Endpoint 健康监控 API
from __future__ import annotations
from typing import Any
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import func
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.pipeline import ApiRequestPipeline
from src.core.exceptions import NotFoundException
from src.core.logger import logger
@@ -27,10 +28,16 @@ from src.models.endpoint_models import (
HealthSummaryResponse,
)
from src.services.health.endpoint import EndpointHealthService
from src.services.health.monitor import health_monitor
from src.api.base.context import ApiRequestContext
from src.services.health.monitor import HealthMonitor, health_monitor
router = APIRouter(tags=["Endpoint Health"])
def _format_str(api_format_enum: Any) -> str:
"""将 DB 查询返回的 api_format可能是 enum 或 str统一转为 str。"""
return api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
pipeline = ApiRequestPipeline()
@@ -234,8 +241,6 @@ class AdminEndpointHealthStatusAdapter(AdminApiAdapter):
lookback_hours: int
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
from src.services.health.endpoint import EndpointHealthService
db = context.db
# 使用共享服务获取健康状态(管理员视图)
@@ -265,30 +270,7 @@ class AdminApiFormatHealthMonitorAdapter(AdminApiAdapter):
now = datetime.now(timezone.utc)
since = now - timedelta(hours=self.lookback_hours)
# 1. 获取所有活跃的 API 格式及其 Provider 数量
active_formats = (
db.query(
ProviderEndpoint.api_format,
func.count(func.distinct(ProviderEndpoint.provider_id)).label("provider_count"),
)
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
.filter(
ProviderEndpoint.is_active.is_(True),
Provider.is_active.is_(True),
)
.group_by(ProviderEndpoint.api_format)
.all()
)
# 构建所有格式的 provider_count 映射
all_formats: dict[str, int] = {}
for api_format_enum, provider_count in active_formats:
api_format = (
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
)
all_formats[api_format] = provider_count
# 1.1 建立每个 API 格式对应的 Endpoint ID 列表(用于时间线生成),并收集活跃的 provider+format 组合
# 1. 单次查询获取所有活跃 endpoint 行,在内存中聚合 provider_count / endpoint_map
endpoint_rows = (
db.query(ProviderEndpoint.api_format, ProviderEndpoint.id, ProviderEndpoint.provider_id)
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
@@ -298,14 +280,20 @@ class AdminApiFormatHealthMonitorAdapter(AdminApiAdapter):
)
.all()
)
all_formats: dict[str, int] = {} # api_format -> distinct provider count
endpoint_map: dict[str, list[str]] = defaultdict(list)
active_provider_formats: set[tuple[str, str]] = set()
_provider_sets: dict[str, set[str]] = defaultdict(set)
for api_format_enum, endpoint_id, provider_id in endpoint_rows:
api_format = (
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
)
endpoint_map[api_format].append(endpoint_id)
active_provider_formats.add((str(provider_id), api_format))
fmt = _format_str(api_format_enum)
endpoint_map[fmt].append(endpoint_id)
_provider_sets[fmt].add(str(provider_id))
active_provider_formats.add((str(provider_id), fmt))
for fmt, pids in _provider_sets.items():
all_formats[fmt] = len(pids)
# 1.2 统计每个 API 格式可用的活跃 Key 数量Key 属于 Provider通过 api_formats 关联格式)
key_counts: dict[str, int] = {}
@@ -321,7 +309,7 @@ class AdminApiFormatHealthMonitorAdapter(AdminApiAdapter):
)
for provider_id, api_formats in active_provider_keys:
pid = str(provider_id)
for fmt in (api_formats or []):
for fmt in api_formats or []:
if (pid, fmt) not in active_provider_formats:
continue
key_counts[fmt] = key_counts.get(fmt, 0) + 1
@@ -347,12 +335,10 @@ class AdminApiFormatHealthMonitorAdapter(AdminApiAdapter):
# 构建每个格式的状态统计
status_counts: dict[str, dict[str, int]] = {}
for api_format_enum, status, count in status_counts_query:
api_format = (
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
)
if api_format not in status_counts:
status_counts[api_format] = {"success": 0, "failed": 0, "skipped": 0}
status_counts[api_format][status] = count
fmt = _format_str(api_format_enum)
if fmt not in status_counts:
status_counts[fmt] = {"success": 0, "failed": 0, "skipped": 0}
status_counts[fmt][status] = count
# 3. 获取最近一段时间的 RequestCandidate限制数量
# 使用上面定义的 final_statuses排除中间状态
@@ -376,15 +362,13 @@ class AdminApiFormatHealthMonitorAdapter(AdminApiAdapter):
grouped_attempts: dict[str, list[RequestCandidate]] = {}
for attempt, api_format_enum, provider_id in rows:
api_format = (
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
)
if api_format not in grouped_attempts:
grouped_attempts[api_format] = []
fmt = _format_str(api_format_enum)
if fmt not in grouped_attempts:
grouped_attempts[fmt] = []
# 只保留每个 API 格式最近 per_format_limit 条记录
if len(grouped_attempts[api_format]) < self.per_format_limit:
grouped_attempts[api_format].append(attempt)
if len(grouped_attempts[fmt]) < self.per_format_limit:
grouped_attempts[fmt].append(attempt)
# 4. 为所有活跃格式生成监控数据(包括没有请求记录的)
monitors: list[ApiFormatHealthMonitor] = []
@@ -551,17 +535,22 @@ class AdminRecoverAllKeysHealthAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
# 查找所有有熔断格式的 Key检查 circuit_breaker_by_format JSON 字段)
all_keys = db.query(ProviderAPIKey).all()
# 粗过滤:仅加载 circuit_breaker_by_format 非空的 Key避免全表扫描
candidates = (
db.query(ProviderAPIKey)
.filter(
ProviderAPIKey.circuit_breaker_by_format.isnot(None),
ProviderAPIKey.circuit_breaker_by_format != "{}",
)
.all()
)
# 筛选有任何格式熔断的 Key
circuit_open_keys = []
for key in all_keys:
circuit_by_format = key.circuit_breaker_by_format or {}
for fmt, circuit_data in circuit_by_format.items():
if circuit_data.get("open"):
circuit_open_keys.append(key)
break
# 精确筛选有任何格式熔断的 Key
circuit_open_keys = [
key
for key in candidates
if any(cb.get("open") for cb in (key.circuit_breaker_by_format or {}).values())
]
if not circuit_open_keys:
return {
@@ -586,11 +575,8 @@ class AdminRecoverAllKeysHealthAdapter(AdminApiAdapter):
db.commit()
# 重置健康监控器的计数
from src.services.health.monitor import HealthMonitor, health_open_circuits
HealthMonitor._open_circuit_keys = 0
health_open_circuits.set(0)
# 重置健康监控器的熔断计数
HealthMonitor.reset_open_circuit_count()
logger.info(f"管理员批量恢复 {len(recovered_keys)} 个 Key 的健康状态")

View File

@@ -4,16 +4,17 @@ Provider API Keys 管理
from __future__ import annotations
from typing import Any
import json
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.models_service import invalidate_models_list_cache
from src.api.base.pipeline import ApiRequestPipeline
from src.core.crypto import crypto_service
@@ -28,7 +29,6 @@ from src.models.endpoint_models import (
EndpointAPIKeyUpdate,
)
from src.services.cache.provider_cache import ProviderCacheService
from src.api.base.context import ApiRequestContext
router = APIRouter(tags=["Provider Keys"])
pipeline = ApiRequestPipeline()
@@ -261,7 +261,9 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
update_data["api_key"] = crypto_service.encrypt(update_data["api_key"])
# 加密 auth_config包含敏感的 Service Account 凭证)
if "auth_config" in update_data and update_data["auth_config"]:
update_data["auth_config"] = crypto_service.encrypt(json.dumps(update_data["auth_config"]))
update_data["auth_config"] = crypto_service.encrypt(
json.dumps(update_data["auth_config"])
)
# 特殊处理 rpm_limit需要区分"未提供"和"显式设置为 null"
if "rpm_limit" in self.key_data.model_fields_set:
@@ -410,10 +412,10 @@ class AdminRevealEndpointKeyAdapter(AdminApiAdapter):
# 检查是否是新格式的占位符(表示 auth_config 丢失)
if decrypted_key == "__placeholder__":
logger.error(f"Vertex AI Key 缺少 auth_config: ID={self.key_id}")
raise InvalidRequestException(
"认证配置丢失,请重新添加该密钥。"
)
logger.info(f"[REVEAL] 查看完整 Key (legacy vertex_ai): ID={self.key_id}, Name={key.name}")
raise InvalidRequestException("认证配置丢失,请重新添加该密钥。")
logger.info(
f"[REVEAL] 查看完整 Key (legacy vertex_ai): ID={self.key_id}, Name={key.name}"
)
return {"auth_type": "vertex_ai", "auth_config": decrypted_key}
except InvalidRequestException:
raise
@@ -760,10 +762,14 @@ class AdminCreateProviderKeyAdapter(AdminApiAdapter):
auto_fetch_models=self.key_data.auto_fetch_models,
locked_models=self.key_data.locked_models if self.key_data.locked_models else None,
model_include_patterns=(
self.key_data.model_include_patterns if self.key_data.model_include_patterns else None
self.key_data.model_include_patterns
if self.key_data.model_include_patterns
else None
),
model_exclude_patterns=(
self.key_data.model_exclude_patterns if self.key_data.model_exclude_patterns else None
self.key_data.model_exclude_patterns
if self.key_data.model_exclude_patterns
else None
),
request_count=0,
success_count=0,

View File

@@ -4,10 +4,10 @@ ProviderEndpoint CRUD 管理 API
from __future__ import annotations
from typing import Any
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import and_
@@ -15,13 +15,14 @@ from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.api.base.models_service import invalidate_models_list_cache
from src.api.base.pipeline import ApiRequestPipeline
from src.core.api_format.signature import parse_signature_key
from src.core.exceptions import InvalidRequestException, NotFoundException
from src.core.logger import logger
from src.database import get_db
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
from src.api.base.context import ApiRequestContext
from src.models.endpoint_models import (
ProviderEndpointCreate,
ProviderEndpointResponse,
@@ -243,7 +244,7 @@ class AdminListProviderEndpointsAdapter(AdminApiAdapter):
total_keys_map: dict[str, int] = {}
active_keys_map: dict[str, int] = {}
for api_formats, is_active in keys:
for fmt in (api_formats or []):
for fmt in api_formats or []:
total_keys_map[fmt] = total_keys_map.get(fmt, 0) + 1
if is_active:
active_keys_map[fmt] = active_keys_map.get(fmt, 0) + 1
@@ -299,11 +300,18 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
)
now = datetime.now(timezone.utc)
sig = parse_signature_key(self.endpoint_data.api_format)
api_family = sig.api_family.value
endpoint_kind = sig.endpoint_kind.value
# 使用归一化后的 signature key确保格式一致性
normalized_api_format = sig.key
new_endpoint = ProviderEndpoint(
id=str(uuid.uuid4()),
provider_id=self.provider_id,
api_format=self.endpoint_data.api_format,
api_format=normalized_api_format,
api_family=api_family,
endpoint_kind=endpoint_kind,
base_url=self.endpoint_data.base_url,
custom_path=self.endpoint_data.custom_path,
header_rules=self.endpoint_data.header_rules,
@@ -323,7 +331,9 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
# 清除 /v1/models 列表缓存
await invalidate_models_list_cache()
logger.info(f"[OK] 创建 Endpoint: Provider={provider.name}, Format={self.endpoint_data.api_format}, ID={new_endpoint.id}")
logger.info(
f"[OK] 创建 Endpoint: Provider={provider.name}, Format={self.endpoint_data.api_format}, ID={new_endpoint.id}"
)
endpoint_dict = {
k: v
@@ -417,6 +427,11 @@ class AdminUpdateProviderEndpointAdapter(AdminApiAdapter):
# proxy 为 None 时保留,用于清除代理配置
for field, value in update_data.items():
setattr(endpoint, field, value)
# Phase 3/4: 自动维护新架构字段,确保新增/历史数据都能被调度器按 family/kind 查询
sig = parse_signature_key(endpoint.api_format)
endpoint.api_family = sig.api_family.value
endpoint.endpoint_kind = sig.endpoint_kind.value
endpoint.updated_at = datetime.now(timezone.utc)
db.commit()
@@ -426,10 +441,14 @@ class AdminUpdateProviderEndpointAdapter(AdminApiAdapter):
await invalidate_models_list_cache()
provider = db.query(Provider).filter(Provider.id == endpoint.provider_id).first()
logger.info(f"[OK] 更新 Endpoint: ID={self.endpoint_id}, Updates={list(update_data.keys())}")
logger.info(
f"[OK] 更新 Endpoint: ID={self.endpoint_id}, Updates={list(update_data.keys())}"
)
endpoint_format = (
endpoint.api_format if isinstance(endpoint.api_format, str) else endpoint.api_format.value
endpoint.api_format
if isinstance(endpoint.api_format, str)
else endpoint.api_format.value
)
keys = (
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
@@ -472,7 +491,9 @@ class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
endpoint_format = (
endpoint.api_format if isinstance(endpoint.api_format, str) else endpoint.api_format.value
endpoint.api_format
if isinstance(endpoint.api_format, str)
else endpoint.api_format.value
)
# 查询包含该格式的所有 Key并从 api_formats 中移除该格式
@@ -488,7 +509,7 @@ class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
# 移除该格式
new_formats = [f for f in key.api_formats if f != endpoint_format]
key.api_formats = new_formats if new_formats else []
flag_modified(key, 'api_formats')
flag_modified(key, "api_formats")
db.delete(endpoint)
db.commit()