forked from xiaohei/taiji-AI-PAD
12 KiB
12 KiB
模型 Token 计费方案分析
版本: v1.0.0 创建时间: 2026-01-08 分析目标: 分析模型 Token 使用量计费的实现方案
1. 当前系统架构
graph TB
subgraph "mcp-server"
A[租户 Agent] --> B[LiteLLM Client]
B --> C[TenantModelKey 表]
end
subgraph "LiteLLM Gateway"
D[/chat/completions] --> E[模型路由]
E --> F[OpenRouter/OpenAI/Anthropic]
G[/spend/logs] --> H[用量数据]
I[PostgreSQL] --> H
end
B -->|API Key| D
B -->|查询用量| G
1.1 当前 LiteLLM 集成状态
| 功能 | 实现状态 | 说明 |
|---|---|---|
| Team 管理 | ✅ 已实现 | 渠道 = LiteLLM Team |
| Key 管理 | ✅ 已实现 | 租户 = LiteLLM Key |
| RPM/TPM 限制 | ✅ 已实现 | 通过 Key 配置 |
| Budget 限制 | ✅ 已实现 | max_budget + budget_duration |
| 用量查询 | ⚠️ 部分实现 | get_spend_logs() 已有,但未集成到计费 |
1.2 LiteLLM 配置
当前配置(litellm.yaml)已启用:
database_url: 使用 PostgreSQL 存储用量数据success_callback: ["langfuse"]: 成功回调failure_callback: ["langfuse"]: 失败回调
2. 方案对比分析
方案 1:读取 LiteLLM 网关日志(实时记录)
sequenceDiagram
participant Agent as 租户 Agent
participant LiteLLM as LiteLLM Gateway
participant MCP as mcp-server
participant DB as 数据库
Agent->>LiteLLM: 调用模型 API
LiteLLM->>LiteLLM: 记录日志
LiteLLM-->>Agent: 返回结果
Note over LiteLLM,MCP: 方案1A: 日志文件监听
LiteLLM->>MCP: 日志文件变化
MCP->>DB: 解析并写入计费记录
Note over LiteLLM,MCP: 方案1B: Webhook 回调
LiteLLM->>MCP: POST /webhook/litellm
MCP->>DB: 写入计费记录
优点:
- ✅ 实时性高,调用完成立即记录
- ✅ 数据准确,直接从源头获取
- ✅ 可以获取详细的请求/响应信息
缺点:
- ❌ 需要配置日志监听或 Webhook
- ❌ 日志解析复杂,格式可能变化
- ❌ 增加系统耦合度
实现复杂度: 中等
方案 2:读取 LiteLLM 数据库
sequenceDiagram
participant Agent as 租户 Agent
participant LiteLLM as LiteLLM Gateway
participant LLMDB as LiteLLM PostgreSQL
participant MCP as mcp-server
participant DB as mcp-server DB
Agent->>LiteLLM: 调用模型 API
LiteLLM->>LLMDB: 记录用量
LiteLLM-->>Agent: 返回结果
Note over MCP: 定时任务(每分钟)
MCP->>LLMDB: 查询新增用量记录
MCP->>DB: 同步到计费表
优点:
- ✅ 数据完整,包含所有历史记录
- ✅ 可以批量同步,减少 API 调用
- ✅ 不依赖 LiteLLM API 可用性
缺点:
- ❌ 需要直接访问 LiteLLM 数据库
- ❌ 依赖 LiteLLM 内部表结构(可能变化)
- ❌ 实时性较差(取决于同步频率)
- ❌ 数据库耦合,升级 LiteLLM 可能出问题
实现复杂度: 中等
方案 3:使用 LiteLLM Spend API(推荐)
sequenceDiagram
participant Agent as 租户 Agent
participant LiteLLM as LiteLLM Gateway
participant MCP as mcp-server
participant DB as 数据库
Agent->>LiteLLM: 调用模型 API
LiteLLM-->>Agent: 返回结果
Note over MCP: 定时任务或按需查询
MCP->>LiteLLM: GET /spend/logs?api_key=xxx
LiteLLM-->>MCP: 返回用量数据
MCP->>DB: 写入计费记录
优点:
- ✅ 使用官方 API,稳定可靠
- ✅ 已有
get_spend_logs()实现 - ✅ 不依赖内部实现细节
- ✅ 支持按 Key、Team、时间范围查询
缺点:
- ❌ 实时性取决于查询频率
- ❌ 需要定时任务或触发机制
- ❌ 大量 Key 时 API 调用开销大
实现复杂度: 低
方案 4:LiteLLM Callback/Webhook(推荐)
sequenceDiagram
participant Agent as 租户 Agent
participant LiteLLM as LiteLLM Gateway
participant MCP as mcp-server
participant DB as 数据库
Agent->>LiteLLM: 调用模型 API
LiteLLM-->>Agent: 返回结果
Note over LiteLLM,MCP: 异步回调
LiteLLM->>MCP: POST /api/v1/billing/litellm-callback
Note right of MCP: 包含: api_key, model, tokens, cost
MCP->>DB: 写入计费记录
优点:
- ✅ 实时性最高,调用完成立即通知
- ✅ LiteLLM 原生支持 custom callback
- ✅ 数据准确,包含完整的用量信息
- ✅ 解耦设计,mcp-server 被动接收
缺点:
- ❌ 需要配置 LiteLLM callback
- ❌ 需要处理回调失败重试
- ❌ mcp-server 需要暴露 webhook 端点
实现复杂度: 中等
方案 5:Agent 侧上报(补充方案)
sequenceDiagram
participant Agent as 租户 Agent
participant LiteLLM as LiteLLM Gateway
participant MCP as mcp-server
participant DB as 数据库
Agent->>LiteLLM: 调用模型 API
LiteLLM-->>Agent: 返回结果(含 usage)
Note over Agent: 解析 response.usage
Agent->>MCP: POST /api/v1/billing/report-usage
Note right of MCP: 包含: model, input_tokens, output_tokens
MCP->>DB: 写入计费记录
优点:
- ✅ 不依赖 LiteLLM 配置
- ✅ Agent 可以添加业务上下文
- ✅ 灵活性高
缺点:
- ❌ 依赖 Agent 正确上报
- ❌ 可能被绕过或伪造
- ❌ 需要修改所有 Agent 代码
实现复杂度: 高(需要修改 Agent)
3. 推荐方案
3.1 短期方案:LiteLLM Spend API(方案 3)
理由:
- 已有
get_spend_logs()实现 - 实现成本最低
- 可以快速上线
实现步骤:
- 创建定时任务:每分钟同步用量数据
- 修改 Dashboard 查询:从同步后的数据查询
- 添加增量同步逻辑:记录上次同步时间
# 伪代码示例
async def sync_litellm_spend():
"""定时同步 LiteLLM 用量数据"""
litellm_client = get_litellm_client()
# 获取所有租户的 Key
tenant_keys = await db.execute(select(TenantModelKey))
for key in tenant_keys:
# 查询该 Key 的用量
spend_logs = await litellm_client.get_spend_logs(
api_key=key.litellm_key_id,
start_date=last_sync_time
)
# 写入计费记录
for log in spend_logs:
billing_record = ModelBillingRecord(
tenant_id=key.tenant_id,
model_name=log["model"],
input_tokens=log["prompt_tokens"],
output_tokens=log["completion_tokens"],
cost=log["spend"],
timestamp=log["created_at"]
)
db.add(billing_record)
3.2 长期方案:LiteLLM Callback(方案 4)
理由:
- 实时性最好
- 架构更优雅
- 可扩展性强
实现步骤:
- 配置 LiteLLM Callback
修改 litellm.yaml:
general_settings:
# ... 其他配置
# 添加自定义回调
success_callback: ["langfuse", "custom_callback"]
failure_callback: ["langfuse"]
# 自定义回调配置
callbacks:
custom_callback:
callback_name: "custom_callback"
callback_type: "success"
callback_vars:
callback_url: "http://mcp-server:8000/api/v1/billing/litellm-callback"
callback_api_key: "os.environ/MCP_CALLBACK_KEY"
- 在 mcp-server 添加 Webhook 端点
# services/mcp-server/app/routes/billing_webhook.py
@router.post("/api/v1/billing/litellm-callback")
async def litellm_callback(
request: Request,
db: AsyncSession = Depends(get_db)
):
"""
接收 LiteLLM 的用量回调
LiteLLM 会在每次成功调用后发送:
- api_key: 使用的 API Key
- model: 模型名称
- prompt_tokens: 输入 Token 数
- completion_tokens: 输出 Token 数
- total_tokens: 总 Token 数
- spend: 花费金额
- metadata: 元数据(包含 tenant_id)
"""
data = await request.json()
# 根据 api_key 查找租户
api_key = data.get("api_key")
tenant_key = await db.execute(
select(TenantModelKey).where(
TenantModelKey.litellm_key_id == api_key
)
)
if not tenant_key:
return {"status": "ignored", "reason": "unknown_key"}
# 创建计费记录
billing_record = ModelBillingRecord(
tenant_id=tenant_key.tenant_id,
channel_id=tenant_key.channel_id,
model_name=data.get("model"),
input_tokens=data.get("prompt_tokens", 0),
output_tokens=data.get("completion_tokens", 0),
total_tokens=data.get("total_tokens", 0),
cost=data.get("spend", 0),
timestamp=datetime.utcnow()
)
db.add(billing_record)
await db.commit()
return {"status": "ok"}
- 创建新的计费表
-- 模型调用计费记录表
CREATE TABLE model_billing_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES users(id),
channel_id UUID REFERENCES channels(id),
-- 模型信息
model_name VARCHAR(100) NOT NULL,
-- Token 使用量
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
-- 费用
cost NUMERIC(12, 6) NOT NULL DEFAULT 0,
eu_consumed NUMERIC(10, 4) DEFAULT 0,
-- 时间
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
-- 索引
INDEX idx_model_billing_tenant (tenant_id),
INDEX idx_model_billing_channel (channel_id),
INDEX idx_model_billing_model (model_name),
INDEX idx_model_billing_timestamp (timestamp)
);
4. 方案对比总结
| 方案 | 实时性 | 可靠性 | 实现复杂度 | 维护成本 | 推荐度 |
|---|---|---|---|---|---|
| 方案1: 日志监听 | ⭐⭐⭐⭐ | ⭐⭐ | 高 | 高 | ⭐⭐ |
| 方案2: 读数据库 | ⭐⭐⭐ | ⭐⭐⭐ | 中 | 高 | ⭐⭐ |
| 方案3: Spend API | ⭐⭐⭐ | ⭐⭐⭐⭐ | 低 | 低 | ⭐⭐⭐⭐ |
| 方案4: Callback | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 中 | 中 | ⭐⭐⭐⭐⭐ |
| 方案5: Agent上报 | ⭐⭐⭐⭐ | ⭐⭐ | 高 | 高 | ⭐⭐ |
5. 实施建议
5.1 第一阶段:快速上线(1-2天)
使用 方案 3(Spend API):
- 创建定时任务,每分钟调用
get_spend_logs() - 将数据写入
model_billing_records表 - 修改 Dashboard 查询,合并 Agent 计费和模型计费
5.2 第二阶段:优化升级(1周)
升级到 方案 4(Callback):
- 配置 LiteLLM custom callback
- 实现 webhook 端点
- 添加重试和幂等处理
- 保留 Spend API 作为数据校验
5.3 数据模型设计
erDiagram
users ||--o{ model_billing_records : has
channels ||--o{ model_billing_records : has
tenant_model_keys ||--o{ model_billing_records : generates
model_billing_records {
uuid id PK
uuid tenant_id FK
uuid channel_id FK
string model_name
int input_tokens
int output_tokens
int total_tokens
decimal cost
decimal eu_consumed
timestamp timestamp
}
6. 相关文件
| 文件 | 说明 |
|---|---|
services/mcp-server/app/litellm_client.py |
LiteLLM 客户端(已有 get_spend_logs) |
services/model-gateway/config/litellm.yaml |
LiteLLM 配置(需添加 callback) |
services/mcp-server/monitoring.py |
Dashboard 数据查询(需修改) |
services/mcp-server/models.py |
数据模型(需添加新表) |