Files
Aether/_deprecated_py_src/api/admin/endpoints/keys.py

438 lines
13 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
Provider API Keys 管理
2025-12-10 20:52:44 +08:00
"""
from __future__ import annotations
2025-12-10 20:52:44 +08:00
from dataclasses import dataclass
from typing import Any
2025-12-10 20:52:44 +08:00
from fastapi import APIRouter, Body, Depends, Query, Request
from pydantic import BaseModel, Field
2025-12-10 20:52:44 +08:00
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
2025-12-10 20:52:44 +08:00
from src.database import get_db
from src.models.database import User
2025-12-10 20:52:44 +08:00
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
2025-12-10 20:52:44 +08:00
router = APIRouter(tags=["Provider Keys"])
pipeline = get_pipeline()
2025-12-10 20:52:44 +08:00
2025-12-10 20:52:44 +08:00
@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 信息
"""
2025-12-10 20:52:44 +08:00
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`: 请求总数
"""
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
@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`: 操作结果消息
"""
2025-12-10 20:52:44 +08:00
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,
2025-12-10 20:52:44 +08:00
request: Request,
skip: int = Query(0, ge=0, description="跳过的记录数"),
limit: int = Query(100, ge=1, le=1000, description="返回的最大记录数"),
2025-12-10 20:52:44 +08:00
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,
)
2025-12-10 20:52:44 +08:00
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
2025-12-10 20:52:44 +08:00
为指定 Provider 添加新的 API Key支持配置多个 API 格式
2025-12-10 20:52:44 +08:00
**路径参数**:
- `provider_id`: Provider ID
2025-12-10 20:52:44 +08:00
**请求体字段**:
- `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)
2025-12-10 20:52:44 +08:00
# -------- Adapters --------
2025-12-10 20:52:44 +08:00
@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,
)
2025-12-10 20:52:44 +08:00
@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)
2025-12-10 20:52:44 +08:00
@dataclass
class AdminClearOAuthInvalidAdapter(AdminApiAdapter):
"""清除 Key 的 OAuth 失效标记。"""
2025-12-10 20:52:44 +08:00
key_id: str
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
return clear_oauth_invalid_response(context.db, self.key_id)
2025-12-10 20:52:44 +08:00
class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
return query_keys_grouped_by_format(context.db)
2025-12-10 20:52:44 +08:00
# ========== Adapters ==========
2025-12-10 20:52:44 +08:00
@dataclass
class AdminListProviderKeysAdapter(AdminApiAdapter):
"""获取 Provider 的所有 Keys"""
provider_id: str
skip: int
limit: int
2025-12-10 20:52:44 +08:00
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,
)