All checks were successful
continuous-integration/drone/push Build is passing
- 新增告警模块 (alerts): 告警规则配置与触发 - 新增成本管理模块 (cost): 成本统计与分析 - 新增配额模块 (quota): 配额管理与限制 - 新增微信模块 (wechat): 微信相关功能接口 - 新增缓存服务 (cache): Redis 缓存封装 - 新增请求日志中间件 (request_logger) - 新增异常处理和链路追踪中间件 - 更新 dashboard 前端展示 - 更新 SDK stats_client 功能
334 lines
10 KiB
Python
334 lines
10 KiB
Python
"""费用管理路由"""
|
|
from typing import Optional, List
|
|
from decimal import Decimal
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import desc
|
|
|
|
from ..database import get_db
|
|
from ..models.pricing import ModelPricing, TenantBilling
|
|
from ..services.cost import CostCalculator
|
|
from .auth import get_current_user, require_operator
|
|
from ..models.user import User
|
|
|
|
router = APIRouter(prefix="/cost", tags=["费用管理"])
|
|
|
|
|
|
# ============= Schemas =============
|
|
|
|
class ModelPricingCreate(BaseModel):
|
|
model_name: str
|
|
provider: Optional[str] = None
|
|
display_name: Optional[str] = None
|
|
input_price_per_1k: float = 0
|
|
output_price_per_1k: float = 0
|
|
fixed_price_per_call: float = 0
|
|
pricing_type: str = "token"
|
|
description: Optional[str] = None
|
|
|
|
|
|
class ModelPricingUpdate(BaseModel):
|
|
provider: Optional[str] = None
|
|
display_name: Optional[str] = None
|
|
input_price_per_1k: Optional[float] = None
|
|
output_price_per_1k: Optional[float] = None
|
|
fixed_price_per_call: Optional[float] = None
|
|
pricing_type: Optional[str] = None
|
|
description: Optional[str] = None
|
|
status: Optional[int] = None
|
|
|
|
|
|
class CostCalculateRequest(BaseModel):
|
|
model_name: str
|
|
input_tokens: int = 0
|
|
output_tokens: int = 0
|
|
|
|
|
|
# ============= Model Pricing API =============
|
|
|
|
@router.get("/pricing")
|
|
async def list_model_pricing(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(50, ge=1, le=100),
|
|
provider: Optional[str] = None,
|
|
status: Optional[int] = None,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""获取模型价格配置列表"""
|
|
query = db.query(ModelPricing)
|
|
|
|
if provider:
|
|
query = query.filter(ModelPricing.provider == provider)
|
|
if status is not None:
|
|
query = query.filter(ModelPricing.status == status)
|
|
|
|
total = query.count()
|
|
items = query.order_by(ModelPricing.model_name).offset((page - 1) * size).limit(size).all()
|
|
|
|
return {
|
|
"total": total,
|
|
"page": page,
|
|
"size": size,
|
|
"items": [format_pricing(p) for p in items]
|
|
}
|
|
|
|
|
|
@router.get("/pricing/{pricing_id}")
|
|
async def get_model_pricing(
|
|
pricing_id: int,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""获取模型价格详情"""
|
|
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
|
|
if not pricing:
|
|
raise HTTPException(status_code=404, detail="模型价格配置不存在")
|
|
return format_pricing(pricing)
|
|
|
|
|
|
@router.post("/pricing")
|
|
async def create_model_pricing(
|
|
data: ModelPricingCreate,
|
|
user: User = Depends(require_operator),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""创建模型价格配置"""
|
|
# 检查是否已存在
|
|
existing = db.query(ModelPricing).filter(ModelPricing.model_name == data.model_name).first()
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="该模型价格配置已存在")
|
|
|
|
pricing = ModelPricing(
|
|
model_name=data.model_name,
|
|
provider=data.provider,
|
|
display_name=data.display_name,
|
|
input_price_per_1k=Decimal(str(data.input_price_per_1k)),
|
|
output_price_per_1k=Decimal(str(data.output_price_per_1k)),
|
|
fixed_price_per_call=Decimal(str(data.fixed_price_per_call)),
|
|
pricing_type=data.pricing_type,
|
|
description=data.description,
|
|
status=1
|
|
)
|
|
db.add(pricing)
|
|
db.commit()
|
|
db.refresh(pricing)
|
|
|
|
return {"success": True, "id": pricing.id}
|
|
|
|
|
|
@router.put("/pricing/{pricing_id}")
|
|
async def update_model_pricing(
|
|
pricing_id: int,
|
|
data: ModelPricingUpdate,
|
|
user: User = Depends(require_operator),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""更新模型价格配置"""
|
|
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
|
|
if not pricing:
|
|
raise HTTPException(status_code=404, detail="模型价格配置不存在")
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
|
|
# 转换价格字段
|
|
for field in ['input_price_per_1k', 'output_price_per_1k', 'fixed_price_per_call']:
|
|
if field in update_data and update_data[field] is not None:
|
|
update_data[field] = Decimal(str(update_data[field]))
|
|
|
|
for key, value in update_data.items():
|
|
setattr(pricing, key, value)
|
|
|
|
db.commit()
|
|
return {"success": True}
|
|
|
|
|
|
@router.delete("/pricing/{pricing_id}")
|
|
async def delete_model_pricing(
|
|
pricing_id: int,
|
|
user: User = Depends(require_operator),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""删除模型价格配置"""
|
|
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
|
|
if not pricing:
|
|
raise HTTPException(status_code=404, detail="模型价格配置不存在")
|
|
|
|
db.delete(pricing)
|
|
db.commit()
|
|
return {"success": True}
|
|
|
|
|
|
# ============= Cost Calculation API =============
|
|
|
|
@router.post("/calculate")
|
|
async def calculate_cost(
|
|
request: CostCalculateRequest,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""计算调用费用"""
|
|
calculator = CostCalculator(db)
|
|
cost = calculator.calculate_cost(
|
|
model_name=request.model_name,
|
|
input_tokens=request.input_tokens,
|
|
output_tokens=request.output_tokens
|
|
)
|
|
|
|
return {
|
|
"model": request.model_name,
|
|
"input_tokens": request.input_tokens,
|
|
"output_tokens": request.output_tokens,
|
|
"cost": float(cost)
|
|
}
|
|
|
|
|
|
@router.get("/summary")
|
|
async def get_cost_summary(
|
|
tenant_id: Optional[str] = None,
|
|
start_date: Optional[str] = None,
|
|
end_date: Optional[str] = None,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""获取费用汇总"""
|
|
calculator = CostCalculator(db)
|
|
return calculator.get_cost_summary(
|
|
tenant_id=tenant_id,
|
|
start_date=start_date,
|
|
end_date=end_date
|
|
)
|
|
|
|
|
|
@router.get("/by-tenant")
|
|
async def get_cost_by_tenant(
|
|
start_date: Optional[str] = None,
|
|
end_date: Optional[str] = None,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""按租户统计费用"""
|
|
calculator = CostCalculator(db)
|
|
return calculator.get_cost_by_tenant(
|
|
start_date=start_date,
|
|
end_date=end_date
|
|
)
|
|
|
|
|
|
@router.get("/by-model")
|
|
async def get_cost_by_model(
|
|
tenant_id: Optional[str] = None,
|
|
start_date: Optional[str] = None,
|
|
end_date: Optional[str] = None,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""按模型统计费用"""
|
|
calculator = CostCalculator(db)
|
|
return calculator.get_cost_by_model(
|
|
tenant_id=tenant_id,
|
|
start_date=start_date,
|
|
end_date=end_date
|
|
)
|
|
|
|
|
|
# ============= Billing API =============
|
|
|
|
@router.get("/billing")
|
|
async def list_billing(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(20, ge=1, le=100),
|
|
tenant_id: Optional[str] = None,
|
|
billing_month: Optional[str] = None,
|
|
user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""获取账单列表"""
|
|
query = db.query(TenantBilling)
|
|
|
|
if tenant_id:
|
|
query = query.filter(TenantBilling.tenant_id == tenant_id)
|
|
if billing_month:
|
|
query = query.filter(TenantBilling.billing_month == billing_month)
|
|
|
|
total = query.count()
|
|
items = query.order_by(desc(TenantBilling.billing_month)).offset((page - 1) * size).limit(size).all()
|
|
|
|
return {
|
|
"total": total,
|
|
"page": page,
|
|
"size": size,
|
|
"items": [format_billing(b) for b in items]
|
|
}
|
|
|
|
|
|
@router.post("/billing/generate")
|
|
async def generate_billing(
|
|
tenant_id: str = Query(...),
|
|
billing_month: str = Query(..., description="格式: YYYY-MM"),
|
|
user: User = Depends(require_operator),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""生成月度账单"""
|
|
calculator = CostCalculator(db)
|
|
billing = calculator.generate_monthly_billing(tenant_id, billing_month)
|
|
|
|
return {
|
|
"success": True,
|
|
"billing": format_billing(billing)
|
|
}
|
|
|
|
|
|
@router.post("/recalculate")
|
|
async def recalculate_costs(
|
|
start_date: Optional[str] = None,
|
|
end_date: Optional[str] = None,
|
|
user: User = Depends(require_operator),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""重新计算事件费用"""
|
|
calculator = CostCalculator(db)
|
|
updated = calculator.update_event_costs(start_date, end_date)
|
|
|
|
return {
|
|
"success": True,
|
|
"updated_count": updated
|
|
}
|
|
|
|
|
|
# ============= Helper Functions =============
|
|
|
|
def format_pricing(pricing: ModelPricing) -> dict:
|
|
return {
|
|
"id": pricing.id,
|
|
"model_name": pricing.model_name,
|
|
"provider": pricing.provider,
|
|
"display_name": pricing.display_name,
|
|
"input_price_per_1k": float(pricing.input_price_per_1k or 0),
|
|
"output_price_per_1k": float(pricing.output_price_per_1k or 0),
|
|
"fixed_price_per_call": float(pricing.fixed_price_per_call or 0),
|
|
"pricing_type": pricing.pricing_type,
|
|
"description": pricing.description,
|
|
"status": pricing.status,
|
|
"created_at": pricing.created_at,
|
|
"updated_at": pricing.updated_at
|
|
}
|
|
|
|
|
|
def format_billing(billing: TenantBilling) -> dict:
|
|
return {
|
|
"id": billing.id,
|
|
"tenant_id": billing.tenant_id,
|
|
"billing_month": billing.billing_month,
|
|
"total_calls": billing.total_calls,
|
|
"total_input_tokens": billing.total_input_tokens,
|
|
"total_output_tokens": billing.total_output_tokens,
|
|
"total_cost": float(billing.total_cost or 0),
|
|
"cost_by_model": billing.cost_by_model,
|
|
"cost_by_app": billing.cost_by_app,
|
|
"status": billing.status,
|
|
"created_at": billing.created_at,
|
|
"updated_at": billing.updated_at
|
|
}
|