mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
24
_deprecated_py_src/api/admin/endpoints/__init__.py
Normal file
24
_deprecated_py_src/api/admin/endpoints/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Endpoint management API routers."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .concurrency import router as concurrency_router
|
||||
from .health import router as health_router
|
||||
from .keys import router as keys_router
|
||||
from .routes import router as routes_router
|
||||
|
||||
router = APIRouter(prefix="/api/admin/endpoints", tags=["Admin - Endpoints"])
|
||||
|
||||
# Endpoint CRUD
|
||||
router.include_router(routes_router)
|
||||
|
||||
# Endpoint Keys management
|
||||
router.include_router(keys_router)
|
||||
|
||||
# Health monitoring
|
||||
router.include_router(health_router)
|
||||
|
||||
# Concurrency control
|
||||
router.include_router(concurrency_router)
|
||||
|
||||
__all__ = ["router"]
|
||||
99
_deprecated_py_src/api/admin/endpoints/concurrency.py
Normal file
99
_deprecated_py_src/api/admin/endpoints/concurrency.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Key RPM 限制管理 API
|
||||
"""
|
||||
|
||||
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 get_pipeline
|
||||
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
|
||||
|
||||
router = APIRouter(tags=["RPM Control"])
|
||||
pipeline = get_pipeline()
|
||||
|
||||
|
||||
@router.get("/rpm/key/{key_id}", response_model=KeyRpmStatusResponse)
|
||||
async def get_key_rpm(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> KeyRpmStatusResponse:
|
||||
"""
|
||||
获取 Key 当前 RPM 状态
|
||||
|
||||
查询指定 API Key 的实时 RPM 使用情况,包括当前 RPM 计数和最大 RPM 限制。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: API Key ID
|
||||
|
||||
**返回字段**:
|
||||
- `key_id`: API Key ID
|
||||
- `current_rpm`: 当前 RPM 计数
|
||||
- `rpm_limit`: RPM 限制
|
||||
"""
|
||||
adapter = AdminKeyRpmAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.delete("/rpm/key/{key_id}")
|
||||
async def reset_key_rpm(
|
||||
key_id: str,
|
||||
http_request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
重置 Key RPM 计数器
|
||||
|
||||
重置指定 API Key 的 RPM 计数器,用于解决计数不准确的问题。
|
||||
管理员功能,请谨慎使用。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: API Key ID
|
||||
|
||||
**返回字段**:
|
||||
- `message`: 操作结果消息
|
||||
"""
|
||||
adapter = AdminResetKeyRpmAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=http_request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# -------- Adapters --------
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminKeyRpmAdapter(AdminApiAdapter):
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == self.key_id).first()
|
||||
if not key:
|
||||
raise NotFoundException(f"Key {self.key_id} 不存在")
|
||||
|
||||
concurrency_manager = await get_concurrency_manager()
|
||||
key_count = await concurrency_manager.get_key_rpm_count(key_id=self.key_id)
|
||||
|
||||
return KeyRpmStatusResponse(
|
||||
key_id=self.key_id,
|
||||
current_rpm=key_count,
|
||||
rpm_limit=key.rpm_limit,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminResetKeyRpmAdapter(AdminApiAdapter):
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
concurrency_manager = await get_concurrency_manager()
|
||||
await concurrency_manager.reset_key_rpm(key_id=self.key_id)
|
||||
return {"message": "RPM 计数已重置"}
|
||||
601
_deprecated_py_src/api/admin/endpoints/health.py
Normal file
601
_deprecated_py_src/api/admin/endpoints/health.py
Normal file
@@ -0,0 +1,601 @@
|
||||
"""
|
||||
Endpoint 健康监控 API
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
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 get_pipeline
|
||||
from src.core.exceptions import NotFoundException
|
||||
from src.core.logger import logger
|
||||
from src.database import get_db
|
||||
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint, RequestCandidate
|
||||
from src.models.endpoint_models import (
|
||||
ApiFormatHealthMonitor,
|
||||
ApiFormatHealthMonitorResponse,
|
||||
EndpointHealthEvent,
|
||||
HealthStatusResponse,
|
||||
HealthSummaryResponse,
|
||||
)
|
||||
from src.services.health.endpoint import EndpointHealthService
|
||||
from src.services.health.monitor import HealthMonitor, get_health_monitor
|
||||
|
||||
router = APIRouter(tags=["Endpoint Health"])
|
||||
|
||||
|
||||
def _recover_key_health_sync(db: Session, key_id: str, api_format: str | None) -> dict[str, Any]:
|
||||
key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == key_id).first()
|
||||
if not key:
|
||||
raise NotFoundException(f"Key {key_id} 不存在")
|
||||
|
||||
success = get_health_monitor().reset_health(db, key_id=key_id, api_format=api_format)
|
||||
if not success:
|
||||
raise Exception("重置健康度失败")
|
||||
|
||||
if not key.is_active:
|
||||
key.is_active = True # type: ignore[assignment]
|
||||
|
||||
db.commit()
|
||||
return {
|
||||
"is_active": bool(key.is_active),
|
||||
"api_format": api_format,
|
||||
}
|
||||
|
||||
|
||||
def _recover_all_keys_health_sync(db: Session) -> list[dict[str, Any]]:
|
||||
candidates = (
|
||||
db.query(ProviderAPIKey)
|
||||
.filter(
|
||||
ProviderAPIKey.circuit_breaker_by_format.isnot(None),
|
||||
ProviderAPIKey.circuit_breaker_by_format != "{}",
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
circuit_open_keys = [
|
||||
key
|
||||
for key in candidates
|
||||
if any(cb.get("open") for cb in (key.circuit_breaker_by_format or {}).values())
|
||||
]
|
||||
|
||||
recovered_keys: list[dict[str, Any]] = []
|
||||
for key in circuit_open_keys:
|
||||
key.health_by_format = {} # type: ignore[assignment]
|
||||
key.circuit_breaker_by_format = {} # type: ignore[assignment]
|
||||
recovered_keys.append(
|
||||
{
|
||||
"key_id": key.id,
|
||||
"key_name": key.name,
|
||||
"provider_id": key.provider_id,
|
||||
"api_formats": key.api_formats,
|
||||
}
|
||||
)
|
||||
|
||||
if recovered_keys:
|
||||
db.commit()
|
||||
|
||||
return recovered_keys
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _fetch_recent_attempts_for_api_format(
|
||||
db: Session,
|
||||
*,
|
||||
api_format: str,
|
||||
since: datetime,
|
||||
per_format_limit: int,
|
||||
) -> list[RequestCandidate]:
|
||||
"""获取单个 API 格式最近的最终态请求,用于事件展示。"""
|
||||
final_statuses = ["success", "failed", "skipped"]
|
||||
return (
|
||||
db.query(RequestCandidate)
|
||||
.join(ProviderEndpoint, RequestCandidate.endpoint_id == ProviderEndpoint.id)
|
||||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||||
.filter(
|
||||
ProviderEndpoint.is_active.is_(True),
|
||||
Provider.is_active.is_(True),
|
||||
ProviderEndpoint.api_format == api_format,
|
||||
RequestCandidate.created_at >= since,
|
||||
RequestCandidate.status.in_(final_statuses),
|
||||
)
|
||||
.order_by(RequestCandidate.created_at.desc())
|
||||
.limit(per_format_limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
pipeline = get_pipeline()
|
||||
|
||||
|
||||
@router.get("/health/summary", response_model=HealthSummaryResponse)
|
||||
async def get_health_summary(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> HealthSummaryResponse:
|
||||
"""
|
||||
获取健康状态摘要
|
||||
|
||||
获取系统整体健康状态摘要,包括所有 Provider、Endpoint 和 Key 的健康状态统计。
|
||||
|
||||
**返回字段**:
|
||||
- `total_providers`: Provider 总数
|
||||
- `active_providers`: 活跃 Provider 数量
|
||||
- `total_endpoints`: Endpoint 总数
|
||||
- `active_endpoints`: 活跃 Endpoint 数量
|
||||
- `total_keys`: Key 总数
|
||||
- `active_keys`: 活跃 Key 数量
|
||||
- `circuit_breaker_open_keys`: 熔断的 Key 数量
|
||||
"""
|
||||
adapter = AdminHealthSummaryAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/health/status")
|
||||
async def get_endpoint_health_status(
|
||||
request: Request,
|
||||
lookback_hours: int = Query(6, ge=1, le=72, description="回溯的小时数"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
获取端点健康状态(简化视图,与用户端点统一)
|
||||
|
||||
获取按 API 格式聚合的端点健康状态时间线,基于 Usage 表统计,
|
||||
返回 50 个时间段的聚合状态,适用于快速查看整体健康趋势。
|
||||
|
||||
与 /health/api-formats 的区别:
|
||||
- /health/status: 返回聚合的时间线状态(50个时间段),基于 Usage 表
|
||||
- /health/api-formats: 返回详细的事件列表,基于 RequestCandidate 表
|
||||
|
||||
**查询参数**:
|
||||
- `lookback_hours`: 回溯的小时数(1-72),默认 6
|
||||
|
||||
**返回字段**:
|
||||
- `api_format`: API 格式名称
|
||||
- `timeline`: 时间线数据(50个时间段)
|
||||
- `time_range_start`: 时间范围起始
|
||||
- `time_range_end`: 时间范围结束
|
||||
"""
|
||||
adapter = AdminEndpointHealthStatusAdapter(lookback_hours=lookback_hours)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/health/api-formats", response_model=ApiFormatHealthMonitorResponse)
|
||||
async def get_api_format_health_monitor(
|
||||
request: Request,
|
||||
lookback_hours: int = Query(6, ge=1, le=72, description="回溯的小时数"),
|
||||
per_format_limit: int = Query(60, ge=10, le=200, description="每个 API 格式的事件数量"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ApiFormatHealthMonitorResponse:
|
||||
"""
|
||||
获取按 API 格式聚合的健康监控时间线(详细事件列表)
|
||||
|
||||
获取每个 API 格式的详细健康监控数据,包括请求事件列表、成功率统计、
|
||||
时间线数据等,基于 RequestCandidate 表查询,适用于详细分析。
|
||||
|
||||
**查询参数**:
|
||||
- `lookback_hours`: 回溯的小时数(1-72),默认 6
|
||||
- `per_format_limit`: 每个 API 格式返回的事件数量(10-200),默认 60
|
||||
|
||||
**返回字段**:
|
||||
- `generated_at`: 数据生成时间
|
||||
- `formats`: API 格式健康监控数据列表
|
||||
- `api_format`: API 格式名称
|
||||
- `total_attempts`: 总请求数
|
||||
- `success_count`: 成功请求数
|
||||
- `failed_count`: 失败请求数
|
||||
- `skipped_count`: 跳过请求数
|
||||
- `success_rate`: 成功率
|
||||
- `provider_count`: Provider 数量
|
||||
- `key_count`: Key 数量
|
||||
- `last_event_at`: 最后事件时间
|
||||
- `events`: 事件列表
|
||||
- `timeline`: 时间线数据
|
||||
- `time_range_start`: 时间范围起始
|
||||
- `time_range_end`: 时间范围结束
|
||||
"""
|
||||
adapter = AdminApiFormatHealthMonitorAdapter(
|
||||
lookback_hours=lookback_hours,
|
||||
per_format_limit=per_format_limit,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/health/key/{key_id}", response_model=HealthStatusResponse)
|
||||
async def get_key_health(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
api_format: str | None = Query(None, description="API 格式(可选,如 CLAUDE、OPENAI)"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> HealthStatusResponse:
|
||||
"""
|
||||
获取 Key 健康状态
|
||||
|
||||
获取指定 API Key 的健康状态详情,包括健康分数、连续失败次数、
|
||||
熔断器状态等信息。支持按 API 格式查询。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: API Key ID
|
||||
|
||||
**查询参数**:
|
||||
- `api_format`: 可选,指定 API 格式(如 CLAUDE、OPENAI)。
|
||||
- 指定时返回该格式的健康度详情
|
||||
- 不指定时返回所有格式的健康度摘要
|
||||
|
||||
**返回字段**:
|
||||
- `key_id`: API Key ID
|
||||
- `key_health_score`: 健康分数(0.0-1.0)
|
||||
- `key_is_active`: 是否活跃
|
||||
- `key_statistics`: 统计信息
|
||||
- `health_by_format`: 按格式的健康度数据(无 api_format 参数时)
|
||||
- `circuit_breaker_open`: 熔断器是否打开(有 api_format 参数时)
|
||||
"""
|
||||
adapter = AdminKeyHealthAdapter(key_id=key_id, api_format=api_format)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.patch("/health/keys/{key_id}")
|
||||
async def recover_key_health(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
api_format: str | None = Query(None, description="API 格式(可选,不指定则恢复所有格式)"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
恢复 Key 健康状态
|
||||
|
||||
手动恢复指定 Key 的健康状态,将健康分数重置为 1.0,关闭熔断器,
|
||||
取消自动禁用,并重置所有失败计数。支持按 API 格式恢复。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: API Key ID
|
||||
|
||||
**查询参数**:
|
||||
- `api_format`: 可选,指定 API 格式(如 CLAUDE、OPENAI)
|
||||
- 指定时仅恢复该格式的健康度
|
||||
- 不指定时恢复所有格式
|
||||
|
||||
**返回字段**:
|
||||
- `message`: 操作结果消息
|
||||
- `details`: 详细信息
|
||||
- `health_score`: 健康分数
|
||||
- `circuit_breaker_open`: 熔断器状态
|
||||
- `is_active`: 是否活跃
|
||||
"""
|
||||
adapter = AdminRecoverKeyHealthAdapter(key_id=key_id, api_format=api_format)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.patch("/health/keys")
|
||||
async def recover_all_keys_health(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
批量恢复所有熔断 Key 的健康状态
|
||||
|
||||
查找所有处于熔断状态的 Key(circuit_breaker_open=True),
|
||||
并批量执行以下操作:
|
||||
1. 将健康分数重置为 1.0
|
||||
2. 关闭熔断器
|
||||
3. 重置失败计数
|
||||
|
||||
**返回字段**:
|
||||
- `message`: 操作结果消息
|
||||
- `recovered_count`: 恢复的 Key 数量
|
||||
- `recovered_keys`: 恢复的 Key 列表
|
||||
- `key_id`: Key ID
|
||||
- `key_name`: Key 名称
|
||||
- `endpoint_id`: Endpoint ID
|
||||
"""
|
||||
adapter = AdminRecoverAllKeysHealthAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# -------- Adapters --------
|
||||
|
||||
|
||||
class AdminHealthSummaryAdapter(AdminApiAdapter):
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
summary = get_health_monitor().get_all_health_status(context.db)
|
||||
return HealthSummaryResponse(**summary)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminEndpointHealthStatusAdapter(AdminApiAdapter):
|
||||
"""管理员端点健康状态适配器(与用户端点统一,但包含管理员字段)"""
|
||||
|
||||
lookback_hours: int
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
|
||||
# 使用共享服务获取健康状态(管理员视图)
|
||||
result = EndpointHealthService.get_endpoint_health_by_format(
|
||||
db=db,
|
||||
lookback_hours=self.lookback_hours,
|
||||
include_admin_fields=True, # 包含管理员字段
|
||||
use_cache=False, # 管理员不使用缓存,确保实时性
|
||||
)
|
||||
|
||||
context.add_audit_metadata(
|
||||
action="endpoint_health_status",
|
||||
format_count=len(result),
|
||||
lookback_hours=self.lookback_hours,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminApiFormatHealthMonitorAdapter(AdminApiAdapter):
|
||||
lookback_hours: int
|
||||
per_format_limit: int
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
now = datetime.now(timezone.utc)
|
||||
since = now - timedelta(hours=self.lookback_hours)
|
||||
|
||||
# 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)
|
||||
.filter(
|
||||
ProviderEndpoint.is_active.is_(True),
|
||||
Provider.is_active.is_(True),
|
||||
)
|
||||
.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:
|
||||
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] = {}
|
||||
if active_provider_formats:
|
||||
active_provider_keys = (
|
||||
db.query(ProviderAPIKey.provider_id, ProviderAPIKey.api_formats)
|
||||
.join(Provider, ProviderAPIKey.provider_id == Provider.id)
|
||||
.filter(
|
||||
Provider.is_active.is_(True),
|
||||
ProviderAPIKey.is_active.is_(True),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for provider_id, api_formats in active_provider_keys:
|
||||
pid = str(provider_id)
|
||||
for fmt in api_formats or []:
|
||||
if (pid, fmt) not in active_provider_formats:
|
||||
continue
|
||||
key_counts[fmt] = key_counts.get(fmt, 0) + 1
|
||||
|
||||
# 2. 统计窗口内每个 API 格式的请求状态分布(真实统计)
|
||||
# 只统计最终状态:success, failed, skipped
|
||||
final_statuses = ["success", "failed", "skipped"]
|
||||
status_counts_query = (
|
||||
db.query(
|
||||
ProviderEndpoint.api_format,
|
||||
RequestCandidate.status,
|
||||
func.count(RequestCandidate.id).label("count"),
|
||||
)
|
||||
.join(RequestCandidate, ProviderEndpoint.id == RequestCandidate.endpoint_id)
|
||||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||||
.filter(
|
||||
ProviderEndpoint.is_active.is_(True),
|
||||
Provider.is_active.is_(True),
|
||||
RequestCandidate.created_at >= since,
|
||||
RequestCandidate.status.in_(final_statuses),
|
||||
)
|
||||
.group_by(ProviderEndpoint.api_format, RequestCandidate.status)
|
||||
.all()
|
||||
)
|
||||
|
||||
# 构建每个格式的状态统计
|
||||
status_counts: dict[str, dict[str, int]] = {}
|
||||
for api_format_enum, status, count in status_counts_query:
|
||||
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. 为所有活跃格式生成监控数据(包括没有请求记录的)
|
||||
monitors: list[ApiFormatHealthMonitor] = []
|
||||
for api_format in all_formats:
|
||||
attempts = _fetch_recent_attempts_for_api_format(
|
||||
db=db,
|
||||
api_format=api_format,
|
||||
since=since,
|
||||
per_format_limit=self.per_format_limit,
|
||||
)
|
||||
# 获取窗口内的真实统计数据
|
||||
# 只统计最终状态:success, failed, skipped
|
||||
# 中间状态(available, pending, used, started)不计入统计
|
||||
format_stats = status_counts.get(api_format, {"success": 0, "failed": 0, "skipped": 0})
|
||||
real_success_count = format_stats.get("success", 0)
|
||||
real_failed_count = format_stats.get("failed", 0)
|
||||
real_skipped_count = format_stats.get("skipped", 0)
|
||||
# total_attempts 只包含最终状态的请求数
|
||||
total_attempts = real_success_count + real_failed_count + real_skipped_count
|
||||
|
||||
# 时间线按时间正序
|
||||
attempts_sorted = list(reversed(attempts))
|
||||
events: list[EndpointHealthEvent] = []
|
||||
for attempt in attempts_sorted:
|
||||
event_timestamp = attempt.finished_at or attempt.started_at or attempt.created_at
|
||||
events.append(
|
||||
EndpointHealthEvent(
|
||||
timestamp=event_timestamp,
|
||||
status=attempt.status,
|
||||
status_code=attempt.status_code,
|
||||
latency_ms=attempt.latency_ms,
|
||||
error_type=attempt.error_type,
|
||||
error_message=attempt.error_message,
|
||||
)
|
||||
)
|
||||
|
||||
# 成功率 = success / (success + failed)
|
||||
# skipped 不算失败,不计入成功率分母
|
||||
# 无实际完成请求时成功率为 1.0(灰色状态)
|
||||
actual_completed = real_success_count + real_failed_count
|
||||
success_rate = real_success_count / actual_completed if actual_completed > 0 else 1.0
|
||||
last_event_at = events[-1].timestamp if events else None
|
||||
|
||||
# 生成 Usage 基于时间窗口的健康时间线
|
||||
timeline_data = EndpointHealthService._generate_timeline_from_usage(
|
||||
db=db,
|
||||
endpoint_ids=endpoint_map.get(api_format, []),
|
||||
now=now,
|
||||
lookback_hours=self.lookback_hours,
|
||||
)
|
||||
|
||||
monitors.append(
|
||||
ApiFormatHealthMonitor(
|
||||
api_format=api_format,
|
||||
total_attempts=total_attempts, # 真实总请求数
|
||||
success_count=real_success_count, # 真实成功数
|
||||
failed_count=real_failed_count, # 真实失败数
|
||||
skipped_count=real_skipped_count, # 真实跳过数
|
||||
success_rate=success_rate, # 基于真实统计的成功率
|
||||
provider_count=all_formats[api_format],
|
||||
key_count=key_counts.get(api_format, 0),
|
||||
last_event_at=last_event_at,
|
||||
events=events, # 限制为 per_format_limit 条(用于时间线显示)
|
||||
timeline=timeline_data.get("timeline", []),
|
||||
time_range_start=timeline_data.get("time_range_start"),
|
||||
time_range_end=timeline_data.get("time_range_end"),
|
||||
)
|
||||
)
|
||||
|
||||
response = ApiFormatHealthMonitorResponse(
|
||||
generated_at=now,
|
||||
formats=monitors,
|
||||
)
|
||||
context.add_audit_metadata(
|
||||
action="api_format_health_monitor",
|
||||
format_count=len(monitors),
|
||||
lookback_hours=self.lookback_hours,
|
||||
per_format_limit=self.per_format_limit,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminKeyHealthAdapter(AdminApiAdapter):
|
||||
key_id: str
|
||||
api_format: str | None = None
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
health_data = get_health_monitor().get_key_health(context.db, self.key_id, self.api_format)
|
||||
if not health_data:
|
||||
raise NotFoundException(f"Key {self.key_id} 不存在")
|
||||
|
||||
# 构建响应
|
||||
response_data = {
|
||||
"key_id": health_data["key_id"],
|
||||
"key_is_active": health_data["is_active"],
|
||||
"key_statistics": health_data.get("statistics"),
|
||||
"key_health_score": health_data.get("health_score", 1.0),
|
||||
}
|
||||
|
||||
if self.api_format:
|
||||
# 单格式查询
|
||||
response_data["api_format"] = self.api_format
|
||||
response_data["key_consecutive_failures"] = health_data.get("consecutive_failures")
|
||||
response_data["key_last_failure_at"] = health_data.get("last_failure_at")
|
||||
circuit = health_data.get("circuit_breaker", {})
|
||||
response_data["circuit_breaker_open"] = circuit.get("open", False)
|
||||
response_data["circuit_breaker_open_at"] = circuit.get("open_at")
|
||||
response_data["next_probe_at"] = circuit.get("next_probe_at")
|
||||
response_data["half_open_until"] = circuit.get("half_open_until")
|
||||
response_data["half_open_successes"] = circuit.get("half_open_successes", 0)
|
||||
response_data["half_open_failures"] = circuit.get("half_open_failures", 0)
|
||||
else:
|
||||
# 全格式查询
|
||||
response_data["any_circuit_open"] = health_data.get("any_circuit_open", False)
|
||||
response_data["health_by_format"] = health_data.get("health_by_format")
|
||||
|
||||
return HealthStatusResponse(**response_data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminRecoverKeyHealthAdapter(AdminApiAdapter):
|
||||
key_id: str
|
||||
api_format: str | None = None
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
await asyncio.to_thread(_recover_key_health_sync, db, self.key_id, self.api_format)
|
||||
|
||||
if self.api_format:
|
||||
logger.info(f"管理员恢复Key健康状态: {self.key_id}/{self.api_format}")
|
||||
return {
|
||||
"message": f"Key 的 {self.api_format} 格式已恢复",
|
||||
"details": {
|
||||
"api_format": self.api_format,
|
||||
"health_score": 1.0,
|
||||
"circuit_breaker_open": False,
|
||||
"is_active": True,
|
||||
},
|
||||
}
|
||||
else:
|
||||
logger.info(f"管理员恢复Key健康状态: {self.key_id} (所有格式)")
|
||||
return {
|
||||
"message": "Key 所有格式已恢复",
|
||||
"details": {
|
||||
"health_score": 1.0,
|
||||
"circuit_breaker_open": False,
|
||||
"is_active": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AdminRecoverAllKeysHealthAdapter(AdminApiAdapter):
|
||||
"""批量恢复所有熔断 Key 的健康状态"""
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
recovered_keys = await asyncio.to_thread(_recover_all_keys_health_sync, db)
|
||||
|
||||
if not recovered_keys:
|
||||
return {
|
||||
"message": "没有需要恢复的 Key",
|
||||
"recovered_count": 0,
|
||||
"recovered_keys": [],
|
||||
}
|
||||
|
||||
# 重置健康监控器的熔断计数
|
||||
HealthMonitor.reset_open_circuit_count()
|
||||
|
||||
logger.info(f"管理员批量恢复 {len(recovered_keys)} 个 Key 的健康状态")
|
||||
|
||||
return {
|
||||
"message": f"已恢复 {len(recovered_keys)} 个 Key",
|
||||
"recovered_count": len(recovered_keys),
|
||||
"recovered_keys": recovered_keys,
|
||||
}
|
||||
437
_deprecated_py_src/api/admin/endpoints/keys.py
Normal file
437
_deprecated_py_src/api/admin/endpoints/keys.py
Normal file
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
Provider API Keys 管理
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
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 get_pipeline
|
||||
from src.database import get_db
|
||||
from src.models.database import User
|
||||
from src.models.endpoint_models import (
|
||||
EndpointAPIKeyCreate,
|
||||
EndpointAPIKeyResponse,
|
||||
EndpointAPIKeyUpdate,
|
||||
)
|
||||
from src.services.provider_keys import (
|
||||
batch_delete_endpoint_keys_response,
|
||||
clear_oauth_invalid_response,
|
||||
create_provider_key_response,
|
||||
delete_endpoint_key_response,
|
||||
export_oauth_key_data,
|
||||
)
|
||||
from src.services.provider_keys import get_keys_grouped_by_format as query_keys_grouped_by_format
|
||||
from src.services.provider_keys import (
|
||||
list_provider_keys_responses,
|
||||
refresh_provider_quota_for_provider,
|
||||
reveal_endpoint_key_payload,
|
||||
update_endpoint_key_response,
|
||||
)
|
||||
from src.services.provider_keys.key_quota_service import (
|
||||
CODEX_WHAM_USAGE_URL as _CODEX_WHAM_USAGE_URL,
|
||||
)
|
||||
from src.utils.auth_utils import require_admin
|
||||
|
||||
router = APIRouter(tags=["Provider Keys"])
|
||||
pipeline = get_pipeline()
|
||||
|
||||
|
||||
@router.put("/keys/{key_id}", response_model=EndpointAPIKeyResponse)
|
||||
async def update_endpoint_key(
|
||||
key_id: str,
|
||||
key_data: EndpointAPIKeyUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> EndpointAPIKeyResponse:
|
||||
"""
|
||||
更新 Provider Key
|
||||
|
||||
更新指定 Key 的配置,支持修改并发限制、速率倍数、优先级、
|
||||
配额限制、能力限制等。支持部分更新。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: Key ID
|
||||
|
||||
**请求体字段**(均为可选):
|
||||
- `api_key`: 新的 API Key 原文
|
||||
- `name`: Key 名称
|
||||
- `note`: 备注
|
||||
- `rate_multipliers`: 按 API 格式的成本倍率
|
||||
- `internal_priority`: 内部优先级
|
||||
- `rpm_limit`: RPM 限制(设置为 null 可切换到自适应模式)
|
||||
- `allowed_models`: 允许的模型列表
|
||||
- `capabilities`: 能力配置
|
||||
- `is_active`: 是否活跃
|
||||
|
||||
**返回字段**:
|
||||
- 包含更新后的完整 Key 信息
|
||||
"""
|
||||
adapter = AdminUpdateEndpointKeyAdapter(key_id=key_id, key_data=key_data)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/keys/grouped-by-format")
|
||||
async def get_keys_grouped_by_format(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
获取按 API 格式分组的所有 Keys
|
||||
|
||||
获取所有活跃的 Key,按 API 格式分组返回,用于全局优先级管理。
|
||||
每个 Key 包含基本信息、健康度指标、能力标签等。
|
||||
|
||||
**返回字段**:
|
||||
- 返回一个字典,键为 API 格式,值为该格式下的 Key 列表
|
||||
- 每个 Key 包含:
|
||||
- `id`: Key ID
|
||||
- `name`: Key 名称
|
||||
- `api_key_masked`: 脱敏后的 API Key
|
||||
- `internal_priority`: 内部优先级
|
||||
- `global_priority_by_format`: 按 API 格式的全局优先级
|
||||
- `format_priority`: 当前格式的优先级
|
||||
- `rate_multipliers`: 按 API 格式的成本倍率
|
||||
- `is_active`: 是否活跃
|
||||
- `circuit_breaker_open`: 熔断器状态
|
||||
- `provider_name`: Provider 名称
|
||||
- `endpoint_base_url`: Endpoint 基础 URL
|
||||
- `api_format`: API 格式
|
||||
- `capabilities`: 能力简称列表
|
||||
- `success_rate`: 成功率
|
||||
- `avg_response_time_ms`: 平均响应时间
|
||||
- `request_count`: 请求总数
|
||||
"""
|
||||
adapter = AdminGetKeysGroupedByFormatAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/keys/{key_id}/reveal")
|
||||
async def reveal_endpoint_key(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
获取完整的 API Key
|
||||
|
||||
解密并返回指定 Key 的完整原文,用于查看和复制。
|
||||
此操作会被记录到审计日志。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: Key ID
|
||||
|
||||
**返回字段**:
|
||||
- `api_key`: 完整的 API Key 原文
|
||||
"""
|
||||
adapter = AdminRevealEndpointKeyAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/keys/{key_id}/export")
|
||||
async def export_key(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> dict:
|
||||
"""
|
||||
导出 OAuth Key 凭据(用于跨实例迁移)
|
||||
|
||||
解密 auth_config,返回精简的扁平 JSON,去掉 null 和临时字段。
|
||||
所有 OAuth Provider 格式统一。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: Key ID
|
||||
"""
|
||||
adapter = AdminExportKeyAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.delete("/keys/{key_id}")
|
||||
async def delete_endpoint_key(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
删除 Provider Key
|
||||
|
||||
删除指定的 API Key。此操作不可逆,请谨慎使用。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: Key ID
|
||||
|
||||
**返回字段**:
|
||||
- `message`: 操作结果消息
|
||||
"""
|
||||
adapter = AdminDeleteEndpointKeyAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/keys/batch-delete")
|
||||
async def batch_delete_endpoint_keys(
|
||||
request: Request,
|
||||
ids: list[str] = Body(..., embed=True, max_length=100),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
批量删除 Provider Keys
|
||||
|
||||
一次性删除多个 Key,按 Provider 聚合执行副作用(缓存失效、模型关联检查),
|
||||
避免逐个删除导致的重复 Redis 操作和性能问题。
|
||||
|
||||
**请求体字段**:
|
||||
- `ids`: Key ID 列表(最多 100 个)
|
||||
|
||||
**返回字段**:
|
||||
- `success_count`: 成功删除的数量
|
||||
- `failed_count`: 失败的数量
|
||||
- `failed`: 失败的详情列表
|
||||
"""
|
||||
adapter = AdminBatchDeleteEndpointKeysAdapter(ids=ids)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/keys/{key_id}/clear-oauth-invalid")
|
||||
async def clear_oauth_invalid(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> dict:
|
||||
"""
|
||||
清除 Key 的 OAuth 失效标记
|
||||
|
||||
手动清除指定 Key 的 oauth_invalid_at / oauth_invalid_reason 状态,
|
||||
通常在管理员确认账号已完成验证后使用。
|
||||
|
||||
这是 admin/status 维修入口,不是 AI 运行时请求恢复路径。
|
||||
Rust 热路径迁移完成后,这里仍负责人工解除 OAuth invalid 标记。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: Key ID
|
||||
|
||||
**返回字段**:
|
||||
- `message`: 操作结果消息
|
||||
"""
|
||||
adapter = AdminClearOAuthInvalidAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# ========== Provider Keys API ==========
|
||||
|
||||
|
||||
@router.get("/providers/{provider_id}/keys", response_model=list[EndpointAPIKeyResponse])
|
||||
async def list_provider_keys(
|
||||
provider_id: str,
|
||||
request: Request,
|
||||
skip: int = Query(0, ge=0, description="跳过的记录数"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="返回的最大记录数"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[EndpointAPIKeyResponse]:
|
||||
"""
|
||||
获取 Provider 的所有 Keys
|
||||
|
||||
获取指定 Provider 下的所有 API Key 列表,支持多 API 格式。
|
||||
结果按优先级和创建时间排序。
|
||||
|
||||
**路径参数**:
|
||||
- `provider_id`: Provider ID
|
||||
|
||||
**查询参数**:
|
||||
- `skip`: 跳过的记录数,用于分页(默认 0)
|
||||
- `limit`: 返回的最大记录数(1-1000,默认 100)
|
||||
"""
|
||||
adapter = AdminListProviderKeysAdapter(
|
||||
provider_id=provider_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/providers/{provider_id}/keys", response_model=EndpointAPIKeyResponse)
|
||||
async def add_provider_key(
|
||||
provider_id: str,
|
||||
key_data: EndpointAPIKeyCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> EndpointAPIKeyResponse:
|
||||
"""
|
||||
为 Provider 添加 Key
|
||||
|
||||
为指定 Provider 添加新的 API Key,支持配置多个 API 格式。
|
||||
|
||||
**路径参数**:
|
||||
- `provider_id`: Provider ID
|
||||
|
||||
**请求体字段**:
|
||||
- `api_formats`: 支持的 API 格式列表(必填)
|
||||
- `api_key`: API Key 原文(将被加密存储)
|
||||
- `name`: Key 名称
|
||||
- 其他配置字段同 Key
|
||||
"""
|
||||
adapter = AdminCreateProviderKeyAdapter(provider_id=provider_id, key_data=key_data)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# -------- Adapters --------
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
|
||||
key_id: str
|
||||
key_data: EndpointAPIKeyUpdate
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return await update_endpoint_key_response(
|
||||
db=context.db,
|
||||
key_id=self.key_id,
|
||||
key_data=self.key_data,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminRevealEndpointKeyAdapter(AdminApiAdapter):
|
||||
"""获取完整的 API Key 或 Auth Config(用于查看和复制)"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return reveal_endpoint_key_payload(context.db, self.key_id)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminExportKeyAdapter(AdminApiAdapter):
|
||||
"""导出 OAuth Key 凭据:解密 auth_config,委托 provider-specific builder 构建导出数据。"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return export_oauth_key_data(context.db, self.key_id)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminDeleteEndpointKeyAdapter(AdminApiAdapter):
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return await delete_endpoint_key_response(db=context.db, key_id=self.key_id)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminBatchDeleteEndpointKeysAdapter(AdminApiAdapter):
|
||||
"""批量删除多个 Provider Key"""
|
||||
|
||||
ids: list[str]
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return await batch_delete_endpoint_keys_response(db=context.db, key_ids=self.ids)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminClearOAuthInvalidAdapter(AdminApiAdapter):
|
||||
"""清除 Key 的 OAuth 失效标记。"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return clear_oauth_invalid_response(context.db, self.key_id)
|
||||
|
||||
|
||||
class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return query_keys_grouped_by_format(context.db)
|
||||
|
||||
|
||||
# ========== Adapters ==========
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminListProviderKeysAdapter(AdminApiAdapter):
|
||||
"""获取 Provider 的所有 Keys"""
|
||||
|
||||
provider_id: str
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return list_provider_keys_responses(context.db, self.provider_id, self.skip, self.limit)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminCreateProviderKeyAdapter(AdminApiAdapter):
|
||||
"""为 Provider 添加 Key"""
|
||||
|
||||
provider_id: str
|
||||
key_data: EndpointAPIKeyCreate
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return await create_provider_key_response(
|
||||
db=context.db,
|
||||
provider_id=self.provider_id,
|
||||
key_data=self.key_data,
|
||||
)
|
||||
|
||||
|
||||
# ========== Quota Refresh API ==========
|
||||
|
||||
|
||||
class RefreshProviderQuotaRequest(BaseModel):
|
||||
key_ids: list[str] | None = Field(default=None, description="仅刷新指定 Key 列表(可选)")
|
||||
|
||||
|
||||
@router.post("/providers/{provider_id}/refresh-quota")
|
||||
async def refresh_provider_quota(
|
||||
provider_id: str,
|
||||
request: Request,
|
||||
payload: RefreshProviderQuotaRequest | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
刷新 Provider 所有 Keys 的限额信息
|
||||
|
||||
支持的 Provider 类型:
|
||||
- Codex: 调用 wham/usage API 获取限额
|
||||
- Antigravity: 调用 fetchAvailableModels 获取配额
|
||||
- Kiro: 调用 getUsageLimits API 获取使用额度
|
||||
|
||||
**路径参数**:
|
||||
- `provider_id`: Provider ID
|
||||
**请求体**(可选):
|
||||
- `key_ids`: 仅刷新指定 Key 列表,不传时刷新所有活跃 Key
|
||||
|
||||
**返回字段**:
|
||||
- `success`: 成功刷新的 Key 数量
|
||||
- `failed`: 失败的 Key 数量
|
||||
- `results`: 每个 Key 的刷新结果
|
||||
"""
|
||||
adapter = AdminRefreshProviderQuotaAdapter(
|
||||
provider_id=provider_id,
|
||||
key_ids=payload.key_ids if payload else None,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
||||
"""刷新 Provider 所有 Keys 的限额信息"""
|
||||
|
||||
provider_id: str
|
||||
key_ids: list[str] | None = None
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return await refresh_provider_quota_for_provider(
|
||||
db=context.db,
|
||||
provider_id=self.provider_id,
|
||||
codex_wham_usage_url=_CODEX_WHAM_USAGE_URL,
|
||||
key_ids=self.key_ids,
|
||||
)
|
||||
637
_deprecated_py_src/api/admin/endpoints/routes.py
Normal file
637
_deprecated_py_src/api/admin/endpoints/routes.py
Normal file
@@ -0,0 +1,637 @@
|
||||
"""
|
||||
ProviderEndpoint CRUD 管理 API
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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_
|
||||
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 get_pipeline
|
||||
from src.core.api_format.metadata import get_default_body_rules_for_endpoint
|
||||
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.core.provider_templates.fixed_providers import FIXED_PROVIDERS
|
||||
from src.core.provider_types import ProviderType
|
||||
from src.database import get_db
|
||||
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
||||
from src.models.endpoint_models import (
|
||||
ProviderEndpointCreate,
|
||||
ProviderEndpointResponse,
|
||||
ProviderEndpointUpdate,
|
||||
)
|
||||
from src.services.provider.stream_policy import UpstreamStreamPolicy, parse_upstream_stream_policy
|
||||
|
||||
router = APIRouter(tags=["Endpoint Management"])
|
||||
pipeline = get_pipeline()
|
||||
|
||||
|
||||
def mask_proxy_password(proxy_config: dict | None) -> dict | None:
|
||||
"""对代理配置中的密码进行脱敏处理"""
|
||||
if not proxy_config:
|
||||
return None
|
||||
masked = dict(proxy_config)
|
||||
if masked.get("password"):
|
||||
masked["password"] = "***"
|
||||
return masked
|
||||
|
||||
|
||||
def _is_fixed_provider(provider_type: str | None) -> bool:
|
||||
"""Whether this provider_type is managed by fixed-provider templates."""
|
||||
normalized = (provider_type or "custom").strip().lower()
|
||||
if normalized == ProviderType.CUSTOM.value:
|
||||
return False
|
||||
try:
|
||||
return ProviderType(normalized) in FIXED_PROVIDERS
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/providers/{provider_id}/endpoints", response_model=list[ProviderEndpointResponse])
|
||||
async def list_provider_endpoints(
|
||||
provider_id: str,
|
||||
request: Request,
|
||||
skip: int = Query(0, ge=0, description="跳过的记录数"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="返回的最大记录数"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[ProviderEndpointResponse]:
|
||||
"""
|
||||
获取指定 Provider 的所有 Endpoints
|
||||
|
||||
获取指定 Provider 下的所有 Endpoint 列表,包括配置、统计信息等。
|
||||
结果按创建时间倒序排列。
|
||||
|
||||
**路径参数**:
|
||||
- `provider_id`: Provider ID
|
||||
|
||||
**查询参数**:
|
||||
- `skip`: 跳过的记录数,用于分页(默认 0)
|
||||
- `limit`: 返回的最大记录数(1-1000,默认 100)
|
||||
|
||||
**返回字段**:
|
||||
- `id`: Endpoint ID
|
||||
- `provider_id`: Provider ID
|
||||
- `provider_name`: Provider 名称
|
||||
- `api_format`: API 格式
|
||||
- `base_url`: 基础 URL
|
||||
- `custom_path`: 自定义路径
|
||||
- `max_retries`: 最大重试次数
|
||||
- `is_active`: 是否活跃
|
||||
- `total_keys`: Key 总数
|
||||
- `active_keys`: 活跃 Key 数量
|
||||
- `proxy`: 代理配置(密码已脱敏)
|
||||
- 其他配置字段
|
||||
"""
|
||||
adapter = AdminListProviderEndpointsAdapter(
|
||||
provider_id=provider_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/providers/{provider_id}/endpoints", response_model=ProviderEndpointResponse)
|
||||
async def create_provider_endpoint(
|
||||
provider_id: str,
|
||||
endpoint_data: ProviderEndpointCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> ProviderEndpointResponse:
|
||||
"""
|
||||
为 Provider 创建新的 Endpoint
|
||||
|
||||
为指定 Provider 创建新的 Endpoint,每个 Provider 的每种 API 格式
|
||||
只能创建一个 Endpoint。
|
||||
|
||||
**路径参数**:
|
||||
- `provider_id`: Provider ID
|
||||
|
||||
**请求体字段**:
|
||||
- `provider_id`: Provider ID(必须与路径参数一致)
|
||||
- `api_format`: API 格式(如 claude、openai、gemini 等)
|
||||
- `base_url`: 基础 URL
|
||||
- `custom_path`: 自定义路径(可选)
|
||||
- `header_rules`: 请求头规则列表(可选,支持 set/drop/rename 操作)
|
||||
- `max_retries`: 最大重试次数(默认 2)
|
||||
- `config`: 额外配置(可选)
|
||||
- `proxy`: 代理配置(可选)
|
||||
|
||||
**返回字段**:
|
||||
- 包含完整的 Endpoint 信息
|
||||
"""
|
||||
adapter = AdminCreateProviderEndpointAdapter(
|
||||
provider_id=provider_id,
|
||||
endpoint_data=endpoint_data,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/defaults/{api_format}/body-rules")
|
||||
async def get_default_endpoint_body_rules(
|
||||
api_format: str,
|
||||
request: Request,
|
||||
provider_type: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
"""获取指定 endpoint signature 的默认 body_rules。"""
|
||||
adapter = AdminGetDefaultBodyRulesAdapter(
|
||||
api_format=api_format, provider_type=provider_type or None
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/{endpoint_id}", response_model=ProviderEndpointResponse)
|
||||
async def get_endpoint(
|
||||
endpoint_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> ProviderEndpointResponse:
|
||||
"""
|
||||
获取 Endpoint 详情
|
||||
|
||||
获取指定 Endpoint 的详细信息,包括配置、统计信息等。
|
||||
|
||||
**路径参数**:
|
||||
- `endpoint_id`: Endpoint ID
|
||||
|
||||
**返回字段**:
|
||||
- `id`: Endpoint ID
|
||||
- `provider_id`: Provider ID
|
||||
- `provider_name`: Provider 名称
|
||||
- `api_format`: API 格式
|
||||
- `base_url`: 基础 URL
|
||||
- `custom_path`: 自定义路径
|
||||
- `max_retries`: 最大重试次数
|
||||
- `is_active`: 是否活跃
|
||||
- `total_keys`: Key 总数
|
||||
- `active_keys`: 活跃 Key 数量
|
||||
- `proxy`: 代理配置(密码已脱敏)
|
||||
- 其他配置字段
|
||||
"""
|
||||
adapter = AdminGetProviderEndpointAdapter(endpoint_id=endpoint_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.put("/{endpoint_id}", response_model=ProviderEndpointResponse)
|
||||
async def update_endpoint(
|
||||
endpoint_id: str,
|
||||
endpoint_data: ProviderEndpointUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> ProviderEndpointResponse:
|
||||
"""
|
||||
更新 Endpoint
|
||||
|
||||
更新指定 Endpoint 的配置。支持部分更新。
|
||||
|
||||
**路径参数**:
|
||||
- `endpoint_id`: Endpoint ID
|
||||
|
||||
**请求体字段**(均为可选):
|
||||
- `base_url`: 基础 URL
|
||||
- `custom_path`: 自定义路径
|
||||
- `header_rules`: 请求头规则列表
|
||||
- `max_retries`: 最大重试次数
|
||||
- `is_active`: 是否活跃
|
||||
- `config`: 额外配置
|
||||
- `proxy`: 代理配置(设置为 null 可清除代理)
|
||||
|
||||
**返回字段**:
|
||||
- 包含更新后的完整 Endpoint 信息
|
||||
"""
|
||||
adapter = AdminUpdateProviderEndpointAdapter(
|
||||
endpoint_id=endpoint_id,
|
||||
endpoint_data=endpoint_data,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.delete("/{endpoint_id}")
|
||||
async def delete_endpoint(
|
||||
endpoint_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
删除 Endpoint
|
||||
|
||||
删除指定的 Endpoint,会影响该 Provider 在该 API 格式下的路由能力。
|
||||
Key 不会被删除,但包含该 API 格式的 Key 将无法被调度使用(直到重新创建该格式的 Endpoint)。
|
||||
|
||||
**路径参数**:
|
||||
- `endpoint_id`: Endpoint ID
|
||||
|
||||
**返回字段**:
|
||||
- `message`: 操作结果消息
|
||||
- `affected_keys_count`: 受影响的 Key 数量(包含该 API 格式)
|
||||
"""
|
||||
adapter = AdminDeleteProviderEndpointAdapter(endpoint_id=endpoint_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# -------- Adapters --------
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminListProviderEndpointsAdapter(AdminApiAdapter):
|
||||
provider_id: str
|
||||
skip: int
|
||||
limit: int
|
||||
|
||||
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} 不存在")
|
||||
|
||||
endpoints = (
|
||||
db.query(ProviderEndpoint)
|
||||
.filter(ProviderEndpoint.provider_id == self.provider_id)
|
||||
.order_by(ProviderEndpoint.created_at.desc())
|
||||
.offset(self.skip)
|
||||
.limit(self.limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Key 是 Provider 级别资源:按 key.api_formats 归类到各 Endpoint.api_format 下
|
||||
keys = (
|
||||
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
|
||||
.filter(ProviderAPIKey.provider_id == self.provider_id)
|
||||
.all()
|
||||
)
|
||||
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 []:
|
||||
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
|
||||
|
||||
result: list[ProviderEndpointResponse] = []
|
||||
for endpoint in endpoints:
|
||||
endpoint_format = (
|
||||
endpoint.api_format
|
||||
if isinstance(endpoint.api_format, str)
|
||||
else endpoint.api_format.value
|
||||
)
|
||||
endpoint_dict = {
|
||||
**endpoint.__dict__,
|
||||
"provider_name": provider.name,
|
||||
"api_format": endpoint.api_format,
|
||||
"total_keys": total_keys_map.get(endpoint_format, 0),
|
||||
"active_keys": active_keys_map.get(endpoint_format, 0),
|
||||
"proxy": mask_proxy_password(endpoint.proxy),
|
||||
}
|
||||
endpoint_dict.pop("_sa_instance_state", None)
|
||||
result.append(ProviderEndpointResponse(**endpoint_dict))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
|
||||
provider_id: str
|
||||
endpoint_data: ProviderEndpointCreate
|
||||
|
||||
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} 不存在")
|
||||
|
||||
# 固定类型 Provider:禁止通过该接口新增 Endpoints(端点由模板自动创建并锁定)
|
||||
provider_type = getattr(provider, "provider_type", None) or "custom"
|
||||
if _is_fixed_provider(provider_type):
|
||||
raise InvalidRequestException("固定类型 Provider 不允许手动新增 Endpoint")
|
||||
|
||||
if self.endpoint_data.provider_id != self.provider_id:
|
||||
raise InvalidRequestException("provider_id 不匹配")
|
||||
|
||||
existing = (
|
||||
db.query(ProviderEndpoint)
|
||||
.filter(
|
||||
and_(
|
||||
ProviderEndpoint.provider_id == self.provider_id,
|
||||
ProviderEndpoint.api_format == self.endpoint_data.api_format,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise InvalidRequestException(
|
||||
f"Provider {provider.name} 已存在 {self.endpoint_data.api_format} 格式的 Endpoint"
|
||||
)
|
||||
|
||||
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
|
||||
body_rules = self.endpoint_data.body_rules
|
||||
if body_rules is None:
|
||||
body_rules = (
|
||||
get_default_body_rules_for_endpoint(
|
||||
normalized_api_format, provider_type=provider_type
|
||||
)
|
||||
or None
|
||||
)
|
||||
|
||||
new_endpoint = ProviderEndpoint(
|
||||
id=str(uuid.uuid4()),
|
||||
provider_id=self.provider_id,
|
||||
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,
|
||||
body_rules=body_rules,
|
||||
max_retries=self.endpoint_data.max_retries,
|
||||
is_active=True,
|
||||
config=self.endpoint_data.config,
|
||||
proxy=self.endpoint_data.proxy.model_dump() if self.endpoint_data.proxy else None,
|
||||
format_acceptance_config=self.endpoint_data.format_acceptance_config,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
db.add(new_endpoint)
|
||||
db.commit()
|
||||
db.refresh(new_endpoint)
|
||||
|
||||
# 清除 /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}"
|
||||
)
|
||||
|
||||
endpoint_dict = {
|
||||
k: v
|
||||
for k, v in new_endpoint.__dict__.items()
|
||||
if k not in {"api_format", "_sa_instance_state", "proxy"}
|
||||
}
|
||||
return ProviderEndpointResponse(
|
||||
**endpoint_dict,
|
||||
provider_name=provider.name,
|
||||
api_format=new_endpoint.api_format,
|
||||
proxy=mask_proxy_password(new_endpoint.proxy),
|
||||
total_keys=0,
|
||||
active_keys=0,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminGetProviderEndpointAdapter(AdminApiAdapter):
|
||||
endpoint_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
endpoint = (
|
||||
db.query(ProviderEndpoint, Provider)
|
||||
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
||||
.filter(ProviderEndpoint.id == self.endpoint_id)
|
||||
.first()
|
||||
)
|
||||
if not endpoint:
|
||||
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
|
||||
|
||||
endpoint_obj, provider = endpoint
|
||||
endpoint_format = (
|
||||
endpoint_obj.api_format
|
||||
if isinstance(endpoint_obj.api_format, str)
|
||||
else endpoint_obj.api_format.value
|
||||
)
|
||||
keys = (
|
||||
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
|
||||
.filter(ProviderAPIKey.provider_id == endpoint_obj.provider_id)
|
||||
.all()
|
||||
)
|
||||
total_keys = 0
|
||||
active_keys = 0
|
||||
for api_formats, is_active in keys:
|
||||
if endpoint_format in (api_formats or []):
|
||||
total_keys += 1
|
||||
if is_active:
|
||||
active_keys += 1
|
||||
|
||||
endpoint_dict = {
|
||||
k: v
|
||||
for k, v in endpoint_obj.__dict__.items()
|
||||
if k not in {"api_format", "_sa_instance_state", "proxy"}
|
||||
}
|
||||
return ProviderEndpointResponse(
|
||||
**endpoint_dict,
|
||||
provider_name=provider.name,
|
||||
api_format=endpoint_obj.api_format,
|
||||
proxy=mask_proxy_password(endpoint_obj.proxy),
|
||||
total_keys=total_keys,
|
||||
active_keys=active_keys,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminUpdateProviderEndpointAdapter(AdminApiAdapter):
|
||||
endpoint_id: str
|
||||
endpoint_data: ProviderEndpointUpdate
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
endpoint = (
|
||||
db.query(ProviderEndpoint).filter(ProviderEndpoint.id == self.endpoint_id).first()
|
||||
)
|
||||
if not endpoint:
|
||||
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
|
||||
|
||||
update_data = self.endpoint_data.model_dump(exclude_unset=True)
|
||||
|
||||
# 固定类型 Provider 的 endpoint:锁定 base_url/custom_path(前端禁用仅是 UX,后端必须强校验)
|
||||
provider = db.query(Provider).filter(Provider.id == endpoint.provider_id).first()
|
||||
if provider:
|
||||
provider_type = getattr(provider, "provider_type", "custom")
|
||||
if _is_fixed_provider(provider_type):
|
||||
if "base_url" in update_data or "custom_path" in update_data:
|
||||
raise InvalidRequestException(
|
||||
"固定类型 Provider 的 Endpoint 不允许修改 base_url/custom_path"
|
||||
)
|
||||
normalized_provider_type = str(provider_type or "custom").strip().lower()
|
||||
endpoint_sig = str(getattr(endpoint, "api_format", "") or "").strip().lower()
|
||||
if (
|
||||
normalized_provider_type == ProviderType.CODEX.value
|
||||
and endpoint_sig == "openai:cli"
|
||||
):
|
||||
has_config_in_payload = "config" in update_data
|
||||
cfg_payload = (
|
||||
update_data.get("config")
|
||||
if has_config_in_payload
|
||||
else getattr(endpoint, "config", None)
|
||||
)
|
||||
cfg = dict(cfg_payload) if isinstance(cfg_payload, dict) else {}
|
||||
requested = (
|
||||
cfg.get("upstream_stream_policy")
|
||||
or cfg.get("upstreamStreamPolicy")
|
||||
or cfg.get("upstream_stream")
|
||||
)
|
||||
if (
|
||||
has_config_in_payload
|
||||
and requested is not None
|
||||
and parse_upstream_stream_policy(requested)
|
||||
!= UpstreamStreamPolicy.FORCE_STREAM
|
||||
):
|
||||
raise InvalidRequestException(
|
||||
"Codex OpenAI CLI 端点固定为强制流式,不允许修改"
|
||||
)
|
||||
cfg.pop("upstreamStreamPolicy", None)
|
||||
cfg.pop("upstream_stream", None)
|
||||
cfg["upstream_stream_policy"] = "force_stream"
|
||||
update_data["config"] = cfg
|
||||
|
||||
# 把 proxy 转换为 dict 存储,支持显式设置为 None 清除代理
|
||||
if "proxy" in update_data:
|
||||
if update_data["proxy"] is not None:
|
||||
new_proxy = dict(update_data["proxy"])
|
||||
# 只有当密码字段未提供时才保留原密码(空字符串视为显式清除)
|
||||
if "password" not in new_proxy and endpoint.proxy:
|
||||
old_password = endpoint.proxy.get("password")
|
||||
if old_password:
|
||||
new_proxy["password"] = old_password
|
||||
update_data["proxy"] = new_proxy
|
||||
# proxy 为 None 时保留,用于清除代理配置
|
||||
|
||||
# JSON 列需要 flag_modified 以确保 SQLAlchemy 检测到变更
|
||||
json_fields = {"header_rules", "body_rules", "config", "proxy", "format_acceptance_config"}
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(endpoint, field, value)
|
||||
if field in json_fields:
|
||||
flag_modified(endpoint, field)
|
||||
|
||||
# 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()
|
||||
db.refresh(endpoint)
|
||||
|
||||
# 清除 /v1/models 列表缓存(is_active 变更会影响模型可用性)
|
||||
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())}"
|
||||
)
|
||||
|
||||
endpoint_format = (
|
||||
endpoint.api_format
|
||||
if isinstance(endpoint.api_format, str)
|
||||
else endpoint.api_format.value
|
||||
)
|
||||
keys = (
|
||||
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
|
||||
.filter(ProviderAPIKey.provider_id == endpoint.provider_id)
|
||||
.all()
|
||||
)
|
||||
total_keys = 0
|
||||
active_keys = 0
|
||||
for api_formats, is_active in keys:
|
||||
if endpoint_format in (api_formats or []):
|
||||
total_keys += 1
|
||||
if is_active:
|
||||
active_keys += 1
|
||||
|
||||
endpoint_dict = {
|
||||
k: v
|
||||
for k, v in endpoint.__dict__.items()
|
||||
if k not in {"api_format", "_sa_instance_state", "proxy"}
|
||||
}
|
||||
return ProviderEndpointResponse(
|
||||
**endpoint_dict,
|
||||
provider_name=provider.name if provider else "Unknown",
|
||||
api_format=endpoint.api_format,
|
||||
proxy=mask_proxy_password(endpoint.proxy),
|
||||
total_keys=total_keys,
|
||||
active_keys=active_keys,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
|
||||
endpoint_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
endpoint = (
|
||||
db.query(ProviderEndpoint).filter(ProviderEndpoint.id == self.endpoint_id).first()
|
||||
)
|
||||
if not endpoint:
|
||||
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
|
||||
|
||||
endpoint_format = (
|
||||
endpoint.api_format
|
||||
if isinstance(endpoint.api_format, str)
|
||||
else endpoint.api_format.value
|
||||
)
|
||||
|
||||
# 查询包含该格式的所有 Key,并从 api_formats 中移除该格式
|
||||
keys = (
|
||||
db.query(ProviderAPIKey)
|
||||
.filter(ProviderAPIKey.provider_id == endpoint.provider_id)
|
||||
.all()
|
||||
)
|
||||
affected_keys_count = 0
|
||||
for key in keys:
|
||||
if key.api_formats and endpoint_format in key.api_formats:
|
||||
affected_keys_count += 1
|
||||
# 移除该格式
|
||||
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")
|
||||
|
||||
db.delete(endpoint)
|
||||
db.commit()
|
||||
|
||||
# 清除 /v1/models 列表缓存
|
||||
await invalidate_models_list_cache()
|
||||
|
||||
logger.warning(
|
||||
f"[DELETE] 删除 Endpoint: ID={self.endpoint_id}, Format={endpoint_format}, "
|
||||
f"AffectedKeys={affected_keys_count}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Endpoint {self.endpoint_id} 已删除",
|
||||
"affected_keys_count": affected_keys_count,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
|
||||
api_format: str
|
||||
provider_type: str | None = None
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
try:
|
||||
normalized_api_format = parse_signature_key(self.api_format).key
|
||||
except Exception as exc:
|
||||
raise InvalidRequestException(f"无效的 api_format: {self.api_format}") from exc
|
||||
|
||||
return {
|
||||
"api_format": normalized_api_format,
|
||||
"body_rules": get_default_body_rules_for_endpoint(
|
||||
normalized_api_format, provider_type=self.provider_type
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user