feat(compute): 算力中心 余额门控+充值+真实来源数值
- relay 匿名不再回落admin令牌(root无限额)→401或受限RELAY令牌(修无余额可用) - /admin/compute/user-balance(充值add/subtract/override)+审计; compute_client.adjust_user_quota - proxy follow_redirects(修gin尾斜杠301/307空数据); ping 返 version+quota_per_unit
This commit is contained in:
@@ -11,7 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..dependencies import get_db
|
||||
from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityCreateRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
|
||||
from ..schemas.operator import TaskCreateRequest, TaskUpdateRequest, TaskStatusRequest, TaskAssignRequest, TaskRecommendRequest, TaskSelectRecommendRequest, ProviderCreateRequest, ProviderUpdateRequest, ContentCreateRequest, ContentStatusRequest, ConfigUpdateRequest, ComputePingResponse, ComputeBalanceRequest, ComputeProvisionRequest, ComputeProvisionResponse, CourseCreateRequest, CourseStatusRequest, ActivityCreateRequest, ActivityStatusRequest, BookingUpdateRequest, TestCreateRequest, TestStatusRequest
|
||||
from ...rbac import require_permission, require_roles, write_audit
|
||||
from ...infrastructure.repositories import Database
|
||||
from ...services import compute_client
|
||||
@@ -274,6 +274,7 @@ async def compute_ping(
|
||||
return ComputePingResponse(
|
||||
ok=status in ("ok", "success") or "success" in status or not data.get("detail"),
|
||||
version=data.get("version"),
|
||||
quota_per_unit=data.get("quota_per_unit"),
|
||||
detail=data.get("detail"),
|
||||
)
|
||||
|
||||
@@ -308,6 +309,35 @@ async def compute_provision(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/compute/user-balance", summary="充值/调整引擎用户余额(add_quota)")
|
||||
async def compute_user_balance(
|
||||
req: ComputeBalanceRequest,
|
||||
request: Request,
|
||||
db: Database = Depends(get_db),
|
||||
actor: dict = Depends(require_roles("operator")),
|
||||
):
|
||||
"""给引擎用户充值/扣减/覆盖余额(配额单位)。
|
||||
|
||||
value 为配额单位:/api/status 的 quota_per_unit(默认 500000 = $1)。
|
||||
mode:add 充值 / subtract 扣减 / override 直接设为该值。
|
||||
引擎按用户余额计量,额度耗尽时调用 /v1 返回 403。
|
||||
"""
|
||||
if req.mode not in ("add", "subtract", "override"):
|
||||
raise HTTPException(status_code=400, detail="mode 需为 add/subtract/override")
|
||||
if req.mode != "override" and req.value <= 0:
|
||||
raise HTTPException(status_code=400, detail="调整量需大于 0")
|
||||
try:
|
||||
result = await compute_client.adjust_user_quota(req.engine_user_id, req.value, req.mode)
|
||||
except compute_client.ComputeError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"算力引擎对接失败: {exc}") from exc
|
||||
await write_audit(db, action="compute.balance", resource="compute",
|
||||
resource_id=str(req.engine_user_id),
|
||||
detail=f"{req.mode} {req.value} quota" + (f" ({req.reason})" if req.reason else ""),
|
||||
user=actor, request=request)
|
||||
return {"ok": result.get("success", True), "engine_user_id": req.engine_user_id,
|
||||
"mode": req.mode, "value": req.value, "message": result.get("message", "")}
|
||||
|
||||
|
||||
@router.post("/compute/sync-users", summary="同步引擎用户=平台总用户(对账)")
|
||||
async def sync_compute_users(
|
||||
request: Request,
|
||||
|
||||
+17
-11
@@ -22,17 +22,23 @@ from ...services import compute_catalog
|
||||
router = APIRouter(prefix="/v1", tags=["relay"])
|
||||
|
||||
|
||||
def _auth_headers(request: Request | None = None) -> dict[str, str]:
|
||||
# 优先透传客户端 Authorization(用户算力 PAT → compute 归户计量)。
|
||||
# 无则回落服务端消费令牌(COMPUTE_RELAY_TOKEN)→ compute 计匿名账。
|
||||
if request is not None:
|
||||
client_auth = request.headers.get("Authorization", "")
|
||||
if client_auth:
|
||||
return {"Authorization": client_auth}
|
||||
token = config.COMPUTE_RELAY_TOKEN or config.COMPUTE_ADMIN_TOKEN
|
||||
if not token:
|
||||
return {}
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
def _auth_headers(request: Request) -> dict[str, str]:
|
||||
"""模型调用鉴权:把请求归到「真实用户令牌」,由引擎按该用户余额计量。
|
||||
|
||||
- 优先透传客户端 Authorization(用户算力 PAT → compute 归户计量,引擎按其余额判定)。
|
||||
- 无用户令牌:仅回落受限消费令牌 COMPUTE_RELAY_TOKEN(引擎按该受限用户余额计量)。
|
||||
- 二者皆无:**拒绝匿名**——绝不回落 COMPUTE_ADMIN_TOKEN(root 无限额度),
|
||||
否则「用户无余额仍能免费使用」。(引擎用户额度耗尽会返回 403,见 billing_session)。
|
||||
"""
|
||||
client_auth = request.headers.get("Authorization", "")
|
||||
if client_auth:
|
||||
return {"Authorization": client_auth}
|
||||
if config.COMPUTE_RELAY_TOKEN:
|
||||
return {"Authorization": f"Bearer {config.COMPUTE_RELAY_TOKEN}"}
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="缺少算力令牌:请携带有效的用户算力令牌(Bearer <PAT>)调用",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/chat/completions")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class TaskCreateRequest(BaseModel):
|
||||
title: str
|
||||
@@ -168,9 +168,19 @@ class TestStatusRequest(BaseModel):
|
||||
class ComputePingResponse(BaseModel):
|
||||
ok: bool
|
||||
version: str | None = None
|
||||
quota_per_unit: float | None = None # 引擎 1 元 = 多少配额(500000 = $1),来自 /api/status
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
class ComputeBalanceRequest(BaseModel):
|
||||
"""充值/调整引擎用户余额。``value`` 为配额单位(见 quota_per_unit)。"""
|
||||
|
||||
engine_user_id: int
|
||||
value: int = Field(ge=0, description="调整量(配额单位),add/subtract 用")
|
||||
mode: str = "add" # add | subtract | override
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class ComputeProvisionRequest(BaseModel):
|
||||
user_id: str
|
||||
|
||||
|
||||
Reference in New Issue
Block a user