2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
2026-01-10 18:43:53 +08:00
|
|
|
|
Provider API Keys 管理
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from dataclasses import dataclass
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from typing import Any
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-03-07 03:49:44 +08:00
|
|
|
|
from fastapi import APIRouter, Body, Depends, Query, Request
|
2026-03-03 11:32:41 +08:00
|
|
|
|
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
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from src.api.base.context import ApiRequestContext
|
2026-03-14 11:59:07 +08:00
|
|
|
|
from src.api.base.pipeline import get_pipeline
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.database import get_db
|
2026-02-28 14:09:36 +08:00
|
|
|
|
from src.models.database import User
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.models.endpoint_models import (
|
|
|
|
|
|
EndpointAPIKeyCreate,
|
|
|
|
|
|
EndpointAPIKeyResponse,
|
|
|
|
|
|
EndpointAPIKeyUpdate,
|
|
|
|
|
|
)
|
2026-02-28 14:09:36 +08:00
|
|
|
|
from src.services.provider_keys import (
|
2026-03-07 03:49:44 +08:00
|
|
|
|
batch_delete_endpoint_keys_response,
|
2026-02-28 14:09:36 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
2026-03-10 09:36:47 +08:00
|
|
|
|
from src.services.provider_keys.key_quota_service import (
|
|
|
|
|
|
CODEX_WHAM_USAGE_URL as _CODEX_WHAM_USAGE_URL,
|
|
|
|
|
|
)
|
2026-02-06 16:37:06 +08:00
|
|
|
|
from src.utils.auth_utils import require_admin
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
router = APIRouter(tags=["Provider Keys"])
|
2026-03-14 11:59:07 +08:00
|
|
|
|
pipeline = get_pipeline()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-14 13:12:19 +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:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
2026-01-10 18:43:53 +08:00
|
|
|
|
更新 Provider Key
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
更新指定 Key 的配置,支持修改并发限制、速率倍数、优先级、
|
|
|
|
|
|
配额限制、能力限制等。支持部分更新。
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `key_id`: Key ID
|
|
|
|
|
|
|
|
|
|
|
|
**请求体字段**(均为可选):
|
|
|
|
|
|
- `api_key`: 新的 API Key 原文
|
|
|
|
|
|
- `name`: Key 名称
|
|
|
|
|
|
- `note`: 备注
|
2026-01-16 17:53:27 +08:00
|
|
|
|
- `rate_multipliers`: 按 API 格式的成本倍率
|
2026-01-07 14:55:07 +08:00
|
|
|
|
- `internal_priority`: 内部优先级
|
2026-01-10 18:43:53 +08:00
|
|
|
|
- `rpm_limit`: RPM 限制(设置为 null 可切换到自适应模式)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
- `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:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取按 API 格式分组的所有 Keys
|
|
|
|
|
|
|
|
|
|
|
|
获取所有活跃的 Key,按 API 格式分组返回,用于全局优先级管理。
|
|
|
|
|
|
每个 Key 包含基本信息、健康度指标、能力标签等。
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- 返回一个字典,键为 API 格式,值为该格式下的 Key 列表
|
|
|
|
|
|
- 每个 Key 包含:
|
|
|
|
|
|
- `id`: Key ID
|
|
|
|
|
|
- `name`: Key 名称
|
|
|
|
|
|
- `api_key_masked`: 脱敏后的 API Key
|
|
|
|
|
|
- `internal_priority`: 内部优先级
|
2026-01-16 17:53:27 +08:00
|
|
|
|
- `global_priority_by_format`: 按 API 格式的全局优先级
|
|
|
|
|
|
- `format_priority`: 当前格式的优先级
|
|
|
|
|
|
- `rate_multipliers`: 按 API 格式的成本倍率
|
2026-01-07 14:55:07 +08:00
|
|
|
|
- `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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-29 17:14:26 +08:00
|
|
|
|
@router.get("/keys/{key_id}/reveal")
|
|
|
|
|
|
async def reveal_endpoint_key(
|
|
|
|
|
|
key_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> dict:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取完整的 API Key
|
|
|
|
|
|
|
|
|
|
|
|
解密并返回指定 Key 的完整原文,用于查看和复制。
|
|
|
|
|
|
此操作会被记录到审计日志。
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `key_id`: Key ID
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `api_key`: 完整的 API Key 原文
|
|
|
|
|
|
"""
|
2025-12-29 17:14:26 +08:00
|
|
|
|
adapter = AdminRevealEndpointKeyAdapter(key_id=key_id)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 01:05:48 +08:00
|
|
|
|
@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:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
2026-01-10 18:43:53 +08:00
|
|
|
|
删除 Provider Key
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
删除指定的 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 03:49:44 +08:00
|
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
@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 状态,
|
|
|
|
|
|
通常在管理员确认账号已完成验证后使用。
|
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
|
这是 admin/status 维修入口,不是 AI 运行时请求恢复路径。
|
|
|
|
|
|
Rust 热路径迁移完成后,这里仍负责人工解除 OAuth invalid 标记。
|
|
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `key_id`: Key ID
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `message`: 操作结果消息
|
|
|
|
|
|
"""
|
2026-02-28 14:09:36 +08:00
|
|
|
|
adapter = AdminClearOAuthInvalidAdapter(key_id=key_id)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
2026-02-06 16:37:06 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# ========== Provider Keys API ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
@router.get("/providers/{provider_id}/keys", response_model=list[EndpointAPIKeyResponse])
|
2026-01-10 18:43:53 +08:00
|
|
|
|
async def list_provider_keys(
|
|
|
|
|
|
provider_id: str,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
request: Request,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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),
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> list[EndpointAPIKeyResponse]:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
2026-01-10 18:43:53 +08:00
|
|
|
|
获取 Provider 的所有 Keys
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
获取指定 Provider 下的所有 API Key 列表,支持多 API 格式。
|
|
|
|
|
|
结果按优先级和创建时间排序。
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
- `provider_id`: Provider ID
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
**查询参数**:
|
|
|
|
|
|
- `skip`: 跳过的记录数,用于分页(默认 0)
|
|
|
|
|
|
- `limit`: 返回的最大记录数(1-1000,默认 100)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
@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
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
为指定 Provider 添加新的 API Key,支持配置多个 API 格式。
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `provider_id`: Provider ID
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +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
|
|
|
|
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# -------- Adapters --------
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminUpdateEndpointKeyAdapter(AdminApiAdapter):
|
|
|
|
|
|
key_id: str
|
|
|
|
|
|
key_data: EndpointAPIKeyUpdate
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return await update_endpoint_key_response(
|
|
|
|
|
|
db=context.db,
|
|
|
|
|
|
key_id=self.key_id,
|
|
|
|
|
|
key_data=self.key_data,
|
2026-01-15 10:32:48 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2025-12-29 17:14:26 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminRevealEndpointKeyAdapter(AdminApiAdapter):
|
2026-01-30 02:43:50 +08:00
|
|
|
|
"""获取完整的 API Key 或 Auth Config(用于查看和复制)"""
|
2025-12-29 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
key_id: str
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return reveal_endpoint_key_payload(context.db, self.key_id)
|
2025-12-29 17:14:26 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 01:05:48 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminExportKeyAdapter(AdminApiAdapter):
|
|
|
|
|
|
"""导出 OAuth Key 凭据:解密 auth_config,委托 provider-specific builder 构建导出数据。"""
|
|
|
|
|
|
|
|
|
|
|
|
key_id: str
|
|
|
|
|
|
|
|
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return export_oauth_key_data(context.db, self.key_id)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-28 14:09:36 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminDeleteEndpointKeyAdapter(AdminApiAdapter):
|
|
|
|
|
|
key_id: str
|
2026-02-09 01:05:48 +08:00
|
|
|
|
|
2026-02-28 14:09:36 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
|
|
|
|
|
return await delete_endpoint_key_response(db=context.db, key_id=self.key_id)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 03:49:44 +08:00
|
|
|
|
@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
|
2026-02-28 14:09:36 +08:00
|
|
|
|
class AdminClearOAuthInvalidAdapter(AdminApiAdapter):
|
|
|
|
|
|
"""清除 Key 的 OAuth 失效标记。"""
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
key_id: str
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return clear_oauth_invalid_response(context.db, self.key_id)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdminGetKeysGroupedByFormatAdapter(AdminApiAdapter):
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return query_keys_grouped_by_format(context.db)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# ========== Adapters ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@dataclass
|
2026-01-10 18:43:53 +08:00
|
|
|
|
class AdminListProviderKeysAdapter(AdminApiAdapter):
|
|
|
|
|
|
"""获取 Provider 的所有 Keys"""
|
|
|
|
|
|
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
|
skip: int
|
|
|
|
|
|
limit: int
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return list_provider_keys_responses(context.db, self.provider_id, self.skip, self.limit)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminCreateProviderKeyAdapter(AdminApiAdapter):
|
|
|
|
|
|
"""为 Provider 添加 Key"""
|
|
|
|
|
|
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
|
key_data: EndpointAPIKeyCreate
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return await create_provider_key_response(
|
|
|
|
|
|
db=context.db,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
provider_id=self.provider_id,
|
2026-02-28 14:09:36 +08:00
|
|
|
|
key_data=self.key_data,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-05 18:09:14 +08:00
|
|
|
|
|
2026-03-10 09:36:47 +08:00
|
|
|
|
# ========== Quota Refresh API ==========
|
2026-02-05 18:05:46 +08:00
|
|
|
|
|
2026-02-05 18:09:14 +08:00
|
|
|
|
|
2026-03-03 11:32:41 +08:00
|
|
|
|
class RefreshProviderQuotaRequest(BaseModel):
|
|
|
|
|
|
key_ids: list[str] | None = Field(default=None, description="仅刷新指定 Key 列表(可选)")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-05 18:09:14 +08:00
|
|
|
|
@router.post("/providers/{provider_id}/refresh-quota")
|
|
|
|
|
|
async def refresh_provider_quota(
|
|
|
|
|
|
provider_id: str,
|
|
|
|
|
|
request: Request,
|
2026-03-03 11:32:41 +08:00
|
|
|
|
payload: RefreshProviderQuotaRequest | None = None,
|
2026-02-05 18:09:14 +08:00
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
"""
|
2026-02-09 01:05:48 +08:00
|
|
|
|
刷新 Provider 所有 Keys 的限额信息
|
2026-02-05 18:09:14 +08:00
|
|
|
|
|
2026-02-09 01:05:48 +08:00
|
|
|
|
支持的 Provider 类型:
|
|
|
|
|
|
- Codex: 调用 wham/usage API 获取限额
|
|
|
|
|
|
- Antigravity: 调用 fetchAvailableModels 获取配额
|
|
|
|
|
|
- Kiro: 调用 getUsageLimits API 获取使用额度
|
2026-02-05 18:09:14 +08:00
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `provider_id`: Provider ID
|
2026-03-03 11:32:41 +08:00
|
|
|
|
**请求体**(可选):
|
|
|
|
|
|
- `key_ids`: 仅刷新指定 Key 列表,不传时刷新所有活跃 Key
|
2026-02-05 18:09:14 +08:00
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `success`: 成功刷新的 Key 数量
|
|
|
|
|
|
- `failed`: 失败的 Key 数量
|
|
|
|
|
|
- `results`: 每个 Key 的刷新结果
|
|
|
|
|
|
"""
|
2026-03-03 11:32:41 +08:00
|
|
|
|
adapter = AdminRefreshProviderQuotaAdapter(
|
|
|
|
|
|
provider_id=provider_id,
|
|
|
|
|
|
key_ids=payload.key_ids if payload else None,
|
|
|
|
|
|
)
|
2026-02-05 18:09:14 +08:00
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
|
|
|
|
|
"""刷新 Provider 所有 Keys 的限额信息"""
|
|
|
|
|
|
|
|
|
|
|
|
provider_id: str
|
2026-03-03 11:32:41 +08:00
|
|
|
|
key_ids: list[str] | None = None
|
2026-02-05 18:09:14 +08:00
|
|
|
|
|
|
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2026-02-28 14:09:36 +08:00
|
|
|
|
return await refresh_provider_quota_for_provider(
|
|
|
|
|
|
db=context.db,
|
|
|
|
|
|
provider_id=self.provider_id,
|
2026-03-10 09:36:47 +08:00
|
|
|
|
codex_wham_usage_url=_CODEX_WHAM_USAGE_URL,
|
2026-03-03 11:32:41 +08:00
|
|
|
|
key_ids=self.key_ids,
|
2026-02-05 18:09:14 +08:00
|
|
|
|
)
|