forked from xiaohei/taiji-AI-PAD
更新大志备注
This commit is contained in:
@@ -46,21 +46,27 @@ def create_app() -> FastAPI:
|
||||
if any(request.url.path.startswith(p) for p in skip_paths):
|
||||
return await call_next(request)
|
||||
|
||||
# 认证逻辑:在独立的数据库会话中完成,不要包裹 call_next
|
||||
principal = None
|
||||
try:
|
||||
async with AsyncSessionLocal() as session:
|
||||
principal = await authenticate_request(request, session)
|
||||
if principal:
|
||||
request.state.principal = principal
|
||||
elif request.url.path.startswith("/api"):
|
||||
# Allow unauthenticated access for checklist placeholder APIs while keeping
|
||||
# any provided principal for future auth-enabled endpoints.
|
||||
request.state.principal = {}
|
||||
response = await call_next(request)
|
||||
return response
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).error(f"Auth middleware error: {e}")
|
||||
logging.getLogger(__name__).error(f"Auth middleware error during authentication: {e}")
|
||||
principal = None
|
||||
|
||||
# 设置 principal 到 request.state
|
||||
if principal:
|
||||
request.state.principal = principal
|
||||
elif request.url.path.startswith("/api") or request.url.path.startswith("/agents"):
|
||||
# Allow unauthenticated access for checklist placeholder APIs while keeping
|
||||
# any provided principal for future auth-enabled endpoints.
|
||||
request.state.principal = {}
|
||||
return await call_next(request)
|
||||
else:
|
||||
request.state.principal = {}
|
||||
|
||||
# call_next 在数据库会话关闭后调用,避免长时间持有连接
|
||||
return await call_next(request)
|
||||
|
||||
return app
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
"""
|
||||
计费与资源管理逻辑
|
||||
|
||||
EU(执行单元)计算说明:
|
||||
=======================
|
||||
|
||||
系统中有两种EU计算方式:
|
||||
|
||||
1. Agent 运行时长计费(本模块)
|
||||
- 计算公式:EU = ceil(duration_seconds / 10)
|
||||
- 即:1 EU = 10秒运行时间,不足10秒按1 EU计算
|
||||
- 适用于:平台Agent、自定义Agent的Pod运行时长
|
||||
- 周期性统计:每小时更新一次计费记录(periodic_billing.py)
|
||||
|
||||
2. 模型 Token 计费(billing_webhook.py)
|
||||
- 计算公式:EU = total_tokens * MODEL_EU_RATE[model_name]
|
||||
- 不同模型有不同的转换率:
|
||||
- gpt-4: 0.0001 EU/token (1000 tokens = 0.1 EU)
|
||||
- gpt-3.5-turbo: 0.00005 EU/token (1000 tokens = 0.05 EU)
|
||||
- claude: 0.0001 EU/token
|
||||
- 默认: 0.0001 EU/token
|
||||
- 适用于:LiteLLM模型调用的Token消耗
|
||||
|
||||
数据存储:
|
||||
- Agent计费记录:agent_billing_records 表
|
||||
- 模型计费记录:model_billing_records 表
|
||||
- 用户余额:balances 表(Balance模型)
|
||||
|
||||
注意:User.eu_balance 和 User.balance 字段已废弃,请使用 Balance 表
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
周期性计费任务模块
|
||||
|
||||
功能:
|
||||
1. 定时更新运行中 Agent 的 EU 消耗和成本
|
||||
2. 周期性扣款(每小时)
|
||||
3. 支持平台 Agent 和自定义 Agent
|
||||
1. 定时更新运行中 Agent 的计费日志(agent_billing_records 表)
|
||||
2. 增量扣款:只扣除上次计费后新增的费用,避免重复扣款
|
||||
3. 余额不足时自动停止 Agent
|
||||
|
||||
计费规则:
|
||||
- VM计算: 0.5 EU/hour
|
||||
- 平台 Agent: 按模板固定价格计费
|
||||
为什么需要周期性统计:
|
||||
- Agent 可能长时间运行,需要定期更新计费记录以便用户查看实时消费
|
||||
- 避免 Agent 结束回调失败导致计费记录不准确
|
||||
- 及时发现余额不足的用户并停止其 Agent,防止欠费
|
||||
|
||||
EU计算(Agent运行时长):
|
||||
- 公式:EU = ceil(duration_seconds / 10)
|
||||
- 即:1 EU = 10秒运行时间,不足10秒按1 EU计算
|
||||
|
||||
成本计算:
|
||||
- 平台 Agent: 按模板固定价格计费($/小时)
|
||||
- 自定义 Agent: 按资源使用量(CPU/内存)计费
|
||||
|
||||
执行周期:每小时执行一次(BILLING_INTERVAL_SECONDS = 3600)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -551,7 +551,7 @@ async def get_admin_dashboard_stats(
|
||||
"totalTenants": total_tenants,
|
||||
"totalAgents": total_agents,
|
||||
"totalCalls": total_calls,
|
||||
"totalRevenue": total_revenue,
|
||||
"totalRevenue": round(total_revenue, 2),
|
||||
# 总资源分配(平台端 + 自定义)
|
||||
"totalAllocatedCpu": round(total_cpu, 2),
|
||||
"totalAllocatedMemory": round(total_memory, 2),
|
||||
@@ -1543,42 +1543,60 @@ async def allocate_channel_resources(
|
||||
|
||||
# LiteLLM 更新成功后,更新本地数据库
|
||||
|
||||
# 删除现有模型资源分配
|
||||
# 分配模型资源(追加模式:只添加新模型,保留已有模型)
|
||||
# 存储每个模型名称,而不是供应商 ID
|
||||
# 这样渠道给租户分配模型时才能正确验证权限
|
||||
from sqlalchemy import delete
|
||||
await db.execute(
|
||||
delete(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == channel_id,
|
||||
ResourceAllocation.target_type == "channel",
|
||||
ResourceAllocation.resource_type == "model"
|
||||
for model_name in all_model_names:
|
||||
# 检查是否已存在该模型的分配
|
||||
existing_model_result = await db.execute(
|
||||
select(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == channel_id,
|
||||
ResourceAllocation.target_type == "channel",
|
||||
ResourceAllocation.resource_type == "model",
|
||||
ResourceAllocation.resource_id == model_name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 分配模型资源(存储每个模型名称,而不是供应商 ID)
|
||||
# 这样渠道给租户分配模型时才能正确验证权限
|
||||
for model_name in all_model_names:
|
||||
allocation = ResourceAllocation(
|
||||
target_id=channel_id,
|
||||
target_type="channel",
|
||||
resource_type="model",
|
||||
resource_id=model_name, # 存储模型名称(如 gpt-4o),而不是供应商 ID
|
||||
)
|
||||
db.add(allocation)
|
||||
existing_model = existing_model_result.scalar_one_or_none()
|
||||
|
||||
if not existing_model:
|
||||
# 只有不存在时才添加新分配
|
||||
allocation = ResourceAllocation(
|
||||
target_id=channel_id,
|
||||
target_type="channel",
|
||||
resource_type="model",
|
||||
resource_id=model_name, # 存储模型名称(如 gpt-4o),而不是供应商 ID
|
||||
)
|
||||
db.add(allocation)
|
||||
logger.info(f"追加模型分配给渠道 {channel.name}: model={model_name}")
|
||||
else:
|
||||
logger.info(f"模型 {model_name} 已分配给渠道 {channel.name},跳过重复分配")
|
||||
|
||||
# Bug 修复:同时创建 ChannelProviderAccess 记录,确保渠道能看到分配的供应商
|
||||
# 删除旧的供应商访问记录
|
||||
await db.execute(
|
||||
delete(ChannelProviderAccess).where(
|
||||
ChannelProviderAccess.channel_id == channel_id
|
||||
)
|
||||
)
|
||||
|
||||
# 为每个供应商创建访问权限记录
|
||||
# 追加模式:只添加新的供应商访问权限,保留已有的
|
||||
user_id = principal.get("claims", {}).get("user_id") or principal.get("sub")
|
||||
for provider_id in provider_ids:
|
||||
try:
|
||||
provider_uuid = uuid.UUID(provider_id)
|
||||
|
||||
# 检查是否已存在该供应商的访问权限
|
||||
existing_access_result = await db.execute(
|
||||
select(ChannelProviderAccess).where(
|
||||
and_(
|
||||
ChannelProviderAccess.channel_id == channel_id,
|
||||
ChannelProviderAccess.provider_id == provider_uuid
|
||||
)
|
||||
)
|
||||
)
|
||||
existing_access = existing_access_result.scalar_one_or_none()
|
||||
|
||||
if existing_access:
|
||||
# 已存在,跳过重复分配
|
||||
logger.info(f"供应商 {provider_id} 的访问权限已存在于渠道 {channel.name},跳过重复分配")
|
||||
continue
|
||||
|
||||
# 查询供应商信息
|
||||
provider_result = await db.execute(
|
||||
select(ModelProvider).where(ModelProvider.id == provider_uuid)
|
||||
@@ -1597,70 +1615,86 @@ async def allocate_channel_resources(
|
||||
approved_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(access)
|
||||
logger.info(f"创建渠道 {channel.name} 的供应商访问权限: {provider.name}")
|
||||
logger.info(f"追加渠道 {channel.name} 的供应商访问权限: {provider.name}")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"无法为供应商 {provider_id} 创建访问权限: {e}")
|
||||
|
||||
# 删除现有 Agent 资源分配
|
||||
await db.execute(
|
||||
delete(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == channel_id,
|
||||
ResourceAllocation.target_type == "channel",
|
||||
ResourceAllocation.resource_type == "agent"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# 分配Agent资源
|
||||
# 删除现有平台 Agent 配额
|
||||
await db.execute(
|
||||
delete(PlatformAgentQuota).where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == channel_id,
|
||||
PlatformAgentQuota.target_type == "channel"
|
||||
)
|
||||
)
|
||||
)
|
||||
# 分配Agent资源(追加模式:在现有配额基础上增加)
|
||||
# 不再删除现有配额,改为追加
|
||||
|
||||
user_id_for_allocation = principal.get("claims", {}).get("user_id") or principal.get("sub")
|
||||
for agent_alloc in req.agents:
|
||||
# 1. 创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
allocation = ResourceAllocation(
|
||||
target_id=channel_id,
|
||||
target_type="channel",
|
||||
resource_type="agent",
|
||||
resource_id=agent_alloc.agentId,
|
||||
quantity=agent_alloc.quantity,
|
||||
# 1. 查找现有的平台 Agent 配额
|
||||
existing_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota).where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == channel_id,
|
||||
PlatformAgentQuota.target_type == "channel",
|
||||
PlatformAgentQuota.template_name == agent_alloc.agentId
|
||||
)
|
||||
)
|
||||
)
|
||||
db.add(allocation)
|
||||
existing_quota = existing_quota_result.scalar_one_or_none()
|
||||
|
||||
# 2. Bug 修复:同时创建 PlatformAgentQuota 记录,确保渠道能看到配额
|
||||
quota = PlatformAgentQuota(
|
||||
target_id=channel_id,
|
||||
target_type="channel",
|
||||
template_name=agent_alloc.agentId, # agentId 是模板名称
|
||||
pod_quota=agent_alloc.quantity,
|
||||
pod_used=0,
|
||||
allocated_by=uuid.UUID(user_id_for_allocation) if user_id_for_allocation else None,
|
||||
allocated_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(quota)
|
||||
if existing_quota:
|
||||
# 追加模式:在现有配额基础上增加
|
||||
existing_quota.pod_quota = (existing_quota.pod_quota or 0) + agent_alloc.quantity
|
||||
existing_quota.allocated_at = datetime.utcnow()
|
||||
if user_id_for_allocation:
|
||||
existing_quota.allocated_by = uuid.UUID(user_id_for_allocation)
|
||||
logger.info(
|
||||
f"追加平台 Agent 配额给渠道 {channel.name}: "
|
||||
f"template={agent_alloc.agentId}, 追加量={agent_alloc.quantity}, 新配额={existing_quota.pod_quota}"
|
||||
)
|
||||
else:
|
||||
# 创建新配额记录
|
||||
quota = PlatformAgentQuota(
|
||||
target_id=channel_id,
|
||||
target_type="channel",
|
||||
template_name=agent_alloc.agentId, # agentId 是模板名称
|
||||
pod_quota=agent_alloc.quantity,
|
||||
pod_used=0,
|
||||
allocated_by=uuid.UUID(user_id_for_allocation) if user_id_for_allocation else None,
|
||||
allocated_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(quota)
|
||||
logger.info(
|
||||
f"创建平台 Agent 配额给渠道 {channel.name}: "
|
||||
f"template={agent_alloc.agentId}, quota={agent_alloc.quantity}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"分配平台 Agent 配额给渠道 {channel.name}: "
|
||||
f"template={agent_alloc.agentId}, quota={agent_alloc.quantity}"
|
||||
# 2. 更新或创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
# 先查找是否已存在
|
||||
existing_alloc_result = await db.execute(
|
||||
select(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == channel_id,
|
||||
ResourceAllocation.target_type == "channel",
|
||||
ResourceAllocation.resource_type == "agent",
|
||||
ResourceAllocation.resource_id == agent_alloc.agentId
|
||||
)
|
||||
)
|
||||
)
|
||||
existing_alloc = existing_alloc_result.scalar_one_or_none()
|
||||
|
||||
if existing_alloc:
|
||||
# 追加模式
|
||||
existing_alloc.quantity = (existing_alloc.quantity or 0) + agent_alloc.quantity
|
||||
else:
|
||||
# 创建新记录
|
||||
allocation = ResourceAllocation(
|
||||
target_id=channel_id,
|
||||
target_type="channel",
|
||||
resource_type="agent",
|
||||
resource_id=agent_alloc.agentId,
|
||||
quantity=agent_alloc.quantity,
|
||||
)
|
||||
db.add(allocation)
|
||||
|
||||
# 更新自定义Agent资源(旧格式,保留兼容)
|
||||
# Bug 修复:customAgentResources 也需要创建 ChannelCustomAgentQuota 记录
|
||||
# 追加模式:在现有配额基础上增加
|
||||
if req.customAgentResources:
|
||||
channel.custom_agent_cpu = req.customAgentResources.cpu
|
||||
channel.custom_agent_memory = req.customAgentResources.memory
|
||||
# 同时更新新格式字段
|
||||
channel.custom_agent_cpu_quota = req.customAgentResources.cpu
|
||||
channel.custom_agent_memory_quota = req.customAgentResources.memory
|
||||
|
||||
# 查找或创建渠道配额记录
|
||||
channel_quota_result = await db.execute(
|
||||
select(ChannelCustomAgentQuota).where(
|
||||
@@ -1670,9 +1704,16 @@ async def allocate_channel_resources(
|
||||
channel_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if channel_quota:
|
||||
# 更新现有配额
|
||||
channel_quota.cpu_quota = req.customAgentResources.cpu
|
||||
channel_quota.memory_quota = req.customAgentResources.memory
|
||||
# 追加模式:在现有配额基础上增加
|
||||
new_cpu_quota = float(channel_quota.cpu_quota or 0) + req.customAgentResources.cpu
|
||||
new_memory_quota = float(channel_quota.memory_quota or 0) + req.customAgentResources.memory
|
||||
channel_quota.cpu_quota = new_cpu_quota
|
||||
channel_quota.memory_quota = new_memory_quota
|
||||
# 同步更新渠道表字段
|
||||
channel.custom_agent_cpu = new_cpu_quota
|
||||
channel.custom_agent_memory = new_memory_quota
|
||||
channel.custom_agent_cpu_quota = new_cpu_quota
|
||||
channel.custom_agent_memory_quota = new_memory_quota
|
||||
else:
|
||||
# 创建新配额记录
|
||||
channel_quota = ChannelCustomAgentQuota(
|
||||
@@ -1683,13 +1724,15 @@ async def allocate_channel_resources(
|
||||
memory_allocated=0,
|
||||
)
|
||||
db.add(channel_quota)
|
||||
# 同步更新渠道表字段
|
||||
channel.custom_agent_cpu = req.customAgentResources.cpu
|
||||
channel.custom_agent_memory = req.customAgentResources.memory
|
||||
channel.custom_agent_cpu_quota = req.customAgentResources.cpu
|
||||
channel.custom_agent_memory_quota = req.customAgentResources.memory
|
||||
|
||||
# 更新自定义 Agent 配额(新格式)
|
||||
# 追加模式:在现有配额基础上增加
|
||||
if req.customAgentQuota:
|
||||
# 更新渠道表中的配额字段
|
||||
channel.custom_agent_cpu_quota = req.customAgentQuota.cpuQuota
|
||||
channel.custom_agent_memory_quota = req.customAgentQuota.memoryQuota
|
||||
|
||||
# 查找或创建渠道配额记录
|
||||
channel_quota_result = await db.execute(
|
||||
select(ChannelCustomAgentQuota).where(
|
||||
@@ -1699,9 +1742,14 @@ async def allocate_channel_resources(
|
||||
channel_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if channel_quota:
|
||||
# 更新现有配额
|
||||
channel_quota.cpu_quota = req.customAgentQuota.cpuQuota
|
||||
channel_quota.memory_quota = req.customAgentQuota.memoryQuota
|
||||
# 追加模式:在现有配额基础上增加
|
||||
new_cpu_quota = float(channel_quota.cpu_quota or 0) + req.customAgentQuota.cpuQuota
|
||||
new_memory_quota = float(channel_quota.memory_quota or 0) + req.customAgentQuota.memoryQuota
|
||||
channel_quota.cpu_quota = new_cpu_quota
|
||||
channel_quota.memory_quota = new_memory_quota
|
||||
# 更新渠道表中的配额字段
|
||||
channel.custom_agent_cpu_quota = new_cpu_quota
|
||||
channel.custom_agent_memory_quota = new_memory_quota
|
||||
else:
|
||||
# 创建新配额记录
|
||||
channel_quota = ChannelCustomAgentQuota(
|
||||
@@ -1712,6 +1760,9 @@ async def allocate_channel_resources(
|
||||
memory_allocated=0,
|
||||
)
|
||||
db.add(channel_quota)
|
||||
# 更新渠道表中的配额字段
|
||||
channel.custom_agent_cpu_quota = req.customAgentQuota.cpuQuota
|
||||
channel.custom_agent_memory_quota = req.customAgentQuota.memoryQuota
|
||||
|
||||
channel.channel_credit = req.channelCredit
|
||||
|
||||
@@ -3063,8 +3114,20 @@ async def review_provider_application(
|
||||
application.review_reason = req.reason
|
||||
application.reviewed_at = datetime.utcnow()
|
||||
|
||||
# 如果批准,创建渠道供应商授权记录
|
||||
# 如果批准,创建渠道供应商授权记录并更新 LiteLLM team
|
||||
if req.approved:
|
||||
# 获取渠道信息
|
||||
channel_result = await db.execute(
|
||||
select(Channel).where(Channel.id == application.channel_id)
|
||||
)
|
||||
channel = channel_result.scalar_one_or_none()
|
||||
|
||||
if not channel:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="渠道不存在"
|
||||
)
|
||||
|
||||
# 检查是否已存在授权
|
||||
existing_result = await db.execute(
|
||||
select(ChannelProviderAccess).where(
|
||||
@@ -3095,6 +3158,77 @@ async def review_provider_application(
|
||||
approved_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(access)
|
||||
|
||||
# 更新 LiteLLM team 的 models 列表(添加该供应商的所有模型)
|
||||
if channel.litellm_team_id and provider.supported_models:
|
||||
try:
|
||||
from app.litellm_client import get_litellm_client, LiteLLMClientError
|
||||
litellm_client = get_litellm_client()
|
||||
|
||||
# 获取渠道当前已分配的模型列表
|
||||
current_models_result = await db.execute(
|
||||
select(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == application.channel_id,
|
||||
ResourceAllocation.target_type == "channel",
|
||||
ResourceAllocation.resource_type == "model"
|
||||
)
|
||||
)
|
||||
)
|
||||
current_models = {alloc.resource_id for alloc in current_models_result.scalars().all()}
|
||||
|
||||
# 添加供应商的模型(去重)
|
||||
new_models = set(provider.supported_models)
|
||||
all_models = list(current_models | new_models)
|
||||
|
||||
# 更新 LiteLLM team
|
||||
await litellm_client.update_team(
|
||||
team_id=channel.litellm_team_id,
|
||||
models=all_models,
|
||||
metadata={
|
||||
"channel_id": str(channel.id),
|
||||
"channel_name": channel.name,
|
||||
"channel_email": channel.email,
|
||||
"models_count": len(all_models),
|
||||
"provider_ids": [str(application.provider_id)],
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"渠道 {channel.name} 的 LiteLLM Team 模型列表已更新(批准供应商申请): {all_models}")
|
||||
|
||||
# 同时更新 ResourceAllocation 记录(追加模式)
|
||||
for model_name in new_models:
|
||||
if model_name not in current_models:
|
||||
allocation = ResourceAllocation(
|
||||
target_id=application.channel_id,
|
||||
target_type="channel",
|
||||
resource_type="model",
|
||||
resource_id=model_name,
|
||||
)
|
||||
db.add(allocation)
|
||||
logger.info(f"追加模型分配给渠道 {channel.name}: model={model_name}(来自供应商申请)")
|
||||
|
||||
except LiteLLMClientError as e:
|
||||
logger.error(f"更新渠道 {channel.name} 的 LiteLLM Team 失败(批准供应商申请): {e}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"LiteLLM Team 更新失败,供应商申请审批已取消: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"更新渠道 {channel.name} 的 LiteLLM 连接失败(批准供应商申请): {e}")
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"无法连接 LiteLLM Gateway,供应商申请审批已取消: {str(e)}"
|
||||
)
|
||||
elif not channel.litellm_team_id:
|
||||
# 渠道没有关联的 LiteLLM Team,无法批准申请
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="渠道没有关联的 LiteLLM Team,无法批准供应商申请。请先重新创建渠道或联系管理员"
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -3860,12 +3994,16 @@ async def allocate_platform_agent_to_channel(
|
||||
quota = quota_result.scalar_one_or_none()
|
||||
|
||||
if quota:
|
||||
# 更新现有配额
|
||||
old_quota = quota.pod_quota
|
||||
quota.pod_quota = pod_quota
|
||||
# 追加模式:在现有配额基础上增加
|
||||
old_quota = quota.pod_quota or 0
|
||||
quota.pod_quota = old_quota + pod_quota
|
||||
quota.allocated_by = user_id
|
||||
quota.allocated_at = datetime.utcnow()
|
||||
message = f"平台 Agent 配额已更新({old_quota} -> {pod_quota})"
|
||||
message = f"平台 Agent 配额已追加({old_quota} + {pod_quota} = {quota.pod_quota})"
|
||||
logger.info(
|
||||
f"追加平台 Agent 配额给渠道 {channel.name}: "
|
||||
f"template={template_name}, 追加量={pod_quota}, 新配额={quota.pod_quota}"
|
||||
)
|
||||
else:
|
||||
# 创建新配额记录
|
||||
quota = PlatformAgentQuota(
|
||||
@@ -3879,6 +4017,10 @@ async def allocate_platform_agent_to_channel(
|
||||
)
|
||||
db.add(quota)
|
||||
message = "平台 Agent 配额分配成功"
|
||||
logger.info(
|
||||
f"创建平台 Agent 配额给渠道 {channel.name}: "
|
||||
f"template={template_name}, quota={pod_quota}"
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
import secrets
|
||||
@@ -602,6 +602,32 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
for template in platform_templates:
|
||||
template_name = template.template
|
||||
|
||||
# 获取渠道的配额记录(需要同步更新 pod_used)
|
||||
channel_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota)
|
||||
.where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == TAIJI_CHANNEL_ID,
|
||||
PlatformAgentQuota.target_type == "channel",
|
||||
PlatformAgentQuota.template_name == template_name
|
||||
)
|
||||
)
|
||||
.with_for_update() # 行锁
|
||||
)
|
||||
channel_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
# 如果渠道没有该模板的配额,跳过
|
||||
if not channel_quota:
|
||||
logger.warning(f"渠道没有平台 Agent '{template_name}' 的配额,跳过分配")
|
||||
continue
|
||||
|
||||
# 检查渠道剩余配额是否足够
|
||||
remaining = channel_quota.pod_quota - (channel_quota.pod_used or 0)
|
||||
if remaining < 1:
|
||||
logger.warning(f"渠道平台 Agent '{template_name}' 配额不足,跳过分配")
|
||||
continue
|
||||
|
||||
# 创建平台 Agent 配额记录
|
||||
platform_quota = PlatformAgentQuota(
|
||||
target_id=user_id,
|
||||
@@ -614,6 +640,9 @@ async def register(req: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
)
|
||||
db.add(platform_quota)
|
||||
|
||||
# Bug 修复:更新渠道的 pod_used(分配给租户的配额视为渠道已使用的配额)
|
||||
channel_quota.pod_used = (channel_quota.pod_used or 0) + 1
|
||||
|
||||
# 同时创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
allocation = ResourceAllocation(
|
||||
target_id=user_id,
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
"""
|
||||
LiteLLM Callback Webhook路由
|
||||
接收LiteLLM的实时Token计费数据
|
||||
|
||||
此模块处理两种计费回调:
|
||||
|
||||
1. LiteLLM 模型调用回调(/litellm-callback)
|
||||
- 数据来源:LiteLLM 的 success_callback
|
||||
- EU计算:基于Token数量和模型类型
|
||||
- 公式:EU = total_tokens * MODEL_EU_RATE[model_name]
|
||||
- 存储表:model_billing_records
|
||||
|
||||
2. Agent Manager 回调(/agent-callback)
|
||||
- 数据来源:Agent Manager 的运行结束通知
|
||||
- EU计算:基于Pod运行时长(调用 app.billing.calculate_eu)
|
||||
- 公式:EU = ceil(duration_seconds / 10)
|
||||
- 存储表:agent_billing_records
|
||||
- 注意:周期性计费任务会预先扣款,此回调只扣除增量部分
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -330,27 +330,37 @@ async def allocate_tenant_resources(
|
||||
# 获取租户所属渠道(用于验证配额)
|
||||
tenant_channel_id = tenant.channel_id
|
||||
|
||||
# 删除现有资源分配
|
||||
await db.execute(
|
||||
select(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == tenant_id,
|
||||
ResourceAllocation.target_type == "tenant"
|
||||
)
|
||||
)
|
||||
)
|
||||
# 追加模式:不再删除现有资源分配,而是在原有基础上追加
|
||||
from sqlalchemy import delete
|
||||
|
||||
# 分配平台 Agent 资源(更新 PlatformAgentQuota)
|
||||
for agent_alloc in req.agents:
|
||||
# 1. 创建 ResourceAllocation 记录(兼容旧逻辑)
|
||||
allocation = ResourceAllocation(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
resource_type="agent",
|
||||
resource_id=agent_alloc.agentId,
|
||||
quantity=agent_alloc.quantity,
|
||||
# 1. 追加模式:查找或创建 ResourceAllocation 记录
|
||||
existing_alloc_result = await db.execute(
|
||||
select(ResourceAllocation).where(
|
||||
and_(
|
||||
ResourceAllocation.target_id == tenant_id,
|
||||
ResourceAllocation.target_type == "tenant",
|
||||
ResourceAllocation.resource_type == "agent",
|
||||
ResourceAllocation.resource_id == agent_alloc.agentId
|
||||
)
|
||||
)
|
||||
)
|
||||
db.add(allocation)
|
||||
existing_alloc = existing_alloc_result.scalar_one_or_none()
|
||||
|
||||
if existing_alloc:
|
||||
# 追加模式:在现有数量基础上增加
|
||||
existing_alloc.quantity = (existing_alloc.quantity or 0) + agent_alloc.quantity
|
||||
else:
|
||||
# 创建新的分配记录
|
||||
allocation = ResourceAllocation(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
resource_type="agent",
|
||||
resource_id=agent_alloc.agentId,
|
||||
quantity=agent_alloc.quantity,
|
||||
)
|
||||
db.add(allocation)
|
||||
|
||||
# 2. 更新 PlatformAgentQuota(平台 Agent 配额管理)
|
||||
# agentId 在这里是模板名称(如 echo_agent)
|
||||
@@ -402,21 +412,22 @@ async def allocate_tenant_resources(
|
||||
tenant_quota = tenant_quota_result.scalar_one_or_none()
|
||||
current_tenant_quota = tenant_quota.pod_quota if tenant_quota else 0
|
||||
|
||||
# 计算配额增量
|
||||
quota_delta = agent_alloc.quantity - current_tenant_quota
|
||||
# 追加模式:quantity 表示要追加的配额数量
|
||||
quota_delta = agent_alloc.quantity # 追加的配额量
|
||||
new_quota = current_tenant_quota + quota_delta # 新的总配额
|
||||
|
||||
# 检查增量是否超过剩余配额
|
||||
# 检查追加量是否超过剩余配额
|
||||
remaining = channel_quota.pod_quota - other_quota - current_tenant_quota
|
||||
if quota_delta > remaining:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"平台 Agent '{template_name}' 配额超出渠道剩余配额。渠道剩余: {remaining},请求增量: {quota_delta}"
|
||||
detail=f"平台 Agent '{template_name}' 配额超出渠道剩余配额。渠道剩余: {remaining},请求追加: {quota_delta}"
|
||||
)
|
||||
|
||||
# 更新或创建租户配额记录
|
||||
if tenant_quota:
|
||||
# 更新现有配额
|
||||
tenant_quota.pod_quota = agent_alloc.quantity
|
||||
# 追加模式:在现有配额基础上增加
|
||||
tenant_quota.pod_quota = new_quota
|
||||
tenant_quota.allocated_at = datetime.utcnow()
|
||||
else:
|
||||
# 创建新配额记录
|
||||
@@ -424,7 +435,7 @@ async def allocate_tenant_resources(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
template_name=template_name,
|
||||
pod_quota=agent_alloc.quantity,
|
||||
pod_quota=new_quota,
|
||||
pod_used=0,
|
||||
allocated_by=None, # 渠道管理员分配
|
||||
allocated_at=datetime.utcnow(),
|
||||
@@ -435,8 +446,8 @@ async def allocate_tenant_resources(
|
||||
channel_quota.pod_used = (channel_quota.pod_used or 0) + quota_delta
|
||||
|
||||
logger.info(
|
||||
f"分配平台 Agent 配额: template={template_name}, tenant={tenant_id}, "
|
||||
f"quota={agent_alloc.quantity}, channel_pod_used={channel_quota.pod_used}"
|
||||
f"追加平台 Agent 配额: template={template_name}, tenant={tenant_id}, "
|
||||
f"追加量={quota_delta}, 新配额={new_quota}, channel_pod_used={channel_quota.pod_used}"
|
||||
)
|
||||
else:
|
||||
# 渠道没有该平台 Agent 的配额,记录警告但不阻止操作
|
||||
@@ -598,6 +609,26 @@ async def allocate_tenant_resources(
|
||||
detail="渠道没有自定义 Agent 配额,请先向管理员申请"
|
||||
)
|
||||
|
||||
# 查找租户现有配额记录
|
||||
tenant_quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota).where(
|
||||
TenantCustomAgentQuota.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
tenant_quota = tenant_quota_result.scalar_one_or_none()
|
||||
|
||||
# 获取租户当前配额
|
||||
current_tenant_cpu = float(tenant_quota.cpu_quota) if tenant_quota else 0
|
||||
current_tenant_memory = float(tenant_quota.memory_quota) if tenant_quota else 0
|
||||
|
||||
# 追加模式:请求值表示要追加的量
|
||||
cpu_delta = req.customAgentQuota.cpuQuota
|
||||
memory_delta = req.customAgentQuota.memoryQuota
|
||||
|
||||
# 计算新配额
|
||||
new_cpu_quota = current_tenant_cpu + cpu_delta
|
||||
new_memory_quota = current_tenant_memory + memory_delta
|
||||
|
||||
# 计算渠道已分配给其他租户的配额
|
||||
other_tenants_quota_result = await db.execute(
|
||||
select(
|
||||
@@ -617,61 +648,47 @@ async def allocate_tenant_resources(
|
||||
other_cpu = float(other_quota.total_cpu or 0) if other_quota else 0
|
||||
other_memory = float(other_quota.total_memory or 0) if other_quota else 0
|
||||
|
||||
# 检查是否超过渠道配额
|
||||
remaining_cpu = float(channel_quota.cpu_quota) - other_cpu
|
||||
remaining_memory = float(channel_quota.memory_quota) - other_memory
|
||||
# 检查追加量是否超过渠道剩余配额
|
||||
# 渠道剩余 = 渠道配额 - 其他租户已分配 - 当前租户已分配
|
||||
remaining_cpu = float(channel_quota.cpu_quota) - other_cpu - current_tenant_cpu
|
||||
remaining_memory = float(channel_quota.memory_quota) - other_memory - current_tenant_memory
|
||||
|
||||
if req.customAgentQuota.cpuQuota > remaining_cpu:
|
||||
if cpu_delta > remaining_cpu:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"CPU 配额超出渠道剩余配额。渠道剩余: {remaining_cpu:.2f} 核,请求: {req.customAgentQuota.cpuQuota:.2f} 核"
|
||||
detail=f"CPU 配额超出渠道剩余配额。渠道剩余: {remaining_cpu:.2f} 核,请求追加: {cpu_delta:.2f} 核"
|
||||
)
|
||||
|
||||
if req.customAgentQuota.memoryQuota > remaining_memory:
|
||||
if memory_delta > remaining_memory:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"内存配额超出渠道剩余配额。渠道剩余: {remaining_memory:.2f} GB,请求: {req.customAgentQuota.memoryQuota:.2f} GB"
|
||||
)
|
||||
|
||||
# 查找或创建租户配额记录
|
||||
tenant_quota_result = await db.execute(
|
||||
select(TenantCustomAgentQuota).where(
|
||||
TenantCustomAgentQuota.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
tenant_quota = tenant_quota_result.scalar_one_or_none()
|
||||
|
||||
# 检查当前使用量是否超过新配额
|
||||
current_cpu_used = float(tenant_quota.cpu_used) if tenant_quota else 0
|
||||
current_memory_used = float(tenant_quota.memory_used) if tenant_quota else 0
|
||||
|
||||
if req.customAgentQuota.cpuQuota < current_cpu_used:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"CPU 配额不能低于当前使用量。当前使用: {current_cpu_used:.2f} 核,请求设置: {req.customAgentQuota.cpuQuota:.2f} 核"
|
||||
)
|
||||
|
||||
if req.customAgentQuota.memoryQuota < current_memory_used:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"内存配额不能低于当前使用量。当前使用: {current_memory_used:.2f} GB,请求设置: {req.customAgentQuota.memoryQuota:.2f} GB"
|
||||
detail=f"内存配额超出渠道剩余配额。渠道剩余: {remaining_memory:.2f} GB,请求追加: {memory_delta:.2f} GB"
|
||||
)
|
||||
|
||||
if tenant_quota:
|
||||
# 更新现有配额
|
||||
tenant_quota.cpu_quota = req.customAgentQuota.cpuQuota
|
||||
tenant_quota.memory_quota = req.customAgentQuota.memoryQuota
|
||||
# 追加模式:在现有配额基础上增加
|
||||
tenant_quota.cpu_quota = new_cpu_quota
|
||||
tenant_quota.memory_quota = new_memory_quota
|
||||
logger.info(
|
||||
f"追加自定义 Agent 配额: tenant={tenant_id}, "
|
||||
f"CPU 追加量={cpu_delta}, 新配额={new_cpu_quota}, "
|
||||
f"内存 追加量={memory_delta}, 新配额={new_memory_quota}"
|
||||
)
|
||||
else:
|
||||
# 创建新配额记录
|
||||
tenant_quota = TenantCustomAgentQuota(
|
||||
tenant_id=tenant_id,
|
||||
cpu_quota=req.customAgentQuota.cpuQuota,
|
||||
memory_quota=req.customAgentQuota.memoryQuota,
|
||||
cpu_quota=new_cpu_quota,
|
||||
memory_quota=new_memory_quota,
|
||||
cpu_used=0,
|
||||
memory_used=0,
|
||||
agent_count=0,
|
||||
)
|
||||
db.add(tenant_quota)
|
||||
logger.info(
|
||||
f"创建自定义 Agent 配额: tenant={tenant_id}, "
|
||||
f"CPU={new_cpu_quota}, 内存={new_memory_quota}"
|
||||
)
|
||||
|
||||
# Bug 修复:在查询之前 flush,确保数据库能看到刚才的更改
|
||||
await db.flush()
|
||||
@@ -2864,6 +2881,24 @@ async def list_available_platform_agents(
|
||||
)
|
||||
quota_map = {q.template_name: q for q in quota_result.scalars().all()}
|
||||
|
||||
# 获取渠道下每个模板已分配给租户的配额总和(与 allocate_tenant_resources 保持一致)
|
||||
tenant_quota_result = await db.execute(
|
||||
select(
|
||||
PlatformAgentQuota.template_name,
|
||||
func.sum(PlatformAgentQuota.pod_quota).label("total_allocated")
|
||||
)
|
||||
.select_from(PlatformAgentQuota)
|
||||
.join(User, PlatformAgentQuota.target_id == User.id)
|
||||
.where(
|
||||
and_(
|
||||
User.channel_id == channel_id,
|
||||
PlatformAgentQuota.target_type == "tenant"
|
||||
)
|
||||
)
|
||||
.group_by(PlatformAgentQuota.template_name)
|
||||
)
|
||||
tenant_allocated_map = {row.template_name: row.total_allocated or 0 for row in tenant_quota_result.all()}
|
||||
|
||||
# 获取待审批的申请
|
||||
pending_result = await db.execute(
|
||||
select(ResourceApplication).where(
|
||||
@@ -2879,12 +2914,15 @@ async def list_available_platform_agents(
|
||||
data = []
|
||||
for template_name, template in platform_templates.items():
|
||||
quota = quota_map.get(template_name)
|
||||
# 使用查询租户配额总和的方式计算剩余配额(与 allocate_tenant_resources 一致)
|
||||
tenant_allocated = tenant_allocated_map.get(template_name, 0)
|
||||
pod_remaining = (quota.pod_quota - tenant_allocated) if quota else 0
|
||||
item = {
|
||||
**template,
|
||||
"hasAccess": quota is not None,
|
||||
"podQuota": quota.pod_quota if quota else 0,
|
||||
"podUsed": quota.pod_used if quota else 0,
|
||||
"podRemaining": (quota.pod_quota - quota.pod_used) if quota else 0,
|
||||
"podUsed": tenant_allocated, # 已分配给租户的配额总和
|
||||
"podRemaining": pod_remaining,
|
||||
"pendingApplication": template_name in pending_apps,
|
||||
}
|
||||
data.append(item)
|
||||
@@ -3208,14 +3246,6 @@ async def allocate_platform_agent_to_tenant(
|
||||
)
|
||||
other_quota = other_tenants_quota_result.scalar() or 0
|
||||
|
||||
# 检查是否超过渠道配额
|
||||
remaining = channel_quota.pod_quota - other_quota
|
||||
if req.podQuota > remaining:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"配额超出渠道剩余配额。渠道剩余: {remaining},请求: {req.podQuota}"
|
||||
)
|
||||
|
||||
# 查找或创建租户配额记录(使用行锁防止并发更新)
|
||||
tenant_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota)
|
||||
@@ -3230,16 +3260,30 @@ async def allocate_platform_agent_to_tenant(
|
||||
)
|
||||
tenant_quota = tenant_quota_result.scalar_one_or_none()
|
||||
|
||||
# 计算配额变化量(用于更新渠道的 pod_used)
|
||||
old_tenant_quota = tenant_quota.pod_quota if tenant_quota else 0
|
||||
old_pod_used = tenant_quota.pod_used if tenant_quota else 0
|
||||
quota_delta = req.podQuota - old_tenant_quota
|
||||
# 追加模式:req.podQuota 表示要追加的配额量
|
||||
current_tenant_quota = tenant_quota.pod_quota if tenant_quota else 0
|
||||
quota_delta = req.podQuota # 追加量
|
||||
new_quota = current_tenant_quota + quota_delta # 新的总配额
|
||||
|
||||
# 计算需要新启动的 Pod 数量
|
||||
pods_to_start = req.podQuota - old_pod_used
|
||||
# 检查追加量是否超过渠道剩余配额
|
||||
# 渠道剩余 = 渠道配额 - 其他租户已分配 - 当前租户已分配
|
||||
remaining = channel_quota.pod_quota - other_quota - current_tenant_quota
|
||||
if quota_delta > remaining:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"配额超出渠道剩余配额。渠道剩余: {remaining},请求追加: {quota_delta}"
|
||||
)
|
||||
|
||||
old_pod_used = tenant_quota.pod_used if tenant_quota else 0
|
||||
|
||||
if tenant_quota:
|
||||
tenant_quota.pod_quota = req.podQuota
|
||||
# 追加模式:在现有配额基础上增加
|
||||
tenant_quota.pod_quota = new_quota
|
||||
tenant_quota.allocated_at = datetime.utcnow()
|
||||
logger.info(
|
||||
f"追加平台 Agent 配额: template={req.templateName}, tenant={tenant_id}, "
|
||||
f"追加量={quota_delta}, 新配额={new_quota}"
|
||||
)
|
||||
else:
|
||||
# 注意:allocated_by 设置为 None,因为渠道管理员的 JWT sub 字段是渠道 ID 而非用户 ID
|
||||
# 如果需要记录分配人,应该在 JWT 中添加实际的用户 ID 字段
|
||||
@@ -3247,12 +3291,16 @@ async def allocate_platform_agent_to_tenant(
|
||||
target_id=tenant_id,
|
||||
target_type="tenant",
|
||||
template_name=req.templateName,
|
||||
pod_quota=req.podQuota,
|
||||
pod_quota=new_quota,
|
||||
pod_used=0,
|
||||
allocated_by=None, # 渠道管理员的 sub 是渠道 ID,不是用户 ID
|
||||
allocated_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(tenant_quota)
|
||||
logger.info(
|
||||
f"创建平台 Agent 配额: template={req.templateName}, tenant={tenant_id}, "
|
||||
f"配额={new_quota}"
|
||||
)
|
||||
|
||||
# Bug 修复:更新渠道的 pod_used(分配给租户的配额视为渠道已使用的配额)
|
||||
channel_quota.pod_used = (channel_quota.pod_used or 0) + quota_delta
|
||||
@@ -3272,11 +3320,11 @@ async def allocate_platform_agent_to_tenant(
|
||||
"tenantName": tenant.name,
|
||||
"templateName": req.templateName,
|
||||
"templateDisplayName": template.get("displayName", req.templateName),
|
||||
"podQuota": req.podQuota,
|
||||
"podQuota": new_quota,
|
||||
"podUsed": tenant_quota.pod_used if tenant_quota else 0,
|
||||
"podRemaining": req.podQuota - (tenant_quota.pod_used if tenant_quota else 0),
|
||||
"podRemaining": new_quota - (tenant_quota.pod_used if tenant_quota else 0),
|
||||
},
|
||||
message=f"平台 Agent 配额分配成功,租户可在租户端部署"
|
||||
message=f"平台 Agent 配额追加成功(追加量: {quota_delta}),租户可在租户端部署"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -462,34 +462,51 @@ async def allocate_platform_agent_to_tenant(
|
||||
)
|
||||
existing_quota = existing_quota_result.scalar_one_or_none()
|
||||
|
||||
# 计算配额增量
|
||||
# 追加模式:request.podQuota 表示要追加的配额量
|
||||
current_quota = existing_quota.pod_quota if existing_quota else 0
|
||||
quota_delta = request.podQuota - current_quota
|
||||
quota_delta = request.podQuota # 追加量
|
||||
new_quota = current_quota + quota_delta # 新的总配额
|
||||
|
||||
# 使用渠道的 pod_used 检查剩余配额(pod_used 表示已分配给租户的配额)
|
||||
if channel_quota.pod_used + quota_delta > channel_quota.pod_quota:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"配额不足,渠道配额: {channel_quota.pod_quota},已分配: {channel_quota.pod_used},请求增量: {quota_delta}"
|
||||
detail=f"配额不足,渠道配额: {channel_quota.pod_quota},已分配: {channel_quota.pod_used},请求追加: {quota_delta}"
|
||||
)
|
||||
|
||||
# 更新或创建租户配额
|
||||
if existing_quota:
|
||||
# 更新现有配额
|
||||
existing_quota.pod_quota = request.podQuota
|
||||
# 追加模式:在现有配额基础上增加
|
||||
existing_quota.pod_quota = new_quota
|
||||
existing_quota.allocated_at = datetime.utcnow()
|
||||
quota = existing_quota
|
||||
logger.info(
|
||||
"追加平台Agent配额",
|
||||
tenant_id=tenant_id,
|
||||
template=request.templateName,
|
||||
quota_delta=quota_delta,
|
||||
new_quota=new_quota
|
||||
)
|
||||
else:
|
||||
# 创建新配额
|
||||
quota = PlatformAgentQuota(
|
||||
target_id=tenant_uuid,
|
||||
target_type="tenant",
|
||||
template_name=request.templateName,
|
||||
pod_quota=request.podQuota,
|
||||
pod_quota=new_quota,
|
||||
pod_used=0,
|
||||
allocated_by=uuid.UUID(_get_user_id(current_user))
|
||||
)
|
||||
db.add(quota)
|
||||
logger.info(
|
||||
"创建平台Agent配额",
|
||||
tenant_id=tenant_id,
|
||||
template=request.templateName,
|
||||
quota=new_quota
|
||||
)
|
||||
|
||||
# Bug 修复:更新渠道的 pod_used(分配给租户的配额视为渠道已使用的配额)
|
||||
channel_quota.pod_used = (channel_quota.pod_used or 0) + quota_delta
|
||||
|
||||
# 立即启动 Pod(平台 Agent 分配时立即启动)
|
||||
try:
|
||||
|
||||
@@ -14,7 +14,7 @@ from database import get_db
|
||||
from app.auth import require_role
|
||||
from models import (
|
||||
User, Agent, ResourceUsage, BillingRecord,
|
||||
AgentTrace
|
||||
AgentTrace, AgentBillingRecord, ModelBillingRecord
|
||||
)
|
||||
|
||||
|
||||
@@ -99,12 +99,23 @@ async def get_platform_overview(
|
||||
# 总活跃 Agent 数
|
||||
active_agents = platform_agents_count + custom_agents_count
|
||||
|
||||
# 5. 本月总EU消费
|
||||
month_eu_result = await db.execute(
|
||||
select(func.sum(ResourceUsage.eu_consumed))
|
||||
.where(ResourceUsage.created_at >= month_start)
|
||||
# 5. 本月总EU消费(合并 Agent运行时间 + 模型Token 两种计费)
|
||||
# 5.1 Agent 运行时间计费的 EU(从 agent_billing_records 表)
|
||||
agent_eu_result = await db.execute(
|
||||
select(func.sum(AgentBillingRecord.eu_consumed))
|
||||
.where(AgentBillingRecord.period_start >= month_start)
|
||||
)
|
||||
month_total_eu = month_eu_result.scalar() or 0
|
||||
agent_eu = agent_eu_result.scalar() or 0
|
||||
|
||||
# 5.2 模型 Token 计费的 EU(从 model_billing_records 表)
|
||||
model_eu_result = await db.execute(
|
||||
select(func.sum(ModelBillingRecord.eu_consumed))
|
||||
.where(ModelBillingRecord.created_at >= month_start)
|
||||
)
|
||||
model_eu = model_eu_result.scalar() or 0
|
||||
|
||||
# 5.3 合并两种 EU 消耗
|
||||
month_total_eu = float(agent_eu or 0) + float(model_eu or 0)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
|
||||
@@ -2304,13 +2304,66 @@ async def deploy_platform_agent(
|
||||
detail=f"您没有使用 {req.agentType} 的权限,请联系渠道管理员分配配额"
|
||||
)
|
||||
|
||||
# 检查 Pod 配额
|
||||
# 检查 Pod 配额(租户配额)
|
||||
if quota.pod_used >= quota.pod_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Pod 配额已用完。配额: {quota.pod_quota},已使用: {quota.pod_used}"
|
||||
)
|
||||
|
||||
# 获取用户所属渠道 ID
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user or not user.channel_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无法获取用户渠道信息"
|
||||
)
|
||||
user_channel_id = user.channel_id
|
||||
|
||||
# 检查渠道配额(渠道层面的总使用量不能超过渠道配额)
|
||||
channel_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota)
|
||||
.where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == user_channel_id,
|
||||
PlatformAgentQuota.target_type == "channel",
|
||||
PlatformAgentQuota.template_name == req.agentType
|
||||
)
|
||||
)
|
||||
)
|
||||
channel_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if not channel_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"渠道没有 {req.agentType} 的配额"
|
||||
)
|
||||
|
||||
# 查询渠道下所有租户的实际 pod_used 总和
|
||||
channel_total_used_result = await db.execute(
|
||||
select(func.sum(PlatformAgentQuota.pod_used).label("total"))
|
||||
.select_from(PlatformAgentQuota)
|
||||
.join(User, PlatformAgentQuota.target_id == User.id)
|
||||
.where(
|
||||
and_(
|
||||
User.channel_id == user_channel_id,
|
||||
PlatformAgentQuota.target_type == "tenant",
|
||||
PlatformAgentQuota.template_name == req.agentType
|
||||
)
|
||||
)
|
||||
)
|
||||
channel_total_used = channel_total_used_result.scalar() or 0
|
||||
|
||||
# 检查渠道层面的总使用量 + 1 是否超过渠道配额
|
||||
if channel_total_used + 1 > channel_quota.pod_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"渠道 {req.agentType} 配额已满。渠道配额: {channel_quota.pod_quota},当前总使用: {channel_total_used}"
|
||||
)
|
||||
|
||||
# 检查用户余额(使用 Balance 表)
|
||||
balance_result = await db.execute(
|
||||
select(Balance).where(Balance.user_id == user_id)
|
||||
@@ -2496,13 +2549,66 @@ async def use_platform_agent(
|
||||
detail=f"您没有使用 {req.agentType} 的权限,请联系渠道管理员分配配额"
|
||||
)
|
||||
|
||||
# 检查 Pod 配额
|
||||
# 检查 Pod 配额(租户配额)
|
||||
if quota.pod_used >= quota.pod_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Pod 配额已用完。配额: {quota.pod_quota},已使用: {quota.pod_used}"
|
||||
)
|
||||
|
||||
# 获取用户所属渠道 ID
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user or not user.channel_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无法获取用户渠道信息"
|
||||
)
|
||||
user_channel_id = user.channel_id
|
||||
|
||||
# 检查渠道配额(渠道层面的总使用量不能超过渠道配额)
|
||||
channel_quota_result = await db.execute(
|
||||
select(PlatformAgentQuota)
|
||||
.where(
|
||||
and_(
|
||||
PlatformAgentQuota.target_id == user_channel_id,
|
||||
PlatformAgentQuota.target_type == "channel",
|
||||
PlatformAgentQuota.template_name == req.agentType
|
||||
)
|
||||
)
|
||||
)
|
||||
channel_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if not channel_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"渠道没有 {req.agentType} 的配额"
|
||||
)
|
||||
|
||||
# 查询渠道下所有租户的实际 pod_used 总和
|
||||
channel_total_used_result = await db.execute(
|
||||
select(func.sum(PlatformAgentQuota.pod_used).label("total"))
|
||||
.select_from(PlatformAgentQuota)
|
||||
.join(User, PlatformAgentQuota.target_id == User.id)
|
||||
.where(
|
||||
and_(
|
||||
User.channel_id == user_channel_id,
|
||||
PlatformAgentQuota.target_type == "tenant",
|
||||
PlatformAgentQuota.template_name == req.agentType
|
||||
)
|
||||
)
|
||||
)
|
||||
channel_total_used = channel_total_used_result.scalar() or 0
|
||||
|
||||
# 检查渠道层面的总使用量 + 1 是否超过渠道配额
|
||||
if channel_total_used + 1 > channel_quota.pod_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"渠道 {req.agentType} 配额已满。渠道配额: {channel_quota.pod_quota},当前总使用: {channel_total_used}"
|
||||
)
|
||||
|
||||
try:
|
||||
client = get_agent_manager_client()
|
||||
|
||||
@@ -2521,6 +2627,7 @@ async def use_platform_agent(
|
||||
)
|
||||
|
||||
# 如果有环境变量,使用 create_agent;否则使用 create_platform_agent
|
||||
env_vars = {} # use_platform_agent 不支持环境变量注入
|
||||
if env_vars:
|
||||
result = await client.create_agent(
|
||||
name=instance_name,
|
||||
@@ -2824,6 +2931,63 @@ async def create_custom_agent(
|
||||
detail=f"内存配额不足。剩余: {remaining_memory:.2f} GB,请求: {memory_request:.2f} GB"
|
||||
)
|
||||
|
||||
# 获取用户所属渠道 ID
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user or not user.channel_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无法获取用户渠道信息"
|
||||
)
|
||||
user_channel_id = user.channel_id
|
||||
|
||||
# 检查渠道配额(渠道层面的总使用量不能超过渠道配额)
|
||||
from models import ChannelCustomAgentQuota
|
||||
channel_quota_result = await db.execute(
|
||||
select(ChannelCustomAgentQuota)
|
||||
.where(ChannelCustomAgentQuota.channel_id == user_channel_id)
|
||||
)
|
||||
channel_custom_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if not channel_custom_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="渠道没有自定义 Agent 配额"
|
||||
)
|
||||
|
||||
# 查询渠道下所有租户的实际 CPU/内存使用量总和
|
||||
channel_usage_result = await db.execute(
|
||||
select(
|
||||
func.sum(TenantCustomAgentQuota.cpu_used).label("total_cpu"),
|
||||
func.sum(TenantCustomAgentQuota.memory_used).label("total_memory")
|
||||
)
|
||||
.select_from(TenantCustomAgentQuota)
|
||||
.join(User, TenantCustomAgentQuota.tenant_id == User.id)
|
||||
.where(User.channel_id == user_channel_id)
|
||||
)
|
||||
channel_usage = channel_usage_result.one()
|
||||
channel_total_cpu_used = float(channel_usage.total_cpu or 0)
|
||||
channel_total_memory_used = float(channel_usage.total_memory or 0)
|
||||
|
||||
channel_cpu_quota = float(channel_custom_quota.cpu_quota or 0)
|
||||
channel_memory_quota = float(channel_custom_quota.memory_quota or 0)
|
||||
|
||||
# 检查渠道层面的 CPU 使用量 + 请求量是否超过渠道配额
|
||||
if channel_total_cpu_used + cpu_request > channel_cpu_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"渠道 CPU 配额已满。渠道配额: {channel_cpu_quota:.2f} 核,当前总使用: {channel_total_cpu_used:.2f} 核,请求: {cpu_request:.2f} 核"
|
||||
)
|
||||
|
||||
# 检查渠道层面的内存使用量 + 请求量是否超过渠道配额
|
||||
if channel_total_memory_used + memory_request > channel_memory_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"渠道内存配额已满。渠道配额: {channel_memory_quota:.2f} GB,当前总使用: {channel_total_memory_used:.2f} GB,请求: {memory_request:.2f} GB"
|
||||
)
|
||||
|
||||
# 验证框架模板类型
|
||||
framework_template = req.frameworkTemplate or "MCP"
|
||||
allowed_frameworks = ["A2A", "langchain", "MCP"]
|
||||
@@ -3362,6 +3526,60 @@ async def scale_custom_agent_api(
|
||||
detail=f"内存配额不足。剩余: {remaining_memory:.2f} GB,需要增加: {memory_delta:.2f} GB"
|
||||
)
|
||||
|
||||
# 检查渠道配额(扩容时需要检查)
|
||||
if cpu_delta > 0 or memory_delta > 0:
|
||||
# 获取用户所属渠道 ID
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user or not user.channel_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="无法获取用户渠道信息"
|
||||
)
|
||||
user_channel_id = user.channel_id
|
||||
|
||||
# 获取渠道配额
|
||||
from models import ChannelCustomAgentQuota
|
||||
channel_quota_result = await db.execute(
|
||||
select(ChannelCustomAgentQuota)
|
||||
.where(ChannelCustomAgentQuota.channel_id == user_channel_id)
|
||||
)
|
||||
channel_custom_quota = channel_quota_result.scalar_one_or_none()
|
||||
|
||||
if channel_custom_quota:
|
||||
# 查询渠道下所有租户的实际使用量总和
|
||||
channel_usage_result = await db.execute(
|
||||
select(
|
||||
func.sum(TenantCustomAgentQuota.cpu_used).label("total_cpu"),
|
||||
func.sum(TenantCustomAgentQuota.memory_used).label("total_memory")
|
||||
)
|
||||
.select_from(TenantCustomAgentQuota)
|
||||
.join(User, TenantCustomAgentQuota.tenant_id == User.id)
|
||||
.where(User.channel_id == user_channel_id)
|
||||
)
|
||||
channel_usage = channel_usage_result.one()
|
||||
channel_total_cpu = float(channel_usage.total_cpu or 0)
|
||||
channel_total_memory = float(channel_usage.total_memory or 0)
|
||||
|
||||
channel_cpu_quota = float(channel_custom_quota.cpu_quota or 0)
|
||||
channel_memory_quota = float(channel_custom_quota.memory_quota or 0)
|
||||
|
||||
# 检查渠道 CPU 配额
|
||||
if cpu_delta > 0 and channel_total_cpu + cpu_delta > channel_cpu_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"渠道 CPU 配额已满。渠道配额: {channel_cpu_quota:.2f} 核,当前总使用: {channel_total_cpu:.2f} 核"
|
||||
)
|
||||
|
||||
# 检查渠道内存配额
|
||||
if memory_delta > 0 and channel_total_memory + memory_delta > channel_memory_quota:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"渠道内存配额已满。渠道配额: {channel_memory_quota:.2f} GB,当前总使用: {channel_total_memory:.2f} GB"
|
||||
)
|
||||
|
||||
try:
|
||||
client = get_agent_manager_client()
|
||||
|
||||
|
||||
@@ -695,11 +695,12 @@ class SystemMonitor:
|
||||
|
||||
try:
|
||||
# Agent运行时长计费 - 独立捕获异常
|
||||
# 注意:agent_billing_records 表使用 cost 列,没有 eu_consumed 列
|
||||
# EU计算:基于Agent运行时长,1 EU = 10秒
|
||||
try:
|
||||
agent_result = await session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COALESCE(SUM(eu_consumed), 0) as total_eu,
|
||||
COALESCE(SUM(cost), 0) as total_cost,
|
||||
COUNT(*) as total_calls
|
||||
FROM agent_billing_records
|
||||
@@ -732,20 +733,36 @@ class SystemMonitor:
|
||||
logger.warning(f"查询model_billing_records失败: {model_error}")
|
||||
# 不要回滚,继续使用同一个session
|
||||
|
||||
# 每小时消耗 - 仅查询存在的表
|
||||
# 每小时消耗 - 合并两个计费表的数据
|
||||
try:
|
||||
# 尝试查询model_billing_records的每小时数据
|
||||
# 使用 UNION ALL 合并 agent_billing_records 和 model_billing_records 的每小时数据
|
||||
hourly_result = await session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
DATE_TRUNC('hour', created_at) as hour,
|
||||
COALESCE(SUM(eu_consumed), 0) as eu,
|
||||
COALESCE(SUM(total_cost), 0) as cost,
|
||||
COUNT(*) as calls
|
||||
FROM model_billing_records
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND created_at > NOW() - INTERVAL '24 hours'
|
||||
GROUP BY DATE_TRUNC('hour', created_at)
|
||||
SELECT hour, SUM(eu) as eu, SUM(cost) as cost, SUM(calls) as calls
|
||||
FROM (
|
||||
-- Agent 运行时长计费
|
||||
SELECT
|
||||
DATE_TRUNC('hour', start_time) as hour,
|
||||
COALESCE(eu_consumed, 0) as eu,
|
||||
COALESCE(cost, 0) as cost,
|
||||
1 as calls
|
||||
FROM agent_billing_records
|
||||
WHERE user_id = :tenant_id
|
||||
AND start_time > NOW() - INTERVAL '24 hours'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- 模型 Token 计费
|
||||
SELECT
|
||||
DATE_TRUNC('hour', created_at) as hour,
|
||||
COALESCE(eu_consumed, 0) as eu,
|
||||
COALESCE(total_cost, 0) as cost,
|
||||
1 as calls
|
||||
FROM model_billing_records
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND created_at > NOW() - INTERVAL '24 hours'
|
||||
) combined
|
||||
GROUP BY hour
|
||||
ORDER BY hour
|
||||
"""),
|
||||
{"tenant_id": tenant_id}
|
||||
@@ -756,17 +773,17 @@ class SystemMonitor:
|
||||
# 不要回滚,继续使用同一个session
|
||||
|
||||
# 合并数据
|
||||
# agent_row: (total_cost, total_calls)
|
||||
# model_row: (total_eu, total_cost, total_calls)
|
||||
agent_cost = float(agent_row[0]) if agent_row else 0
|
||||
agent_calls = int(agent_row[1]) if agent_row else 0
|
||||
# agent_row: (total_eu, total_cost, total_calls) - Agent运行时长计费
|
||||
# model_row: (total_eu, total_cost, total_calls) - 模型Token计费
|
||||
agent_eu = float(agent_row[0]) if agent_row else 0
|
||||
agent_cost = float(agent_row[1]) if agent_row else 0
|
||||
agent_calls = int(agent_row[2]) if agent_row else 0
|
||||
|
||||
model_eu = float(model_row[0]) if model_row else 0
|
||||
model_cost = float(model_row[1]) if model_row else 0
|
||||
model_calls = int(model_row[2]) if model_row else 0
|
||||
|
||||
# Agent计费使用cost换算EU(假设1 EU = 1 cost单位)
|
||||
agent_eu = agent_cost
|
||||
# EU消耗 = Agent运行时长EU + 模型Token EU
|
||||
total_eu = agent_eu + model_eu
|
||||
total_cost = agent_cost + model_cost
|
||||
total_calls = agent_calls + model_calls
|
||||
|
||||
Reference in New Issue
Block a user