Files
Aether/_deprecated_py_src/api/admin/endpoints/keys.py
fawney19 1d9c77522a 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)
2026-04-03 16:26:16 +08:00

438 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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,
)