Files
Aether/_deprecated_py_src/api/admin/providers/summary.py

933 lines
32 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
Provider 摘要与健康监控 API
"""
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Protocol
2025-12-10 20:52:44 +08:00
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import case, func
from sqlalchemy.orm import Session, load_only
2025-12-10 20:52:44 +08:00
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 get_pipeline
from src.config.constants import CacheTTL
2025-12-10 20:52:44 +08:00
from src.core.enums import ProviderBillingType
from src.core.exceptions import InvalidRequestException, NotFoundException
2025-12-10 20:52:44 +08:00
from src.core.logger import logger
from src.database import get_db
from src.models.admin_requests import (
ClaudeCodeAdvancedConfig,
FailoverRulesConfig,
PoolAdvancedConfig,
)
2025-12-10 20:52:44 +08:00
from src.models.database import (
Model,
Provider,
ProviderAPIKey,
ProviderEndpoint,
RequestCandidate,
)
from src.models.endpoint_models import (
EndpointHealthEvent,
EndpointHealthMonitor,
ProviderEndpointHealthMonitorResponse,
ProviderSummaryPageResponse,
2025-12-10 20:52:44 +08:00
ProviderUpdateRequest,
ProviderWithEndpointsSummary,
)
from src.services.cache.model_cache import ModelCacheService
from src.services.cache.provider_cache import ProviderCacheService
from src.utils.cache_decorator import cache_result
2025-12-10 20:52:44 +08:00
class _HasProviderSortFields(Protocol):
is_active: Any
provider_priority: Any
created_at: Any
2025-12-10 20:52:44 +08:00
router = APIRouter(tags=["Provider Summary"])
pipeline = get_pipeline()
2025-12-10 20:52:44 +08:00
def _provider_summary_ordering(provider_model: _HasProviderSortFields) -> tuple[Any, Any, Any]:
"""Provider 摘要列表排序:启用在前,其次按优先级与创建时间。"""
return (
case((provider_model.is_active == True, 0), else_=1).asc(),
provider_model.provider_priority.asc(),
provider_model.created_at.asc(),
)
@router.get("/summary", response_model=ProviderSummaryPageResponse)
2025-12-10 20:52:44 +08:00
async def get_providers_summary(
request: Request,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=10000),
search: str = Query("", description="按名称搜索"),
status: str = Query("all", description="all/active/inactive"),
api_format: str = Query("all", description="API 格式筛选"),
model_id: str = Query("all", description="全局模型 ID 筛选"),
2025-12-10 20:52:44 +08:00
db: Session = Depends(get_db),
) -> ProviderSummaryPageResponse:
"""获取提供商摘要信息(分页)"""
adapter = AdminProviderSummaryAdapter(
page=page,
page_size=page_size,
search=search,
status=status,
api_format=api_format,
model_id=model_id,
)
2025-12-10 20:52:44 +08:00
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/{provider_id}/summary", response_model=ProviderWithEndpointsSummary)
async def get_provider_summary(
provider_id: str,
request: Request,
db: Session = Depends(get_db),
) -> ProviderWithEndpointsSummary:
"""
获取单个提供商摘要信息
获取指定提供商的详细摘要信息包含端点密钥模型统计和健康状态
**路径参数**:
- `provider_id`: 提供商 ID
**返回字段**:
- `id`: 提供商 ID
- `name`: 提供商名称
- `description`: 描述信息
- `website`: 官网地址
- `provider_priority`: 优先级
- `is_active`: 是否启用
- `billing_type`: 计费类型
- `monthly_quota_usd`: 月度配额美元
- `monthly_used_usd`: 本月已使用金额美元
- `quota_reset_day`: 配额重置日期
- `quota_last_reset_at`: 上次配额重置时间
- `quota_expires_at`: 配额过期时间
- `timeout`: 默认请求超时
- `max_retries`: 默认最大重试次数
- `proxy`: 默认代理配置
- `total_endpoints`: 端点总数
- `active_endpoints`: 活跃端点数
- `total_keys`: 密钥总数
- `active_keys`: 活跃密钥数
- `total_models`: 模型总数
- `active_models`: 活跃模型数
- `avg_health_score`: 平均健康分数0-1
- `unhealthy_endpoints`: 不健康端点数健康分数 < 0.5
- `api_formats`: 支持的 API 格式列表
- `endpoint_health_details`: 端点健康详情包含 api_format, health_score, is_active, active_keys
- `created_at`: 创建时间
- `updated_at`: 更新时间
"""
adapter = AdminProviderDetailAdapter(provider_id=provider_id)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
2025-12-10 20:52:44 +08:00
@router.get("/{provider_id}/health-monitor", response_model=ProviderEndpointHealthMonitorResponse)
async def get_provider_health_monitor(
provider_id: str,
request: Request,
lookback_hours: int = Query(6, ge=1, le=72, description="回溯的小时数"),
per_endpoint_limit: int = Query(48, ge=10, le=200, description="每个端点的事件数量"),
db: Session = Depends(get_db),
) -> ProviderEndpointHealthMonitorResponse:
"""
获取提供商健康监控数据
获取指定提供商下所有端点的健康监控时间线包含请求成功率延迟错误信息等
**路径参数**:
- `provider_id`: 提供商 ID
**查询参数**:
- `lookback_hours`: 回溯的小时数范围 1-72默认为 6
- `per_endpoint_limit`: 每个端点返回的事件数量范围 10-200默认为 48
**返回字段**:
- `provider_id`: 提供商 ID
- `provider_name`: 提供商名称
- `generated_at`: 生成时间
- `endpoints`: 端点健康监控数据数组每项包含
- `endpoint_id`: 端点 ID
- `api_format`: API 格式
- `is_active`: 是否活跃
- `total_attempts`: 总请求次数
- `success_count`: 成功次数
- `failed_count`: 失败次数
- `skipped_count`: 跳过次数
- `success_rate`: 成功率0-1
- `last_event_at`: 最后事件时间
- `events`: 事件详情数组包含 timestamp, status, status_code, latency_ms, error_type, error_message
"""
2025-12-10 20:52:44 +08:00
adapter = AdminProviderHealthMonitorAdapter(
provider_id=provider_id,
lookback_hours=lookback_hours,
per_endpoint_limit=per_endpoint_limit,
)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.patch("/{provider_id}", response_model=ProviderWithEndpointsSummary)
async def update_provider_settings(
provider_id: str,
update_data: ProviderUpdateRequest,
request: Request,
db: Session = Depends(get_db),
) -> ProviderWithEndpointsSummary:
"""
更新提供商基础配置
更新提供商的基础配置信息如名称描述优先级等只需传入需要更新的字段
**路径参数**:
- `provider_id`: 提供商 ID
**请求体字段**所有字段可选:
- `name`: 提供商名称
- `description`: 描述信息
- `website`: 官网地址
- `provider_priority`: 优先级
- `is_active`: 是否启用
- `billing_type`: 计费类型
- `monthly_quota_usd`: 月度配额美元
- `quota_reset_day`: 配额重置日期
- `quota_expires_at`: 配额过期时间
- `timeout`: 默认请求超时
- `max_retries`: 默认最大重试次数
- `proxy`: 默认代理配置
**返回字段**: 返回更新后的提供商摘要信息 GET /summary 接口返回格式相同
"""
2025-12-10 20:52:44 +08:00
adapter = AdminUpdateProviderSettingsAdapter(provider_id=provider_id, update_data=update_data)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
def _extract_pool_advanced_from_config(
provider_config: dict[str, Any] | None,
*,
provider_id: str,
) -> PoolAdvancedConfig | None:
"""从 Provider.config 中安全提取通用号池配置。
优先查找 ``pool_advanced``回退查找 ``claude_code_advanced`` 中的号池字段
"""
cfg = provider_config or {}
raw = cfg.get("pool_advanced")
if raw is None:
return None
if isinstance(raw, PoolAdvancedConfig):
return raw
if not isinstance(raw, dict):
logger.warning(
"Provider {} 的 pool_advanced 类型无效: {},已忽略",
provider_id,
type(raw).__name__,
)
return None
try:
return PoolAdvancedConfig.model_validate(raw)
except Exception as exc:
logger.warning(
"Provider {} 的 pool_advanced 配置无效,已忽略: {}",
provider_id,
str(exc),
)
return None
def _extract_claude_code_advanced_from_config(
provider_config: dict[str, Any] | None,
*,
provider_id: str,
) -> ClaudeCodeAdvancedConfig | None:
"""从 Provider.config 中安全提取 Claude Code 高级配置。"""
raw_config = (provider_config or {}).get("claude_code_advanced")
if raw_config is None:
return None
if isinstance(raw_config, ClaudeCodeAdvancedConfig):
return raw_config
if not isinstance(raw_config, dict):
logger.warning(
"Provider {} 的 claude_code_advanced 类型无效: {},已忽略",
provider_id,
type(raw_config).__name__,
)
return None
try:
return ClaudeCodeAdvancedConfig.model_validate(raw_config)
except Exception as exc:
logger.warning(
"Provider {} 的 claude_code_advanced 配置无效,已忽略: {}",
provider_id,
str(exc),
)
return None
def _extract_failover_rules_from_config(
provider_config: dict[str, Any] | None,
*,
provider_id: str,
) -> FailoverRulesConfig | None:
"""从 Provider.config 中安全提取故障转移规则配置。"""
raw = (provider_config or {}).get("failover_rules")
if raw is None:
return None
if isinstance(raw, FailoverRulesConfig):
return raw
if not isinstance(raw, dict):
logger.warning(
"Provider {} 的 failover_rules 类型无效: {},已忽略",
provider_id,
type(raw).__name__,
)
return None
try:
return FailoverRulesConfig.model_validate(raw)
except Exception as exc:
logger.warning(
"Provider {} 的 failover_rules 配置无效,已忽略: {}",
provider_id,
str(exc),
)
return None
2025-12-10 20:52:44 +08:00
def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndpointsSummary:
endpoints = (
db.query(ProviderEndpoint)
.options(
load_only(
ProviderEndpoint.id,
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.is_active,
)
)
.filter(ProviderEndpoint.provider_id == provider.id)
.all()
)
2025-12-10 20:52:44 +08:00
key_stats = (
db.query(
2025-12-10 20:52:44 +08:00
func.count(ProviderAPIKey.id).label("total"),
func.sum(case((ProviderAPIKey.is_active == True, 1), else_=0)).label("active"),
)
.filter(ProviderAPIKey.provider_id == provider.id)
.first()
)
total_keys = int(key_stats.total or 0)
active_keys = int(key_stats.active or 0)
2025-12-10 20:52:44 +08:00
model_stats = (
db.query(
func.count(Model.id).label("total"),
func.sum(case((Model.is_active == True, 1), else_=0)).label("active"),
)
.filter(Model.provider_id == provider.id)
.first()
)
total_models = int(model_stats.total or 0)
2025-12-10 20:52:44 +08:00
active_models = int(model_stats.active or 0)
global_model_ids = [
row[0]
for row in db.query(Model.global_model_id)
.filter(
Model.provider_id == provider.id,
Model.is_active == True,
Model.global_model_id.isnot(None),
)
.distinct()
.all()
]
all_keys = (
db.query(ProviderAPIKey)
.options(
load_only(
ProviderAPIKey.id,
ProviderAPIKey.provider_id,
ProviderAPIKey.is_active,
ProviderAPIKey.api_formats,
ProviderAPIKey.health_by_format,
)
)
.filter(ProviderAPIKey.provider_id == provider.id)
.all()
)
return _compose_provider_summary(
provider=provider,
endpoints=endpoints,
all_keys=all_keys,
total_keys=total_keys,
active_keys=active_keys,
total_models=total_models,
active_models=active_models,
global_model_ids=global_model_ids,
)
2025-12-10 20:52:44 +08:00
def _compose_provider_summary(
*,
provider: Provider,
endpoints: list[ProviderEndpoint],
all_keys: list[ProviderAPIKey],
total_keys: int,
active_keys: int,
total_models: int,
active_models: int,
global_model_ids: list[Any],
) -> ProviderWithEndpointsSummary:
total_endpoints = len(endpoints)
active_endpoints = sum(1 for e in endpoints if e.is_active)
api_formats = [e.api_format for e in endpoints]
2025-12-10 20:52:44 +08:00
# 按 api_formats 分组 keys通过 api_formats 关联)
format_to_endpoint_id: dict[str, str] = {e.api_format: e.id for e in endpoints}
keys_by_endpoint: dict[str, list[ProviderAPIKey]] = {e.id: [] for e in endpoints}
2025-12-10 20:52:44 +08:00
for key in all_keys:
formats = key.api_formats or []
for fmt in formats:
endpoint_id = format_to_endpoint_id.get(fmt)
if endpoint_id:
keys_by_endpoint[endpoint_id].append(key)
2025-12-10 20:52:44 +08:00
endpoint_health_map: dict[str, float] = {}
for endpoint in endpoints:
keys = keys_by_endpoint.get(endpoint.id, [])
if keys:
api_fmt = endpoint.api_format
health_scores: list[float] = []
for k in keys:
health_by_format = k.health_by_format or {}
if api_fmt in health_by_format:
score = health_by_format[api_fmt].get("health_score")
if score is not None:
health_scores.append(float(score))
else:
health_scores.append(1.0)
2025-12-10 20:52:44 +08:00
avg_health = sum(health_scores) / len(health_scores) if health_scores else 1.0
endpoint_health_map[endpoint.id] = avg_health
else:
endpoint_health_map[endpoint.id] = 1.0
all_health_scores = list(endpoint_health_map.values())
avg_health_score = sum(all_health_scores) / len(all_health_scores) if all_health_scores else 1.0
unhealthy_endpoints = sum(1 for score in all_health_scores if score < 0.5)
active_keys_by_endpoint: dict[str, int] = {}
for endpoint_id, keys in keys_by_endpoint.items():
active_keys_by_endpoint[endpoint_id] = sum(1 for k in keys if k.is_active)
endpoint_health_details = [
{
"api_format": e.api_format,
"health_score": endpoint_health_map.get(e.id, 1.0),
"is_active": e.is_active,
"total_keys": len(keys_by_endpoint.get(e.id, [])),
2025-12-10 20:52:44 +08:00
"active_keys": active_keys_by_endpoint.get(e.id, 0),
}
for e in endpoints
]
provider_config_raw = provider.config
provider_config = provider_config_raw if isinstance(provider_config_raw, dict) else {}
if provider_config_raw is not None and not isinstance(provider_config_raw, dict):
logger.warning(
"Provider {} 的 config 类型无效: {},按空配置处理",
provider.id,
type(provider_config_raw).__name__,
)
# 检查是否配置了 Provider Ops余额监控等
provider_ops_config = provider_config.get("provider_ops")
ops_configured = bool(provider_ops_config)
ops_architecture_id = (
provider_ops_config.get("architecture_id") if provider_ops_config else None
)
claude_code_advanced = _extract_claude_code_advanced_from_config(
provider_config,
provider_id=str(provider.id),
)
pool_advanced = _extract_pool_advanced_from_config(
provider_config,
provider_id=str(provider.id),
)
failover_rules = _extract_failover_rules_from_config(
provider_config,
provider_id=str(provider.id),
)
2025-12-10 20:52:44 +08:00
return ProviderWithEndpointsSummary(
id=provider.id,
name=provider.name,
provider_type=getattr(provider, "provider_type", None),
2025-12-10 20:52:44 +08:00
description=provider.description,
website=provider.website,
provider_priority=provider.provider_priority,
keep_priority_on_conversion=provider.keep_priority_on_conversion,
enable_format_conversion=provider.enable_format_conversion,
2025-12-10 20:52:44 +08:00
is_active=provider.is_active,
billing_type=provider.billing_type.value if provider.billing_type else None,
monthly_quota_usd=provider.monthly_quota_usd,
monthly_used_usd=provider.monthly_used_usd,
quota_reset_day=provider.quota_reset_day,
quota_last_reset_at=provider.quota_last_reset_at,
quota_expires_at=provider.quota_expires_at,
max_retries=provider.max_retries,
proxy=provider.proxy,
stream_first_byte_timeout=provider.stream_first_byte_timeout,
request_timeout=provider.request_timeout,
claude_code_advanced=claude_code_advanced,
pool_advanced=pool_advanced,
failover_rules=failover_rules,
2025-12-10 20:52:44 +08:00
total_endpoints=total_endpoints,
active_endpoints=active_endpoints,
total_keys=total_keys,
active_keys=active_keys,
total_models=total_models,
active_models=active_models,
global_model_ids=global_model_ids,
2025-12-10 20:52:44 +08:00
avg_health_score=avg_health_score,
unhealthy_endpoints=unhealthy_endpoints,
api_formats=api_formats,
endpoint_health_details=endpoint_health_details,
ops_configured=ops_configured,
ops_architecture_id=ops_architecture_id,
2025-12-10 20:52:44 +08:00
created_at=provider.created_at,
updated_at=provider.updated_at,
)
def _build_provider_summaries_batch(
db: Session, providers: list[Provider]
) -> list[ProviderWithEndpointsSummary]:
if not providers:
return []
provider_ids = [provider.id for provider in providers]
endpoint_rows = (
db.query(ProviderEndpoint)
.options(
load_only(
ProviderEndpoint.id,
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.is_active,
)
)
.filter(ProviderEndpoint.provider_id.in_(provider_ids))
.all()
)
endpoints_by_provider: dict[str, list[ProviderEndpoint]] = {}
for endpoint in endpoint_rows:
endpoints_by_provider.setdefault(str(endpoint.provider_id), []).append(endpoint)
key_rows = (
db.query(ProviderAPIKey)
.options(
load_only(
ProviderAPIKey.id,
ProviderAPIKey.provider_id,
ProviderAPIKey.is_active,
ProviderAPIKey.api_formats,
ProviderAPIKey.health_by_format,
)
)
.filter(ProviderAPIKey.provider_id.in_(provider_ids))
.all()
)
keys_by_provider: dict[str, list[ProviderAPIKey]] = {}
for key in key_rows:
keys_by_provider.setdefault(str(key.provider_id), []).append(key)
key_stats_rows = (
db.query(
ProviderAPIKey.provider_id.label("provider_id"),
func.count(ProviderAPIKey.id).label("total"),
func.sum(case((ProviderAPIKey.is_active == True, 1), else_=0)).label("active"),
)
.filter(ProviderAPIKey.provider_id.in_(provider_ids))
.group_by(ProviderAPIKey.provider_id)
.all()
)
key_stats_by_provider: dict[str, dict[str, int]] = {
str(row.provider_id): {
"total": int(row.total or 0),
"active": int(row.active or 0),
}
for row in key_stats_rows
}
model_stats_rows = (
db.query(
Model.provider_id.label("provider_id"),
func.count(Model.id).label("total"),
func.sum(case((Model.is_active == True, 1), else_=0)).label("active"),
)
.filter(Model.provider_id.in_(provider_ids))
.group_by(Model.provider_id)
.all()
)
model_stats_by_provider: dict[str, dict[str, int]] = {
str(row.provider_id): {
"total": int(row.total or 0),
"active": int(row.active or 0),
}
for row in model_stats_rows
}
global_model_rows = (
db.query(Model.provider_id, Model.global_model_id)
.filter(
Model.provider_id.in_(provider_ids),
Model.is_active == True,
Model.global_model_id.isnot(None),
)
.distinct()
.all()
)
global_model_ids_by_provider: dict[str, list[Any]] = {}
for provider_id, global_model_id in global_model_rows:
global_model_ids_by_provider.setdefault(str(provider_id), []).append(global_model_id)
summaries: list[ProviderWithEndpointsSummary] = []
for provider in providers:
pid = str(provider.id)
key_stats = key_stats_by_provider.get(pid, {"total": 0, "active": 0})
model_stats = model_stats_by_provider.get(pid, {"total": 0, "active": 0})
summaries.append(
_compose_provider_summary(
provider=provider,
endpoints=endpoints_by_provider.get(pid, []),
all_keys=keys_by_provider.get(pid, []),
total_keys=key_stats["total"],
active_keys=key_stats["active"],
total_models=model_stats["total"],
active_models=model_stats["active"],
global_model_ids=global_model_ids_by_provider.get(pid, []),
)
)
return summaries
2025-12-10 20:52:44 +08:00
# -------- Adapters --------
@dataclass
class AdminProviderHealthMonitorAdapter(AdminApiAdapter):
provider_id: str
lookback_hours: int
per_endpoint_limit: int
@cache_result(
key_prefix="admin:providers:health-monitor",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
vary_by=["provider_id", "lookback_hours", "per_endpoint_limit"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise NotFoundException(f"Provider {self.provider_id} 不存在")
endpoints = (
db.query(ProviderEndpoint)
.options(
load_only(
ProviderEndpoint.id,
ProviderEndpoint.provider_id,
ProviderEndpoint.api_format,
ProviderEndpoint.is_active,
)
)
2025-12-10 20:52:44 +08:00
.filter(ProviderEndpoint.provider_id == self.provider_id)
.all()
)
now = datetime.now(timezone.utc)
since = now - timedelta(hours=self.lookback_hours)
endpoint_ids = [str(endpoint.id) for endpoint in endpoints]
2025-12-10 20:52:44 +08:00
if not endpoint_ids:
response = ProviderEndpointHealthMonitorResponse(
provider_id=provider.id,
provider_name=provider.name,
2025-12-10 20:52:44 +08:00
generated_at=now,
endpoints=[],
)
context.add_audit_metadata(
action="provider_health_monitor",
provider_id=self.provider_id,
endpoint_count=0,
lookback_hours=self.lookback_hours,
)
return response.model_dump()
2025-12-10 20:52:44 +08:00
ranked_attempts_subq = (
2025-12-10 20:52:44 +08:00
db.query(RequestCandidate)
.with_entities(
RequestCandidate.endpoint_id.label("endpoint_id"),
RequestCandidate.status.label("status"),
RequestCandidate.status_code.label("status_code"),
RequestCandidate.latency_ms.label("latency_ms"),
RequestCandidate.error_type.label("error_type"),
RequestCandidate.error_message.label("error_message"),
func.coalesce(
RequestCandidate.finished_at,
RequestCandidate.started_at,
RequestCandidate.created_at,
).label("event_timestamp"),
func.row_number()
.over(
partition_by=RequestCandidate.endpoint_id,
order_by=RequestCandidate.created_at.desc(),
)
.label("rn"),
)
2025-12-10 20:52:44 +08:00
.filter(
RequestCandidate.endpoint_id.in_(endpoint_ids),
RequestCandidate.created_at >= since,
)
.subquery()
)
attempt_rows = (
db.query(ranked_attempts_subq)
.filter(ranked_attempts_subq.c.rn <= self.per_endpoint_limit)
.order_by(
ranked_attempts_subq.c.endpoint_id.asc(),
ranked_attempts_subq.c.event_timestamp.asc(),
)
.all()
2025-12-10 20:52:44 +08:00
)
events_by_endpoint: dict[str, list[EndpointHealthEvent]] = {eid: [] for eid in endpoint_ids}
for row in attempt_rows:
endpoint_id = str(row.endpoint_id) if row.endpoint_id is not None else ""
if not endpoint_id or endpoint_id not in events_by_endpoint:
2025-12-10 20:52:44 +08:00
continue
events_by_endpoint[endpoint_id].append(
EndpointHealthEvent(
timestamp=row.event_timestamp,
status=row.status,
status_code=row.status_code,
latency_ms=row.latency_ms,
error_type=row.error_type,
error_message=row.error_message,
)
)
2025-12-10 20:52:44 +08:00
endpoint_monitors: list[EndpointHealthMonitor] = []
2025-12-10 20:52:44 +08:00
for endpoint in endpoints:
endpoint_id = str(endpoint.id)
events = events_by_endpoint.get(endpoint_id, [])
2025-12-10 20:52:44 +08:00
success_count = sum(1 for event in events if event.status == "success")
failed_count = sum(1 for event in events if event.status == "failed")
skipped_count = sum(1 for event in events if event.status == "skipped")
total_attempts = len(events)
success_rate = success_count / total_attempts if total_attempts else 1.0
last_event_at = events[-1].timestamp if events else None
endpoint_monitors.append(
EndpointHealthMonitor(
endpoint_id=endpoint.id,
api_format=endpoint.api_format,
is_active=endpoint.is_active,
total_attempts=total_attempts,
success_count=success_count,
failed_count=failed_count,
skipped_count=skipped_count,
success_rate=success_rate,
last_event_at=last_event_at,
events=events,
)
)
response = ProviderEndpointHealthMonitorResponse(
provider_id=provider.id,
provider_name=provider.name,
2025-12-10 20:52:44 +08:00
generated_at=now,
endpoints=endpoint_monitors,
)
context.add_audit_metadata(
action="provider_health_monitor",
provider_id=self.provider_id,
endpoint_count=len(endpoint_monitors),
lookback_hours=self.lookback_hours,
per_endpoint_limit=self.per_endpoint_limit,
)
return response.model_dump()
2025-12-10 20:52:44 +08:00
@dataclass
2025-12-10 20:52:44 +08:00
class AdminProviderSummaryAdapter(AdminApiAdapter):
page: int = 1
page_size: int = 20
search: str = ""
status: str = "all"
api_format: str = "all"
model_id: str = "all"
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
db = context.db
query = db.query(Provider)
# 搜索筛选
if self.search.strip():
keywords = self.search.strip().lower().split()
for kw in keywords:
query = query.filter(func.lower(Provider.name).contains(kw))
# 状态筛选
if self.status == "active":
query = query.filter(Provider.is_active == True)
elif self.status == "inactive":
query = query.filter(Provider.is_active == False)
# API 格式筛选
if self.api_format != "all":
query = query.filter(
Provider.id.in_(
db.query(ProviderEndpoint.provider_id)
.filter(ProviderEndpoint.api_format == self.api_format)
.distinct()
)
)
# 全局模型 ID 筛选
if self.model_id != "all":
query = query.filter(
Provider.id.in_(
db.query(Model.provider_id)
.filter(
Model.global_model_id == self.model_id,
Model.is_active == True,
)
.distinct()
)
)
total = query.count()
2025-12-10 20:52:44 +08:00
providers = (
query.order_by(*_provider_summary_ordering(Provider))
.offset((self.page - 1) * self.page_size)
.limit(self.page_size)
2025-12-10 20:52:44 +08:00
.all()
)
items = _build_provider_summaries_batch(db, providers)
return ProviderSummaryPageResponse(
total=total,
page=self.page,
page_size=self.page_size,
items=items,
).model_dump()
@dataclass
class AdminProviderDetailAdapter(AdminApiAdapter):
provider_id: str
@cache_result(
key_prefix="admin:providers:summary:detail",
ttl=CacheTTL.ADMIN_USAGE_RECORDS,
user_specific=False,
vary_by=["provider_id"],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise NotFoundException(f"Provider {self.provider_id} not found")
return _build_provider_summary(db, provider).model_dump()
2025-12-10 20:52:44 +08:00
@dataclass
class AdminUpdateProviderSettingsAdapter(AdminApiAdapter):
provider_id: str
update_data: ProviderUpdateRequest
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
2025-12-10 20:52:44 +08:00
db = context.db
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
if not provider:
raise NotFoundException("Provider not found", "provider")
update_dict = self.update_data.model_dump(exclude_unset=True)
if "claude_code_advanced" in update_dict:
claude_advanced = update_dict.pop("claude_code_advanced")
provider_type = str(getattr(provider, "provider_type", "") or "").strip().lower()
if claude_advanced is not None and provider_type != "claude_code":
raise InvalidRequestException(
"claude_code_advanced 仅适用于 provider_type=claude_code"
)
provider_config = dict(provider.config or {})
if claude_advanced is None:
provider_config.pop("claude_code_advanced", None)
else:
provider_config["claude_code_advanced"] = dict(claude_advanced)
update_dict["config"] = provider_config or None
if "pool_advanced" in update_dict:
pool_advanced = update_dict.pop("pool_advanced")
provider_config = dict(update_dict.get("config") or provider.config or {})
if pool_advanced is None:
provider_config.pop("pool_advanced", None)
else:
provider_config["pool_advanced"] = dict(pool_advanced)
update_dict["config"] = provider_config or None
2025-12-10 20:52:44 +08:00
if "billing_type" in update_dict and update_dict["billing_type"] is not None:
update_dict["billing_type"] = ProviderBillingType(update_dict["billing_type"])
for key, value in update_dict.items():
setattr(provider, key, value)
provider.updated_at = datetime.now(timezone.utc)
2025-12-10 20:52:44 +08:00
db.commit()
db.refresh(provider)
admin_name = context.user.username if context.user else "admin"
logger.info(f"Provider {provider.name} updated by {admin_name}: {update_dict}")
# 缓存失效
affects_model_visibility = {"is_active", "enable_format_conversion"} & update_dict.keys()
if affects_model_visibility:
await invalidate_models_list_cache()
if "is_active" in update_dict:
await ModelCacheService.invalidate_all_resolve_cache()
if "billing_type" in update_dict:
await ProviderCacheService.invalidate_provider_cache(provider.id)
2025-12-10 20:52:44 +08:00
return _build_provider_summary(db, provider)