diff --git a/.env.backup b/.env.backup new file mode 100644 index 0000000..aae4d2a --- /dev/null +++ b/.env.backup @@ -0,0 +1,33 @@ +# Taiji AI-PAD 环境变量配置 + +# 数据库配置 +ASYNC_DATABASE_URL=postgresql+asyncpg://taiji:By%40123456.@taijipda.postgres.database.azure.com:5432/postgres +DATABASE_URL=postgresql://taiji:By%40123456.@taijipda.postgres.database.azure.com:5432/postgres?sslmode=require + +# Redis配置 (Azure Cache for Redis - 端口10000) +# REDIS_URL已配置为Azure Redis (端口10000) +REDIS_URL=rediss://:nkJgt1ERFpdeYrEFNyFtsc5K4ycvx2jIeAzCaGGf1OQ%3D@taiji.southeastasia.redis.azure.net:10000/0?ssl_cert_reqs=none + +# NATS消息队列配置 +NATS_URL=nats://nats:4222 + +# LiteLLM网关配置 +LITELLM_MASTER_KEY=sk-1234567890abcdef +LITELLM_URL=http://litellm-gateway:4000 + +# OpenRouter配置 +OPENROUTER_API_KEY=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 +OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 + +# RapidAPI配置 +RAPIDAPI_KEY=33902cc39dmsha572ec6ae920fb5p13c196jsn8a11209a7e67 +RAPIDAPI_HOST=rapidapi.com + +# JWT配置 +JWT_SECRET_KEY=your-super-secret-jwt-key-change-this-in-production +JWT_ALGORITHM=HS256 +JWT_EXPIRE_MINUTES=1440 + +# 应用配置 +APP_ENV=development +LOG_LEVEL=INFO diff --git a/docker-compose.yml b/docker-compose.yml index 311f9af..410af38 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,7 +83,8 @@ services: - ASYNC_DATABASE_URL=${ASYNC_DATABASE_URL} - REDIS_URL=${REDIS_URL} - NATS_URL=nats://nats:4222 - - LITELLM_URL=http://litellm-gateway:4000 + - LITELLM_URL=${LITELLM_URL:-http://4.144.175.186} + - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:-sk-1f06b8f0d2e34c9b8a9f3d75a1c4e9b7-7e3a2c6bd9f441d8} - AGENT_MANAGER_URL=${AGENT_MANAGER_URL:-http://host.docker.internal:8000} extra_hosts: - "host.docker.internal:host-gateway" diff --git a/plans/Agent-Manager接口变动需求文档.md b/plans/Agent-Manager接口变动需求文档.md new file mode 100644 index 0000000..36d83f6 --- /dev/null +++ b/plans/Agent-Manager接口变动需求文档.md @@ -0,0 +1,263 @@ +# Agent-Manager 接口变动需求文档 + +> **版本**: v1.0.0 +> **创建时间**: 2026-01-07 +> **关联设计**: 模型供应商与租户模型使用设计方案 v5.0 + +--- + +## 1. 背景 + +根据「模型供应商与租户模型使用设计方案」,mcp-server 需要在创建 Agent 时注入 LiteLLM 相关的环境变量,使 Agent 能够通过 LiteLLM 网关访问模型。 + +**核心变更**: +- Agent 创建时需要支持注入额外的环境变量 +- 环境变量包含敏感信息(API Key),需要安全处理 + +--- + +## 2. 接口变动清单 + +### 2.1 创建 Agent 接口 + +**接口路径**: `POST /agents` + +**变更内容**: 请求体中的 `env_vars` 字段需要支持以下新的环境变量 + +| 环境变量 | 类型 | 必填 | 说明 | +|----------|------|------|------| +| `OPENAI_API_BASE` | string | 否 | LiteLLM 网关地址,如 `http://4.144.175.186` | +| `OPENAI_API_KEY` | string | 否 | 租户的 LiteLLM API Key(敏感信息) | +| `MODEL_NAME` | string | 否 | 模型名称,如 `azure/gpt-4` | +| `LITELLM_MODEL` | string | 否 | 同 MODEL_NAME,兼容不同框架 | + +**请求示例**: +```json +{ + "name": "my-custom-agent", + "template": "langchain-agent", + "config": { + "user_id": "user-uuid", + "cpu_request": "100m", + "cpu_limit": "500m", + "memory_request": "128Mi", + "memory_limit": "512Mi" + }, + "env_vars": { + "OPENAI_API_BASE": "http://4.144.175.186", + "OPENAI_API_KEY": "sk-xxxxxxxxxxxxxxxx", + "MODEL_NAME": "azure/gpt-4", + "LITELLM_MODEL": "azure/gpt-4", + "CUSTOM_VAR": "custom_value" + } +} +``` + +**安全要求**: +1. `OPENAI_API_KEY` 等敏感环境变量应作为 Kubernetes Secret 存储,而非明文写入 Pod spec +2. 建议使用 `secretKeyRef` 引用 Secret 中的值 +3. 日志中不应打印敏感环境变量的值 + +--- + +### 2.2 创建平台 Agent 接口 + +**接口路径**: `POST /platform-agents` + +**变更内容**: 同上,支持注入 LiteLLM 环境变量 + +--- + +### 2.3 创建自定义 Agent 接口 + +**接口路径**: `POST /custom-agents` + +**变更内容**: 同上,支持注入 LiteLLM 环境变量 + +--- + +## 3. 实现建议 + +### 3.1 环境变量注入方式 + +**方式一:直接注入(简单但不够安全)** +```yaml +spec: + containers: + - name: agent + env: + - name: OPENAI_API_BASE + value: "http://4.144.175.186" + - name: OPENAI_API_KEY + value: "sk-xxxxxxxx" # 不推荐 +``` + +**方式二:使用 Secret(推荐)** +```yaml +spec: + containers: + - name: agent + env: + - name: OPENAI_API_BASE + value: "http://4.144.175.186" + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: agent-{agent-name}-secrets + key: openai-api-key +``` + +### 3.2 Secret 管理 + +Agent-Manager 需要: +1. 在创建 Agent 前,先创建对应的 Secret +2. Secret 名称建议使用 `agent-{agent-name}-secrets` +3. 删除 Agent 时同时删除对应的 Secret + +**创建 Secret 示例**: +```python +from kubernetes import client + +def create_agent_secret(name: str, namespace: str, env_vars: dict): + """创建 Agent 的 Secret""" + # 筛选敏感环境变量 + sensitive_keys = ["OPENAI_API_KEY", "API_KEY", "SECRET_KEY"] + secret_data = {} + + for key, value in env_vars.items(): + if any(sk in key.upper() for sk in sensitive_keys): + secret_data[key.lower().replace("_", "-")] = base64.b64encode(value.encode()).decode() + + if not secret_data: + return None + + secret = client.V1Secret( + metadata=client.V1ObjectMeta( + name=f"agent-{name}-secrets", + namespace=namespace, + labels={"app": "agent", "agent-name": name} + ), + type="Opaque", + data=secret_data + ) + + core_v1 = client.CoreV1Api() + return core_v1.create_namespaced_secret(namespace, secret) +``` + +--- + +## 4. mcp-server 调用示例 + +```python +# mcp-server 中创建 Agent 的代码 +async def create_custom_agent( + name: str, + template: str, + user_id: str, + model_name: str, + db: AsyncSession +): + # 1. 查询租户的模型 Key + tenant_key = await get_tenant_model_key(user_id, model_name, db) + + if not tenant_key: + raise HTTPException(403, f"您没有使用模型 {model_name} 的权限") + + # 2. 解密 Key + decrypted_key = litellm_client.decrypt_key(tenant_key.litellm_key_hash) + + # 3. 构建环境变量 + env_vars = { + "OPENAI_API_BASE": settings.litellm_url, + "OPENAI_API_KEY": decrypted_key, + "MODEL_NAME": model_name, + "LITELLM_MODEL": model_name, + } + + # 4. 调用 Agent Manager + result = await agent_manager_client.create_custom_agent( + name=name, + template=template, + user_id=user_id, + env_vars=env_vars, + config=agent_config + ) + + return result +``` + +--- + +## 5. 测试要点 + +### 5.1 功能测试 + +| 测试项 | 预期结果 | +|--------|----------| +| 创建 Agent 时传入 LiteLLM 环境变量 | Agent Pod 中能读取到这些环境变量 | +| Agent 使用注入的 Key 调用 LiteLLM | 请求成功,返回模型响应 | +| 不传入 LiteLLM 环境变量 | Agent 正常创建,但无法调用模型 | + +### 5.2 安全测试 + +| 测试项 | 预期结果 | +|--------|----------| +| 查看 Pod spec | 敏感环境变量通过 secretKeyRef 引用 | +| 查看 Agent Manager 日志 | 不包含 API Key 明文 | +| 删除 Agent | 对应的 Secret 也被删除 | + +--- + +## 6. 时间线 + +| 阶段 | 时间 | 内容 | +|------|------|------| +| 需求确认 | 2026-01-07 | 确认接口变动范围 | +| 开发实现 | 2026-01-08 ~ 2026-01-10 | Agent Manager 支持 Secret 管理 | +| 联调测试 | 2026-01-11 ~ 2026-01-12 | mcp-server 与 Agent Manager 联调 | +| 上线部署 | 2026-01-13 | 生产环境部署 | + +--- + +## 7. 联系人 + +- **mcp-server 负责人**: [待填写] +- **Agent Manager 负责人**: [待填写] + +--- + +## 附录 A: 现有 Agent Manager 接口参考 + +根据 `plans/agent-manager接口文档.md`,现有接口已支持 `env_vars` 参数,本次变更主要是: + +1. **明确 LiteLLM 相关环境变量的命名规范** +2. **增加敏感环境变量的安全处理要求** +3. **确保 mcp-server 和 Agent Manager 的对接一致性** + +--- + +## 附录 B: LiteLLM 环境变量说明 + +| 环境变量 | 用途 | 示例值 | +|----------|------|--------| +| `OPENAI_API_BASE` | LiteLLM 网关地址 | `http://4.144.175.186` | +| `OPENAI_API_KEY` | 租户的 LiteLLM API Key | `sk-xxxxxxxx` | +| `MODEL_NAME` | 要使用的模型名称 | `azure/gpt-4` | +| `LITELLM_MODEL` | 同 MODEL_NAME | `azure/gpt-4` | + +**Agent 代码中使用示例**: +```python +import os +from openai import OpenAI + +client = OpenAI( + base_url=os.getenv("OPENAI_API_BASE"), + api_key=os.getenv("OPENAI_API_KEY"), +) + +response = client.chat.completions.create( + model=os.getenv("MODEL_NAME", "gpt-4"), + messages=[{"role": "user", "content": "Hello!"}] +) +``` diff --git a/plans/LiteLLM集成实现总结.md b/plans/LiteLLM集成实现总结.md new file mode 100644 index 0000000..a4cba66 --- /dev/null +++ b/plans/LiteLLM集成实现总结.md @@ -0,0 +1,261 @@ +# LiteLLM 集成实现总结 + +> **版本**: v1.0.0 +> **完成时间**: 2026-01-07 +> **状态**: ✅ 已完成 + +--- + +## 1. 实现概述 + +根据「模型供应商与租户模型使用设计方案 v5.0」,已完成 mcp-server 与 LiteLLM 的集成。 + +### 1.1 核心功能 + +| 功能 | 状态 | 说明 | +|------|------|------| +| 创建渠道时同步创建 LiteLLM team | ✅ 已完成 | 渠道 = LiteLLM team | +| 删除渠道时同步删除 LiteLLM team | ✅ 已完成 | 级联删除 | +| 分配模型给租户时创建 LiteLLM key | ✅ 已完成 | 包含 RPM/TPM/Budget 配额 | +| 取消模型分配时删除 LiteLLM key | ✅ 已完成 | 清理资源 | +| 更新租户配额时更新 LiteLLM key | ✅ 已完成 | 实时生效 | +| 租户查看可用模型 | ✅ 已完成 | 从 tenant_model_keys 表查询 | +| Agent 创建时注入 LiteLLM key | ✅ 已完成 | 环境变量注入 | + +--- + +## 2. 新增/修改的文件 + +### 2.1 新增文件 + +| 文件路径 | 说明 | +|----------|------| +| `services/mcp-server/app/litellm_client.py` | LiteLLM Admin API 客户端(525行) | +| `services/mcp-server/migrations/011_add_litellm_integration.sql` | 数据库迁移脚本 | +| `services/mcp-server/migrations/run_011_migration.py` | 迁移执行脚本 | +| `plans/Agent-Manager接口变动需求文档.md` | Agent Manager 对接需求 | + +### 2.2 修改文件 + +| 文件路径 | 修改内容 | +|----------|----------| +| `services/mcp-server/models.py` | 新增 `TenantModelKey` 模型,`Channel` 增加 `litellm_team_id` 字段 | +| `services/mcp-server/config.py` | 更新 LiteLLM master key 配置 | +| `services/mcp-server/app/routes/admin.py` | 渠道创建/删除时同步 LiteLLM team | +| `services/mcp-server/app/routes/channel.py` | 新增模型分配接口(4个端点) | +| `services/mcp-server/app/routes/user.py` | 新增租户模型查询接口,Agent 创建时注入 key | +| `docker-compose.yml` | 添加 LITELLM_URL 和 LITELLM_MASTER_KEY 环境变量 | +| `.env` | 更新 LiteLLM 配置 | + +--- + +## 3. 数据库变更 + +### 3.1 新增表:tenant_model_keys + +```sql +CREATE TABLE tenant_model_keys ( + 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, + litellm_key_id VARCHAR(255) NOT NULL, + litellm_key_hash TEXT NOT NULL, -- 加密存储 + rpm_limit INTEGER DEFAULT 0, + tpm_limit INTEGER DEFAULT 0, + max_budget NUMERIC(12, 2), + budget_duration VARCHAR(20) DEFAULT 'monthly', + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + CONSTRAINT uq_tenant_model UNIQUE(tenant_id, model_name) +); +``` + +### 3.2 修改表:channels + +```sql +ALTER TABLE channels ADD COLUMN litellm_team_id VARCHAR(100); +``` + +--- + +## 4. API 接口清单 + +### 4.1 渠道管理(admin.py) + +| 接口 | 方法 | 功能 | LiteLLM 操作 | +|------|------|------|-------------| +| `/api/admin/channels/create` | POST | 创建渠道 | 创建 team | +| `/api/admin/channels/{id}` | DELETE | 删除渠道 | 删除 team | + +### 4.2 模型分配(channel.py) + +| 接口 | 方法 | 功能 | LiteLLM 操作 | +|------|------|------|-------------| +| `/api/channel/tenants/{id}/models` | PUT | 分配模型给租户 | 创建 key | +| `/api/channel/tenants/{id}/models` | GET | 获取租户模型列表 | - | +| `/api/channel/tenants/{id}/models/{model}` | DELETE | 取消模型分配 | 删除 key | +| `/api/channel/tenants/{id}/models/{model}/quota` | PUT | 更新配额 | 更新 key | + +### 4.3 租户接口(user.py) + +| 接口 | 方法 | 功能 | +|------|------|------| +| `/api/user/models/available` | GET | 获取可用模型列表 | +| `/api/user/models/usage/stats` | GET | 获取用量统计 | +| `/api/user/custom-agents` | POST | 创建 Agent(注入 key) | + +--- + +## 5. 配置说明 + +### 5.1 环境变量 + +| 变量名 | 说明 | 示例值 | +|--------|------|--------| +| `LITELLM_URL` | LiteLLM 网关地址 | `http://4.144.175.186` | +| `LITELLM_MASTER_KEY` | LiteLLM 管理密钥 | `sk-1f06b8f0d2e34c9b8a9f3d75a1c4e9b7-7e3a2c6bd9f441d8` | +| `LITELLM_KEY_ENCRYPTION_KEY` | Key 加密密钥 | Base64 编码的 32 字节密钥 | + +### 5.2 .env 配置 + +```bash +# LiteLLM网关配置 +LITELLM_MASTER_KEY=sk-1f06b8f0d2e34c9b8a9f3d75a1c4e9b7-7e3a2c6bd9f441d8 +LITELLM_URL=http://4.144.175.186 +``` + +--- + +## 6. 测试验证 + +### 6.1 测试流程 + +``` +1. 超级管理员登录 ✅ +2. 创建渠道 → LiteLLM team 创建成功 ✅ +3. 分配模型给渠道 ✅ +4. 渠道管理员登录 ✅ +5. 创建租户 ✅ +6. 分配模型给租户 → LiteLLM key 创建成功 ✅ +7. 租户登录 ✅ +8. 租户查看可用模型 ✅ +``` + +### 6.2 测试结果 + +**创建渠道响应**: +```json +{ + "success": true, + "data": { + "id": "6e147f61-e2a8-422a-ab86-17ebbf3eff7b", + "name": "LiteLLM集成测试渠道", + "litellmTeamId": "4fe022d7-f484-4cf7-ac91-de1ed06aa99c" + } +} +``` + +**分配模型响应**: +```json +{ + "success": true, + "data": { + "tenantId": "7363295d-2a22-4f60-9298-ef65d2715731", + "modelName": "gpt-3.5-turbo", + "rpmLimit": 60, + "tpmLimit": 10000, + "maxBudget": 100.0, + "status": "active" + } +} +``` + +**LiteLLM 中的 Key 信息**: +```json +{ + "models": ["gpt-3.5-turbo"], + "rpm_limit": 60, + "tpm_limit": 10000, + "max_budget": 100.0, + "budget_duration": "monthly", + "metadata": { + "tenant_id": "7363295d-2a22-4f60-9298-ef65d2715731", + "channel_id": "6e147f61-e2a8-422a-ab86-17ebbf3eff7b", + "tenant_name": "测试租户", + "channel_name": "LiteLLM集成测试渠道" + } +} +``` + +--- + +## 7. 待完成事项 + +### 7.1 Agent Manager 对接 + +需要 Agent Manager 支持: +1. 接收 `env_vars` 参数中的 LiteLLM 环境变量 +2. 将敏感环境变量(如 `OPENAI_API_KEY`)存储为 Kubernetes Secret +3. 在 Pod spec 中通过 `secretKeyRef` 引用 + +详见:`plans/Agent-Manager接口变动需求文档.md` + +### 7.2 充值功能 + +充值接口 `/api/user/billing/recharge` 需要: +1. 更新 `tenant_model_keys.max_budget` +2. 调用 LiteLLM `/key/update` 更新 budget + +### 7.3 用量统计 + +用量统计接口需要: +1. 调用 LiteLLM `/spend/logs` 获取实际用量 +2. 与本地记录对比 + +--- + +## 8. 架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ mcp-server │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ admin.py │ │ channel.py │ │ user.py │ │ +│ │ 创建/删除渠道│ │ 分配模型给租户│ │ 查询可用模型/创建Agent │ │ +│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │ +│ │ │ │ │ +│ └────────────────┼──────────────────────┘ │ +│ │ │ +│ ┌───────▼───────┐ │ +│ │ litellm_client│ │ +│ │ (Admin API) │ │ +│ └───────┬───────┘ │ +└──────────────────────────┼───────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ LiteLLM Gateway │ +│ http://4.144.175.186 │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ /team/new │ │/key/generate│ │ /chat/completions │ │ +│ │ /team/delete│ │ /key/update │ │ (Agent 调用) │ │ +│ └─────────────┘ │ /key/delete │ └─────────────────────────┘ │ +│ └─────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 9. 总结 + +LiteLLM 集成已完成核心功能: + +1. ✅ **渠道 = LiteLLM team**:创建渠道时自动创建 team +2. ✅ **租户模型 = LiteLLM key**:分配模型时创建带配额的 key +3. ✅ **配额管理**:RPM/TPM/Budget 由 LiteLLM 执行 +4. ✅ **Key 加密存储**:使用 Fernet 加密存储 API Key +5. ✅ **实时生效**:配额更新后立即生效,无需重启 + +下一步:与 Agent Manager 联调,确保 Agent 能正确使用注入的 LiteLLM key 调用模型。 diff --git a/plans/LiteLLM集成接口变动清单.md b/plans/LiteLLM集成接口变动清单.md new file mode 100644 index 0000000..9edae94 --- /dev/null +++ b/plans/LiteLLM集成接口变动清单.md @@ -0,0 +1,406 @@ +# LiteLLM 集成接口变动清单 + +> **版本**: v1.0.0 +> **创建时间**: 2026-01-07 +> **状态**: 已实现 + +--- + +## 1. 超级管理员端接口变动 (admin.py) + +### 1.1 已修改接口 + +#### POST /api/admin/channels/create - 创建渠道 + +**变动说明**: 创建渠道时同步在 LiteLLM 中创建对应的 team + +**请求参数**: 无变化 + +**响应变动**: +```json +{ + "success": true, + "data": { + "id": "渠道ID", + "name": "渠道名称", + "email": "渠道邮箱", + "litellmTeamId": "LiteLLM team ID (新增)", + "litellmWarning": "LiteLLM 创建失败时的警告信息 (可选)" + }, + "message": "渠道创建成功" +} +``` + +**新增响应字段**: +| 字段 | 类型 | 说明 | +|------|------|------| +| `litellmTeamId` | string | LiteLLM 中创建的 team ID | +| `litellmWarning` | string | 可选,LiteLLM 操作失败时的警告信息 | + +--- + +#### DELETE /api/admin/channels/{channel_id} - 删除渠道 + +**变动说明**: 删除渠道时同步删除 LiteLLM 中对应的 team + +**请求参数**: 无变化 + +**响应变动**: +```json +{ + "success": true, + "data": { + "id": "渠道ID", + "litellmWarning": "LiteLLM 删除失败时的警告信息 (可选)" + }, + "message": "渠道已删除" +} +``` + +**新增响应字段**: +| 字段 | 类型 | 说明 | +|------|------|------| +| `litellmWarning` | string | 可选,LiteLLM 操作失败时的警告信息 | + +--- + +### 1.2 数据库变动 + +#### Channel 表新增字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `litellm_team_id` | VARCHAR(100) | LiteLLM 中对应的 team ID | + +--- + +## 2. 渠道端接口变动 (channel.py) + +### 2.1 新增接口 + +#### PUT /api/channel/tenants/{tenant_id}/models - 分配模型给租户 + +**功能**: 为租户创建 LiteLLM API Key,绑定指定的模型和配额 + +**请求参数** (Query): +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `model_name` | string | 是 | 模型名称,如 `azure/gpt-4` | +| `rpm_limit` | int | 否 | 每分钟请求数限制,默认 60 | +| `tpm_limit` | int | 否 | 每分钟 Token 数限制,默认 10000 | +| `max_budget` | float | 否 | 最大预算,默认 100.0 | +| `budget_duration` | string | 否 | 预算周期,`monthly` 或 `total`,默认 `monthly` | +| `channel_id` | string | 否 | 渠道ID(超级管理员必填) | + +**响应**: +```json +{ + "success": true, + "data": { + "tenantId": "租户ID", + "tenantName": "租户名称", + "modelName": "模型名称", + "rpmLimit": 60, + "tpmLimit": 10000, + "maxBudget": 100.0, + "budgetDuration": "monthly", + "status": "active" + }, + "message": "模型 'xxx' 分配成功" +} +``` + +**权限**: `manage:resources` (channel_admin, billing_admin, super_admin) + +--- + +#### GET /api/channel/tenants/{tenant_id}/models - 获取租户的模型分配列表 + +**功能**: 返回租户已分配的所有模型及其配额信息 + +**请求参数** (Query): +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `channel_id` | string | 否 | 渠道ID(超级管理员必填) | + +**响应**: +```json +{ + "success": true, + "data": { + "tenantId": "租户ID", + "tenantName": "租户名称", + "models": [ + { + "modelName": "azure/gpt-4", + "rpmLimit": 60, + "tpmLimit": 10000, + "maxBudget": 100.0, + "budgetDuration": "monthly", + "status": "active", + "createdAt": "2026-01-07T12:00:00", + "updatedAt": "2026-01-07T12:00:00" + } + ] + } +} +``` + +**权限**: `view:resources` (channel_admin, billing_admin, operations_admin, super_admin) + +--- + +#### DELETE /api/channel/tenants/{tenant_id}/models/{model_name} - 取消租户的模型分配 + +**功能**: 删除租户在 LiteLLM 中的 API Key,租户将无法再使用该模型 + +**请求参数** (Query): +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `channel_id` | string | 否 | 渠道ID(超级管理员必填) | + +**响应**: +```json +{ + "success": true, + "data": { + "tenantId": "租户ID", + "tenantName": "租户名称", + "modelName": "模型名称", + "litellmWarning": "LiteLLM Key 删除失败时的警告信息 (可选)" + }, + "message": "模型 'xxx' 分配已取消" +} +``` + +**权限**: `manage:resources` (channel_admin, billing_admin, super_admin) + +--- + +#### PUT /api/channel/tenants/{tenant_id}/models/{model_name}/quota - 更新租户的模型配额 + +**功能**: 更新租户在 LiteLLM 中的 API Key 配额,更新后立即生效 + +**请求参数** (Query): +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `rpm_limit` | int | 否 | 每分钟请求数限制 | +| `tpm_limit` | int | 否 | 每分钟 Token 数限制 | +| `max_budget` | float | 否 | 最大预算 | +| `budget_duration` | string | 否 | 预算周期,`monthly` 或 `total` | +| `channel_id` | string | 否 | 渠道ID(超级管理员必填) | + +**响应**: +```json +{ + "success": true, + "data": { + "tenantId": "租户ID", + "tenantName": "租户名称", + "modelName": "模型名称", + "rpmLimit": 120, + "tpmLimit": 20000, + "maxBudget": 200.0, + "budgetDuration": "monthly", + "status": "active" + }, + "message": "模型配额更新成功,立即生效" +} +``` + +**权限**: `manage:resources` (channel_admin, billing_admin, super_admin) + +--- + +### 2.2 数据库变动 + +#### 新增表: tenant_model_keys + +```sql +CREATE TABLE tenant_model_keys ( + 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, + + -- LiteLLM Key 信息 + litellm_key_id VARCHAR(255) NOT NULL, -- LiteLLM 返回的完整 key + litellm_key_hash TEXT NOT NULL, -- 加密存储 + + -- 配额配置(与 LiteLLM 同步) + rpm_limit INTEGER DEFAULT 0, + tpm_limit INTEGER DEFAULT 0, + max_budget NUMERIC(12, 2), + budget_duration VARCHAR(20) DEFAULT 'monthly', + + -- 状态 + status VARCHAR(20) DEFAULT 'active', + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + UNIQUE(tenant_id, model_name) +); +``` + +--- + +## 3. 用户端接口变动 (user.py) + +### 3.1 新增接口 + +#### GET /api/user/models/available - 获取可用模型列表 + +**功能**: 返回当前租户已分配的所有模型 + +**响应**: +```json +{ + "success": true, + "data": { + "models": [ + { + "modelName": "azure/gpt-4", + "rpmLimit": 60, + "tpmLimit": 10000, + "maxBudget": 100.0, + "budgetDuration": "monthly", + "status": "active" + } + ] + } +} +``` + +**权限**: 已认证用户 + +--- + +#### GET /api/user/models/usage/stats - 获取模型用量统计 + +**功能**: 从 LiteLLM 获取当前租户的模型用量统计 + +**响应**: +```json +{ + "success": true, + "data": { + "models": [ + { + "modelName": "azure/gpt-4", + "rpmLimit": 60, + "tpmLimit": 10000, + "maxBudget": 100.0, + "budgetDuration": "monthly", + "spend": 25.50, + "budgetRemaining": 74.50, + "status": "active" + } + ], + "totalSpend": 25.50, + "totalBudget": 100.0 + } +} +``` + +**权限**: 已认证用户 + +--- + +### 3.2 已修改接口 + +#### POST /api/user/custom-agents - 创建自定义 Agent + +**变动说明**: 创建 Agent 时自动注入 LiteLLM Key 环境变量 + +**新增逻辑**: +1. 查询租户的模型 Key +2. 如果租户有可用的模型 Key,自动注入以下环境变量: + - `OPENAI_API_BASE`: LiteLLM 网关地址 + - `OPENAI_API_KEY`: 租户的 LiteLLM Key + - `MODEL_NAME`: 模型名称 + +--- + +## 4. 接口变动汇总 + +### 4.1 超级管理员端 (admin.py) + +| 接口 | 方法 | 变动类型 | 说明 | +|------|------|----------|------| +| `/api/admin/channels/create` | POST | 修改 | 响应新增 `litellmTeamId` 字段 | +| `/api/admin/channels/{id}` | DELETE | 修改 | 同步删除 LiteLLM team | + +### 4.2 渠道端 (channel.py) + +| 接口 | 方法 | 变动类型 | 说明 | +|------|------|----------|------| +| `/api/channel/tenants/{id}/models` | PUT | **新增** | 分配模型给租户 | +| `/api/channel/tenants/{id}/models` | GET | **新增** | 获取租户的模型列表 | +| `/api/channel/tenants/{id}/models/{model}` | DELETE | **新增** | 取消模型分配 | +| `/api/channel/tenants/{id}/models/{model}/quota` | PUT | **新增** | 更新模型配额 | + +### 4.3 用户端 (user.py) + +| 接口 | 方法 | 变动类型 | 说明 | +|------|------|----------|------| +| `/api/user/models/available` | GET | **新增** | 获取可用模型列表 | +| `/api/user/models/usage/stats` | GET | **新增** | 获取模型用量统计 | +| `/api/user/custom-agents` | POST | 修改 | 自动注入 LiteLLM Key | + +--- + +## 5. 前端对接注意事项 + +### 5.1 渠道管理员界面 + +1. **租户详情页** 需要新增"模型管理"标签页,包含: + - 模型列表展示 + - 分配模型按钮 + - 配额编辑功能 + - 取消分配功能 + +2. **分配模型表单** 需要包含: + - 模型选择(下拉框,从渠道已有模型中选择) + - RPM 限制输入 + - TPM 限制输入 + - 预算限制输入 + - 预算周期选择(月度/总计) + +### 5.2 租户用户界面 + +1. **仪表板** 可以展示: + - 可用模型列表 + - 各模型的配额使用情况 + - 预算剩余 + +2. **创建 Agent** 时: + - 如果租户有可用模型,Agent 会自动获得访问权限 + - 无需用户手动配置 API Key + +### 5.3 超级管理员界面 + +1. **渠道列表** 可以展示 `litellmTeamId`(可选) +2. **创建渠道** 后检查响应中的 `litellmWarning` 字段 + +--- + +## 6. 错误处理 + +### 6.1 常见错误码 + +| 错误码 | 说明 | +|--------|------| +| 400 | 参数错误(如无效的渠道ID格式) | +| 403 | 权限不足(如渠道没有该模型的权限) | +| 404 | 资源不存在(如租户不存在、模型分配记录不存在) | +| 500 | LiteLLM 操作失败 | + +### 6.2 LiteLLM 操作失败处理 + +- 创建渠道时 LiteLLM team 创建失败:渠道仍会创建成功,响应中包含 `litellmWarning` +- 删除渠道时 LiteLLM team 删除失败:渠道仍会删除成功,响应中包含 `litellmWarning` +- 分配模型时 LiteLLM Key 创建失败:返回 500 错误,分配失败 +- 更新配额时 LiteLLM Key 更新失败:返回 500 错误,更新失败 diff --git a/plans/超级管理员控制台-后端接口需求清单(已人工审核).md b/plans/超级管理员控制台-后端接口需求清单(已人工审核).md deleted file mode 100644 index eff39b7..0000000 --- a/plans/超级管理员控制台-后端接口需求清单(已人工审核).md +++ /dev/null @@ -1,1503 +0,0 @@ -# 超级管理员控制台 - 后端接口清单 - -> **版本**: v1.2.0 -> **更新时间**: 2026-01-06 -> **说明**: 本文档基于前端业务逻辑分析,列出所有后端接口需求,包括已对接接口和未对接接口,按钮操作接口和数据展示接口 - ---- - -## 目录 - -1. [概览模块 (Overview)](#概览模块-overview) -2. [渠道管理模块 (Channels)](#渠道管理模块-channels) -3. [资源管理模块 (Resources)](#资源管理模块-resources) -4. [监控模块 (Monitoring)](#监控模块-monitoring) -5. [计费模块 (Billing)](#计费模块-billing) -6. [设置模块 (Settings)](#设置模块-settings) -7. [附录:接口汇总表](#附录接口汇总表) - ---- - -## 概览模块 (Overview) - -### 数据展示接口 - -#### D1. 仪表板统计接口 ✅ 已对接 - -**展示位置**: 概览页面 → 顶部统计卡片区域 - -**展示内容**: -- 总渠道数(如:5) -- 总租户数(如:7) -- 总收入(如:$0) - -**功能描述**: 获取平台整体统计数据,用于概览页面顶部的统计卡片展示 - -**接口**: -``` -GET /api/admin/dashboard/stats -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.totalChannels | int | 总渠道数 | -| data.totalTenants | int | 总租户数 | -| data.totalRevenue | float | 总收入 | -| data.totalAgents | int | 总Agent数(用于活跃指标) | - -**前端调用**: `TaijiAPIClient.getAdminDashboardStats()` - ---- - -#### D2. 系统监控指标接口 ✅ 已对接 - -**展示位置**: 概览页面 → 系统指标卡片 - -**展示内容**: -- CPU使用率(如:5.9%) -- 内存使用率(如:33.6%) -- 存储使用率(如:64.8%) -- 活跃Agent(如:0%) - -**功能描述**: 获取平台整体的系统监控指标 - -**接口**: -``` -GET /api/v1/monitoring/metrics -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.cpu_usage | float | CPU使用率百分比 | -| data.memory_usage | float | 内存使用率百分比 | -| data.disk_usage | float | 存储使用率百分比 | -| data.system.cpu_usage_percent | float | 备选:CPU使用率 | -| data.system.memory_usage_percent | float | 备选:内存使用率 | -| data.system.disk_usage_percent | float | 备选:存储使用率 | - -**前端调用**: `TaijiAPIClient.getMonitoringMetrics()` - ---- - -#### D3. 最近登录租户列表接口 ✅ 已对接 - -**展示位置**: 概览页面 → 最近登录的租户列表 - -**展示内容**: 显示最近登录的租户列表,包含租户名称、邮箱、渠道、状态等 - -**功能描述**: 获取最近登录的租户列表,用于概览页面展示 - -**接口**: -``` -GET /api/admin/dashboard/recent-logins?limit=10 -``` - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| limit | int | 否 | 返回数量,默认10,最多50 | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.recentTenants | array | 最近登录的租户列表 | -| data.recentTenants[].id | string | 租户ID | -| data.recentTenants[].name | string | 租户名称 | -| data.recentTenants[].email | string | 邮箱 | -| data.recentTenants[].channelName | string | 所属渠道 | -| data.recentTenants[].lastLoginAt | string | 最后登录时间 | -| data.recentTenants[].status | string | 状态 | - -**前端调用**: `TaijiAPIClient.getRecentLogins(10)` - ---- - -#### D4. 平台资源分配统计接口 ✅ 已对接(复用) - -**展示位置**: 概览页面 → 平台资源分配统计卡片 - -**展示内容**: -- 已分配CPU(如:0.0 核) -- 已分配内存(如:0.0 GB) -- 共 X 个 Agent -- 平均 X GB/Agent - -**功能描述**: 获取平台所有Agent的资源分配汇总统计 - -**复用接口**: -``` -GET /api/admin/platform-agents/status -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.summary.total | int | Agent总数 | -| data.summary.totalCpuAllocated | float | 已分配CPU总核数 | -| data.summary.totalMemoryAllocated | float | 已分配内存总量(GB) | -| data.summary.avgMemoryPerAgent | float | 平均每Agent内存 | - -**前端调用**: `TaijiAPIClient.getPlatformAgentStatus()` - ---- - -### 按钮操作接口 - -## 渠道管理模块 (Channels) - -### 数据展示接口 - -#### D2. 渠道统计概览接口 - -**展示位置**: 渠道管理页面 → 渠道列表卡片 - -**展示内容**: 每个渠道卡片显示租户数、月收入、佣金比例等统计信息 - -**功能描述**: 获取渠道列表及其统计数据,用于渠道卡片展示 - -**接口需求**: -``` -GET /api/admin/channels/stats -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.channels | array | 渠道列表(含统计数据) | - ---- - -### 按钮操作接口 - -#### 2. 搜索渠道按钮 - -**按钮位置**: 渠道管理页面 → 搜索框 - -**按钮作用**: 在渠道列表中搜索特定渠道 - -**功能描述**: 用户输入关键词后,根据渠道名称、联系人、邮箱等字段进行模糊搜索 - -**接口需求**: -``` -GET /api/admin/channels/search -``` - -**请求参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| keyword | string | 是 | 搜索关键词 | -| status | string | 否 | 状态筛选(active/inactive) | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.channels | array | 匹配的渠道列表 | - ---- - -#### 3. 查看渠道详情按钮 - -**按钮位置**: 渠道管理页面 → 渠道卡片 → 更多操作菜单 → "查看详情" - -**按钮作用**: 查看渠道的完整详细信息 - -**功能描述**: 点击后弹出对话框,显示渠道的基本信息、资源配置、配额信息等详细数据 - -**接口需求**: -``` -GET /api/admin/channels/{channel_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 是 | 渠道ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 渠道ID | -| data.name | string | 渠道名称 | -| data.email | string | 联系邮箱 | -| data.status | string | 状态 | -| data.createdAt | string | 创建时间 | -| data.cpuCores | float | 分配的CPU核心数 | -| data.memory | string | 分配的内存大小 | -| data.tenantCount | int | 租户总数 | -| data.creditLimit | float | 授信额度 | -| data.usedCredit | float | 已用授信 | -| data.remainingCredit | float | 剩余授信 | -| data.commissionRate | float | 佣金比例 | - ---- - -#### 4. 删除渠道按钮 - -**按钮位置**: 渠道管理页面 → 渠道卡片 → 更多操作菜单 → "删除渠道" - -**按钮作用**: 删除指定渠道(软删除) - -**功能描述**: 点击后弹出确认对话框,确认后将渠道状态设为inactive,要求渠道下无活跃租户 - -**接口需求**: -``` -DELETE /api/admin/channels/{channel_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 是 | 渠道ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| message | string | 操作结果消息 | - ---- - -#### 5. 删除租户按钮 - -**按钮位置**: 渠道管理页面 → 查看租户对话框 → 租户列表 → "删除"按钮 - -**按钮作用**: 删除指定租户(软删除) - -**功能描述**: 点击后弹出确认对话框,确认后将租户状态设为inactive - -**接口需求**: -``` -DELETE /api/channel/tenants/{tenant_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| tenant_id | string | 是 | 租户ID | - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 超级管理员必填 | 渠道ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| message | string | 操作结果消息 | - ---- - -#### 6. 禁用租户按钮 - -**按钮位置**: 渠道管理页面 → 查看租户对话框 → 租户列表 → "禁用"按钮 - -**按钮作用**: 暂停租户账号 - -**功能描述**: 将租户状态设为suspended,租户将无法登录和使用服务 - -**接口需求**: -``` -PUT /api/channel/tenants/{tenant_id}/status -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| tenant_id | string | 是 | 租户ID | - -**请求体**: -```json -{ - "status": "suspended" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.tenantId | string | 租户ID | -| data.status | string | 新状态 | -| message | string | 操作结果消息 | - ---- - -#### 7. 修改租户密码按钮 - -**按钮位置**: 渠道管理页面 → 查看租户对话框 → 租户列表 → "修改密码"按钮 - -**按钮作用**: 重置租户登录密码 - -**功能描述**: 点击后弹出对话框,输入新密码和确认密码,提交后更新租户密码 - -**接口需求**: -``` -PUT /api/channel/tenants/{tenant_id}/password -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| tenant_id | string | 是 | 租户ID | - -**请求体**: -```json -{ - "newPassword": "NewSecurePass123" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.tenantId | string | 租户ID | -| message | string | 操作结果消息 | - ---- - -#### 8. 管理租户权限按钮 - -**按钮位置**: 渠道管理页面 → 查看租户对话框 → 租户列表 → "管理权限"按钮 - -**按钮作用**: 配置租户的功能访问权限 - -**功能描述**: 点击后弹出对话框,显示权限复选框列表,勾选后保存租户的权限配置 - -**接口需求**: -``` -PUT /api/channel/tenants/{tenant_id}/permissions -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| tenant_id | string | 是 | 租户ID | - -**请求体**: -```json -{ - "permissions": ["dashboard", "agents", "models", "billing", "resources", "data-tools", "api-gateway"] -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.tenantId | string | 租户ID | -| data.permissions | array | 更新后的权限列表 | -| message | string | 操作结果消息 | - ---- - -#### 9. 供应商申请审批-拒绝按钮 - -**按钮位置**: 渠道管理页面 → 渠道申请审批表格 → "审批"按钮 → 审批对话框 → "拒绝"按钮 - -**按钮作用**: 拒绝渠道的供应商申请 - -**功能描述**: 点击后将申请状态设为rejected,渠道将无法使用该供应商 - -**接口需求**: -``` -PUT /api/admin/providers/applications/{application_id}/review -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| application_id | string | 是 | 申请ID | - -**请求体**: -```json -{ - "approved": false, - "reason": "申请被拒绝" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.applicationId | string | 申请ID | -| data.status | string | 新状态(rejected) | -| message | string | 操作结果消息 | - ---- - -#### 10. 供应商申请审批-批准按钮 - -**按钮位置**: 渠道管理页面 → 渠道申请审批表格 → "审批"按钮 → 审批对话框 → "批准"按钮 - -**按钮作用**: 批准渠道的供应商申请 - -**功能描述**: 点击后将申请状态设为approved,自动创建ChannelProviderAccess记录 - -**接口需求**: -``` -PUT /api/admin/providers/applications/{application_id}/review -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| application_id | string | 是 | 申请ID | - -**请求体**: -```json -{ - "approved": true, - "reason": "申请已批准" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.applicationId | string | 申请ID | -| data.status | string | 新状态(approved) | -| message | string | 操作结果消息 | - ---- - -#### 11. 平台Agent申请审批-拒绝按钮 - -**按钮位置**: 渠道管理页面 → 平台Agent申请审批表格 → "审批"按钮 → 审批对话框 → "拒绝"按钮 - -**按钮作用**: 拒绝渠道的平台Agent申请 - -**功能描述**: 点击后将申请状态设为rejected,渠道将无法使用该平台Agent - -**接口需求**: -``` -PUT /api/admin/applications/platform-agents/{application_id}/review -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| application_id | string | 是 | 申请ID | - -**请求体**: -```json -{ - "action": "reject", - "reviewReason": "申请被拒绝" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.applicationId | string | 申请ID | -| data.status | string | 新状态(rejected) | -| message | string | 操作结果消息 | - ---- - -#### 12. 平台Agent申请审批-批准按钮 - -**按钮位置**: 渠道管理页面 → 平台Agent申请审批表格 → "审批"按钮 → 审批对话框 → "批准"按钮 - -**按钮作用**: 批准渠道的平台Agent申请 - -**功能描述**: 点击后将申请状态设为approved,为渠道分配指定数量的Pod配额 - -**接口需求**: -``` -PUT /api/admin/applications/platform-agents/{application_id}/review -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| application_id | string | 是 | 申请ID | - -**请求体**: -```json -{ - "action": "approve", - "podQuota": 5, - "reviewReason": "申请已批准" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.applicationId | string | 申请ID | -| data.status | string | 新状态(approved) | -| message | string | 操作结果消息 | - ---- - -#### 27. 保存资源配置按钮 - -**按钮位置**: 渠道管理页面 → 渠道卡片 → "资源管理"菜单项 → 资源管理对话框 → "保存配置"按钮 - -**按钮作用**: 保存渠道的资源配置(模型、Agent、自定义Agent资源、授信额度) - -**功能描述**: 为渠道配置可用的模型供应商、Agent分配及数量、自定义Agent的CPU/内存资源、授信额度 - -**接口需求**: -``` -PUT /api/admin/channels/{channel_id}/resources -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 是 | 渠道ID | - -**请求体**: -```json -{ - "models": ["model-id-1", "model-id-2"], - "agents": [ - {"agentId": "agent-id-1", "quantity": 10}, - {"agentId": "agent-id-2", "quantity": 5} - ], - "customAgentResources": {"cpu": 2.0, "memory": 4.0}, - "channelCredit": 100000.00 -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.channelId | string | 渠道ID | -| message | string | 操作结果消息 | - ---- - -#### 28. 保存渠道编辑按钮 - -**按钮位置**: 渠道管理页面 → 渠道卡片 → "编辑"菜单项 → 编辑对话框 → "保存更改"按钮 - -**按钮作用**: 保存渠道基本信息的修改 - -**功能描述**: 修改渠道名称、联系人、邮箱、电话等基本信息 - -**接口需求**: -``` -PUT /api/admin/channels/{channel_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 是 | 渠道ID | - -**请求体**: -```json -{ - "name": "更新后的渠道名", - "email": "newemail@channel.com", - "contactName": "张三", - "phone": "+86-10-12345678" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 渠道ID | -| message | string | 操作结果消息 | - ---- - -#### 29. 保存佣金修改按钮 - -**按钮位置**: 渠道管理页面 → 渠道卡片 → "修改佣金"菜单项 → 佣金对话框 → "保存"按钮 - -**按钮作用**: 更新渠道的佣金比例 - -**功能描述**: 修改渠道的佣金分成比例 - -**接口需求**: -``` -PUT /api/admin/channels/{channel_id}/commission -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 是 | 渠道ID | - -**请求体**: -```json -{ - "commissionRate": 0.18 -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.channelId | string | 渠道ID | -| data.commissionRate | float | 新的佣金比例 | -| message | string | 操作结果消息 | - ---- - -#### 30. 创建渠道按钮 - -**按钮位置**: 渠道管理页面 → "添加渠道"按钮 → 创建对话框 → "创建"按钮 - -**按钮作用**: 创建新的分销渠道 - -**功能描述**: 填写渠道名称、邮箱、密码、佣金比例,创建新渠道账户 - -**接口需求**: -``` -POST /api/admin/channels/create -``` - -**请求体**: -```json -{ - "name": "新渠道", - "email": "channel@example.com", - "password": "SecurePass123", - "commissionRate": 0.15 -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 渠道ID | -| data.name | string | 渠道名称 | -| message | string | 操作结果消息 | - ---- - -#### 31. 添加租户按钮 - -**按钮位置**: 渠道管理页面 → 查看租户对话框 → "添加租户"按钮 → 添加租户对话框 → "创建租户"按钮 - -**按钮作用**: 为渠道创建新租户或管理员 - -**功能描述**: 填写租户名称、邮箱、密码、系统权限,创建新租户或渠道管理员 - -**接口需求(创建租户)**: -``` -POST /api/channel/tenants/create -``` - -**请求体**: -```json -{ - "name": "租户名称", - "email": "tenant@example.com", - "password": "SecurePass123", - "subscriptionTier": "free", - "channelId": "channel-uuid" -} -``` - -**接口需求(创建渠道管理员)**: -``` -POST /api/admin/admins/create -``` - -**请求体**: -```json -{ - "name": "管理员名称", - "email": "admin@example.com", - "password": "SecurePass123", - "role": "billing_admin", - "channelId": "channel-uuid" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 用户ID | -| message | string | 操作结果消息 | - ---- - -#### 32. 删除渠道管理员按钮 - -**按钮位置**: 渠道管理页面 → 编辑渠道对话框 → 管理员管理区域 → 管理员行 → 删除图标按钮 - -**按钮作用**: 从渠道移除管理员 - -**功能描述**: 点击后将管理员从该渠道移除 - -**接口需求**: -``` -DELETE /api/admin/channels/{channel_id}/admins/{admin_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| channel_id | string | 是 | 渠道ID | -| admin_id | string | 是 | 管理员ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| message | string | 操作结果消息 | - ---- - -## 资源管理模块 (Resources) - -### 数据展示接口 - -#### D3. Agent模板列表接口 - -**展示位置**: 资源管理页面 → 平台Agent模板管理区域 - -**展示内容**: 显示所有可用的Agent模板卡片,包含名称、描述、CPU/内存配置等 - -**功能描述**: 获取平台所有Agent模板的列表和配置信息 - -**接口需求**: -``` -GET /api/admin/platform-agents/templates -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.templates | array | 模板列表 | -| data.templates[].id | string | 模板ID | -| data.templates[].name | string | 模板名称 | -| data.templates[].displayName | string | 显示名称 | -| data.templates[].description | string | 模板描述 | -| data.templates[].cpuRequest | string | CPU请求量 | -| data.templates[].cpuLimit | string | CPU上限 | -| data.templates[].memoryRequest | string | 内存请求量 | -| data.templates[].memoryLimit | string | 内存上限 | -| data.templates[].maxPods | int | 最大Pod数量 | -| data.templates[].isEnabled | bool | 是否启用 | - ---- - -#### D4. 模型供应商列表接口 - -**展示位置**: 资源管理页面 → 货源供应商管理区域 - -**展示内容**: 显示所有模型供应商卡片,包含名称、状态、支持模型数、RPM/TPM等 - -**功能描述**: 获取平台所有模型供应商的列表和配置信息 - -**接口需求**: -``` -GET /api/providers/models -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.providers | array | 供应商列表 | -| data.providers[].id | string | 供应商ID | -| data.providers[].name | string | 供应商名称 | -| data.providers[].provider | string | 供应商类型 | -| data.providers[].supportedModels | array | 支持的模型列表 | -| data.providers[].rpm | int | 每分钟请求数限制 | -| data.providers[].tpm | int | 每分钟令牌数限制 | -| data.providers[].status | string | 状态 | - ---- - -### 按钮操作接口 - -#### 13. Agent模板配置-保存按钮 - -**按钮位置**: 资源管理页面 → Agent模板卡片 → "配置"按钮 → 配置对话框 → "保存配置"按钮 - -**按钮作用**: 保存Agent模板的K8s资源配置 - -**功能描述**: 配置Agent模板的CPU请求/限制、内存请求/限制、最大实例数等参数 - -**接口需求**: -``` -PUT /api/admin/platform-agents/templates/{name}/config -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| name | string | 是 | 模板名称(如 echo_agent) | - -**请求体**: -```json -{ - "cpuRequest": "100m", - "cpuLimit": "500m", - "memoryRequest": "128Mi", - "memoryLimit": "512Mi", - "maxPods": 10, - "isEnabled": true, - "displayName": "Echo 测试服务", - "description": "简单的 Echo 服务,用于测试和调试" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.templateName | string | 模板名称 | -| message | string | 操作结果消息 | - ---- - -#### 14. Agent模板删除按钮 - -**按钮位置**: 资源管理页面 → Agent模板卡片 → "删除"按钮 - -**按钮作用**: 删除Agent模板配置 - -**功能描述**: 点击后弹出确认对话框,确认后删除该Agent模板的配置 - -**接口需求**: -``` -DELETE /api/admin/platform-agents/templates/{name} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| name | string | 是 | 模板名称 | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| message | string | 操作结果消息 | - ---- - -#### 15. 添加模型供应商按钮 - -**按钮位置**: 资源管理页面 → "添加模型供应商"按钮 - -**按钮作用**: 创建新的模型供应商配置 - -**功能描述**: 点击后弹出对话框,填写供应商名称、API URL、API密钥、支持的模型列表、RPM/TPM限制等信息 - -**接口需求**: -``` -POST /api/providers/models/create -``` - -**请求体**: -```json -{ - "name": "OpenAI", - "provider": "openai", - "apiKey": "sk-...", - "apiUrl": "https://api.openai.com/v1", - "supportedModels": ["gpt-4", "gpt-3.5-turbo"], - "rpm": 1000, - "tpm": 100000 -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 供应商ID | -| message | string | 操作结果消息 | - ---- - -#### 16. 模型供应商配置按钮 - -**按钮位置**: 资源管理页面 → 模型供应商卡片 → "配置"按钮 - -**按钮作用**: 修改模型供应商配置 - -**功能描述**: 点击后弹出对话框,可修改供应商的API URL、API密钥、支持的模型列表、RPM/TPM限制等 - -**接口需求**: -``` -PUT /api/providers/models/{provider_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| provider_id | string | 是 | 供应商ID | - -**请求体**: -```json -{ - "name": "OpenAI", - "apiUrl": "https://api.openai.com/v1", - "apiKey": "sk-...", - "supportedModels": ["gpt-4", "gpt-3.5-turbo", "gpt-4-turbo"], - "rpm": 2000, - "tpm": 200000 -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 供应商ID | -| message | string | 操作结果消息 | - ---- - -#### 17. 模型供应商测试延迟按钮 - -**按钮位置**: 资源管理页面 → 模型供应商卡片 → "测试延迟"按钮 - -**按钮作用**: 测试与模型供应商的连接状态和延迟 - -**功能描述**: 点击后向供应商API发送测试请求,返回连接状态和响应延迟 - -**接口需求**: -``` -POST /api/providers/models/{provider_id}/test -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| provider_id | string | 是 | 供应商ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.status | string | 连接状态(connected/failed) | -| data.latency | int | 响应延迟(毫秒) | -| data.message | string | 测试结果消息 | - ---- - -#### 18. 模型供应商删除按钮 - -**按钮位置**: 资源管理页面 → 模型供应商卡片 → "删除"按钮 - -**按钮作用**: 删除模型供应商配置 - -**功能描述**: 点击后弹出确认对话框,确认后删除该供应商配置 - -**接口需求**: -``` -DELETE /api/providers/models/{provider_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| provider_id | string | 是 | 供应商ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| message | string | 操作结果消息 | - ---- - -## 监控模块 (Monitoring) - -### 数据展示接口 - -#### D5. Agent健康监控汇总接口 - -**展示位置**: 监控页面 → Agent健康监控区域 → 汇总统计卡片 - -**展示内容**: 显示Agent总数、健康Agent数、警告/异常Agent数等汇总统计 - -**功能描述**: 获取所有Agent的健康状态汇总统计 - -**接口需求**: -``` -GET /api/admin/platform-agents/status -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.summary.total | int | Agent总数 | -| data.summary.byHealthStatus.healthy | int | 健康Agent数 | -| data.summary.byHealthStatus.warning | int | 警告Agent数 | -| data.summary.byHealthStatus.critical | int | 异常Agent数 | -| data.agents | array | Agent详细列表 | - ---- - -#### D6. Agent详细指标接口 - -**展示位置**: 监控页面 → Agent健康监控区域 → Agent卡片 - -**展示内容**: 每个Agent卡片显示CPU使用率、内存使用率、CPU/内存上限、运行状态等 - -**功能描述**: 获取每个Agent的详细资源使用指标 - -**接口需求**: -``` -GET /api/admin/platform-agents/status -``` - -**响应字段(agents数组中每个元素)**: -| 字段 | 类型 | 说明 | -|------|------|------| -| id | string | Agent ID | -| name | string | Agent名称 | -| type | string | Agent类型(platform/custom) | -| healthStatus | string | 健康状态(healthy/warning/critical) | -| cpuUsage | string | CPU实际使用量 | -| cpuLimit | string | CPU上限 | -| cpuUtilization | float | CPU利用率百分比 | -| memoryUsage | string | 内存实际使用量 | -| memoryLimit | string | 内存上限 | -| memoryUtilization | float | 内存利用率百分比 | -| status | string | 运行状态 | -| source | string | 数据来源(k8s/database) | - ---- - -#### D7. 系统监控指标接口 - -**展示位置**: 概览页面 → 系统指标卡片 - -**展示内容**: 显示CPU使用率、内存使用率、存储使用率、活跃Agent数等系统级指标 - -**功能描述**: 获取平台整体的系统监控指标 - -**接口需求**: -``` -GET /api/v1/monitoring/metrics -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.cpu_usage | float | CPU使用率百分比 | -| data.memory_usage | float | 内存使用率百分比 | -| data.disk_usage | float | 存储使用率百分比 | -| data.active_agents | int | 活跃Agent数量 | - ---- - -## 计费模块 (Billing) - -### 数据展示接口 - -#### D8. 计费概览统计接口 - -**展示位置**: 计费管理页面 → 统计卡片区域 - -**展示内容**: 显示渠道总数、总计费额、总EU消耗等汇总统计 - -**功能描述**: 获取计费数据的汇总统计信息 - -**接口需求**: -``` -GET /api/admin/billing/overview -``` - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| startTime | string | 是 | 开始时间(ISO 8601格式) | -| endTime | string | 是 | 结束时间(ISO 8601格式) | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.summary.totalChannels | int | 渠道总数 | -| data.summary.totalBilling | float | 总计费额 | -| data.summary.totalEU | int | 总EU消耗 | - ---- - -#### D9. 渠道维度计费详情接口 - -**展示位置**: 计费管理页面 → 渠道维度 → 渠道计费详情表格 - -**展示内容**: 显示每个渠道的调用次数、总EU、渠道总价等 - -**功能描述**: 获取按渠道维度分组的计费详情 - -**接口需求**: -``` -GET /api/admin/billing/overview -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.channelStats | array | 渠道统计列表 | -| data.channelStats[].channelId | string | 渠道ID | -| data.channelStats[].channelName | string | 渠道名称 | -| data.channelStats[].calls | int | 调用次数 | -| data.channelStats[].totalEU | int | 总EU | -| data.channelStats[].totalCost | float | 渠道总价 | - ---- - -#### D10. 租户维度计费详情接口 - -**展示位置**: 计费管理页面 → 租户维度 → 租户计费详情表格 - -**展示内容**: 显示每个租户的所属渠道、调用次数、总EU、用户总价等 - -**功能描述**: 获取按租户维度分组的计费详情 - -**接口需求**: -``` -GET /api/admin/billing/overview -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.tenantStats | array | 租户统计列表 | -| data.tenantStats[].tenantId | string | 租户ID | -| data.tenantStats[].tenantName | string | 租户名称 | -| data.tenantStats[].channelName | string | 渠道名称 | -| data.tenantStats[].calls | int | 调用次数 | -| data.tenantStats[].totalEU | int | 总EU | -| data.tenantStats[].totalCost | float | 用户总价 | - ---- - -#### D11. 调用记录明细接口 - -**展示位置**: 计费管理页面 → 调用记录 → 调用记录明细表格 - -**展示内容**: 显示每次调用的ID、租户、渠道、调用时间、时长、EU、单次调用总价等 - -**功能描述**: 获取详细的调用记录列表 - -**接口需求**: -``` -GET /api/admin/billing/call-records -``` - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| startTime | string | 是 | 开始时间 | -| endTime | string | 是 | 结束时间 | -| page | int | 否 | 页码,默认1 | -| pageSize | int | 否 | 每页数量,默认20 | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.records | array | 调用记录列表 | -| data.records[].id | string | 调用ID | -| data.records[].tenantName | string | 租户名称 | -| data.records[].channelName | string | 渠道名称 | -| data.records[].callTime | string | 调用时间 | -| data.records[].duration | int | 时长(秒) | -| data.records[].eu | float | EU消耗 | -| data.records[].cost | float | 单次调用总价 | -| data.pagination.total | int | 总记录数 | -| data.pagination.page | int | 当前页 | - ---- - -### 按钮操作接口 - -#### 19. 时间查询按钮 - -**按钮位置**: 计费管理页面 → "时间查询"按钮 - -**按钮作用**: 按时间范围筛选计费数据 - -**功能描述**: 点击后弹出对话框,选择开始时间和结束时间,查询该时间段内的计费数据 - -**接口需求**: -``` -GET /api/admin/billing/overview -``` - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| startTime | string | 是 | 开始时间(ISO 8601格式) | -| endTime | string | 是 | 结束时间(ISO 8601格式) | - ---- - -#### 20. 筛选按钮 - -**按钮位置**: 计费管理页面 → "筛选"按钮 - -**按钮作用**: 按条件筛选计费数据 - -**功能描述**: 点击后弹出对话框,可按客户名称、最小/最大调用次数等条件筛选 - -**接口需求**: -``` -GET /api/admin/billing/overview -``` - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| startTime | string | 是 | 开始时间 | -| endTime | string | 是 | 结束时间 | -| channelName | string | 否 | 渠道名称筛选 | -| tenantName | string | 否 | 租户名称筛选 | -| minCalls | int | 否 | 最小调用次数 | -| maxCalls | int | 否 | 最大调用次数 | - ---- - -#### 21. 导出按钮 - -**按钮位置**: 计费管理页面 → "导出"按钮 - -**按钮作用**: 导出计费数据为文件 - -**功能描述**: 点击后将当前筛选条件下的计费数据导出为Excel/CSV/PDF格式文件 - -**接口需求**: -``` -GET /api/admin/billing/export -``` - -**查询参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| startTime | string | 是 | 开始时间 | -| endTime | string | 是 | 结束时间 | -| format | string | 是 | 导出格式(excel/csv/pdf) | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.fileUrl | string | 导出文件下载URL | -| message | string | 操作结果消息 | - ---- - -## 设置模块 (Settings) - -### 数据展示接口 - -#### D12. 管理员列表接口 - -**展示位置**: 设置页面 → 当前管理员列表 - -**展示内容**: 显示所有系统管理员的姓名、邮箱、角色、状态等 - -**功能描述**: 获取系统管理员列表 - -**接口需求**: -``` -GET /api/admin/admins -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.admins | array | 管理员列表 | -| data.admins[].id | string | 管理员ID | -| data.admins[].name | string | 管理员姓名 | -| data.admins[].email | string | 邮箱 | -| data.admins[].role | string | 角色 | -| data.admins[].status | string | 状态 | - ---- - -### 按钮操作接口 - -#### 22. 添加管理员按钮 - -**按钮位置**: 设置页面 → 当前管理员列表 → "添加管理员"按钮 - -**按钮作用**: 创建新的系统管理员账户 - -**功能描述**: 点击后弹出对话框,填写管理员姓名、邮箱、密码、角色,创建新管理员 - -**接口需求**: -``` -POST /api/admin/admins/create -``` - -**请求体**: -```json -{ - "name": "管理员姓名", - "email": "admin@example.com", - "password": "SecurePass123", - "role": "billing_admin" -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.id | string | 管理员ID | -| message | string | 操作结果消息 | - ---- - -#### 23. 删除管理员按钮 - -**按钮位置**: 设置页面 → 当前管理员列表 → 管理员行 → 删除图标按钮 - -**按钮作用**: 删除系统管理员账户(软删除) - -**功能描述**: 点击后弹出确认对话框,确认后将管理员状态设为inactive - -**接口需求**: -``` -DELETE /api/admin/admins/{admin_id} -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| admin_id | string | 是 | 管理员ID | - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| message | string | 操作结果消息 | - ---- - -#### 24-26. 角色权限配置按钮 - -**按钮位置**: 设置页面 → 角色权限配置区域 → "保存权限配置"按钮 - -**按钮作用**: 保存角色的标签页访问权限配置 - -**功能描述**: 选择角色后,勾选该角色可访问的标签页,点击保存更新权限配置 - -**接口需求**: -``` -PUT /api/admin/roles/{role_id}/permissions -``` - -**路径参数**: -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| role_id | string | 是 | 角色ID(billing-admin/operations-admin/super-admin) | - -**请求体**: -```json -{ - "permissions": ["overview", "channels", "resources", "monitoring", "billing", "settings"] -} -``` - -**响应字段**: -| 字段 | 类型 | 说明 | -|------|------|------| -| success | bool | 是否成功 | -| data.roleId | string | 角色ID | -| data.permissions | array | 更新后的权限列表 | -| message | string | 操作结果消息 | - ---- - -## 附录:接口汇总表 - -### 数据展示接口汇总 - -| 序号 | 接口 | 方法 | 展示内容 | 模块 | -|------|------|------|----------|------| -| D1 | /api/admin/platform/resource-allocation | GET | 平台资源分配统计 | 概览 | -| D2 | /api/admin/channels/stats | GET | 渠道统计概览 | 渠道管理 | -| D3 | /api/admin/platform-agents/templates | GET | Agent模板列表 | 资源管理 | -| D4 | /api/providers/models | GET | 模型供应商列表 | 资源管理 | -| D5 | /api/admin/platform-agents/status | GET | Agent健康监控汇总 | 监控 | -| D6 | /api/admin/platform-agents/status | GET | Agent详细指标 | 监控 | -| D7 | /api/v1/monitoring/metrics | GET | 系统监控指标 | 概览 | -| D8 | /api/admin/billing/overview | GET | 计费概览统计 | 计费 | -| D9 | /api/admin/billing/overview | GET | 渠道维度计费详情 | 计费 | -| D10 | /api/admin/billing/overview | GET | 租户维度计费详情 | 计费 | -| D11 | /api/admin/billing/call-records | GET | 调用记录明细 | 计费 | -| D12 | /api/admin/admins | GET | 管理员列表 | 设置 | - -### 按钮操作接口汇总 - -| 序号 | 接口 | 方法 | 按钮/功能 | 模块 | -|------|------|------|----------|------| -| 1 | /api/admin/dashboard/recent-logins/search | GET | 搜索租户 | 概览 | -| 2 | /api/admin/channels/search | GET | 搜索渠道 | 渠道管理 | -| 3 | /api/admin/channels/{channel_id} | GET | 查看渠道详情 | 渠道管理 | -| 4 | /api/admin/channels/{channel_id} | DELETE | 删除渠道 | 渠道管理 | -| 5 | /api/channel/tenants/{tenant_id} | DELETE | 删除租户 | 渠道管理 | -| 6 | /api/channel/tenants/{tenant_id}/status | PUT | 禁用租户 | 渠道管理 | -| 7 | /api/channel/tenants/{tenant_id}/password | PUT | 修改租户密码 | 渠道管理 | -| 8 | /api/channel/tenants/{tenant_id}/permissions | PUT | 管理租户权限 | 渠道管理 | -| 9 | /api/admin/providers/applications/{id}/review | PUT | 供应商申请审批-拒绝 | 渠道管理 | -| 10 | /api/admin/providers/applications/{id}/review | PUT | 供应商申请审批-批准 | 渠道管理 | -| 11 | /api/admin/applications/platform-agents/{id}/review | PUT | 平台Agent申请审批-拒绝 | 渠道管理 | -| 12 | /api/admin/applications/platform-agents/{id}/review | PUT | 平台Agent申请审批-批准 | 渠道管理 | -| 13 | /api/admin/platform-agents/templates/{name}/config | PUT | Agent模板配置-保存 | 资源管理 | -| 14 | /api/admin/platform-agents/templates/{name} | DELETE | Agent模板删除 | 资源管理 | -| 15 | /api/providers/models/create | POST | 添加模型供应商 | 资源管理 | -| 16 | /api/providers/models/{provider_id} | PUT | 模型供应商配置 | 资源管理 | -| 17 | /api/providers/models/{provider_id}/test | POST | 模型供应商测试延迟 | 资源管理 | -| 18 | /api/providers/models/{provider_id} | DELETE | 模型供应商删除 | 资源管理 | -| 19 | /api/admin/billing/overview | GET | 时间查询 | 计费 | -| 20 | /api/admin/billing/overview | GET | 筛选 | 计费 | -| 21 | /api/admin/billing/export | GET | 导出 | 计费 | -| 22 | /api/admin/admins/create | POST | 添加管理员 | 设置 | -| 23 | /api/admin/admins/{admin_id} | DELETE | 删除管理员 | 设置 | -| 24-26 | /api/admin/roles/{role_id}/permissions | PUT | 保存权限配置 | 设置 | -| 27 | /api/admin/channels/{channel_id}/resources | PUT | 保存资源配置 | 渠道管理 | -| 28 | /api/admin/channels/{channel_id} | PUT | 保存渠道编辑 | 渠道管理 | -| 29 | /api/admin/channels/{channel_id}/commission | PUT | 保存佣金修改 | 渠道管理 | -| 30 | /api/admin/channels/create | POST | 创建渠道 | 渠道管理 | -| 31 | /api/channel/tenants/create | POST | 添加租户 | 渠道管理 | -| 32 | /api/admin/channels/{channel_id}/admins/{admin_id} | DELETE | 删除渠道管理员 | 渠道管理 | - ---- - -## 更新日志 - -### v1.1.0 (2026-01-06) - -- 新增数据展示接口(D1-D12) -- 补充监控模块的Agent详细指标接口 -- 补充计费模块的调用记录明细接口 -- 完善接口汇总表 \ No newline at end of file diff --git a/services/mcp-server/app/litellm_client.py b/services/mcp-server/app/litellm_client.py new file mode 100644 index 0000000..2341a52 --- /dev/null +++ b/services/mcp-server/app/litellm_client.py @@ -0,0 +1,525 @@ +""" +LiteLLM Admin API 客户端 + +用于与 LiteLLM Gateway 进行交互,管理 team 和 key。 + +职责划分: +- mcp-server: 业务规则制定者(决定谁能用什么模型、配额多少) +- litellm-gateway: 规则执行者(真正拦截超额请求) + +概念映射: +- 渠道 (Channel) = LiteLLM team +- 租户 (Tenant) = LiteLLM key(归属于 team) +""" + +import logging +from typing import Optional, Dict, Any, List +from dataclasses import dataclass +import httpx +from cryptography.fernet import Fernet +import base64 +import hashlib + +from config import settings + +logger = logging.getLogger(__name__) + + +@dataclass +class LiteLLMTeam: + """LiteLLM Team 信息""" + team_id: str + team_alias: str + metadata: Dict[str, Any] + + +@dataclass +class LiteLLMKey: + """LiteLLM Key 信息""" + key: str # 完整的 API key + key_name: Optional[str] = None + team_id: Optional[str] = None + models: Optional[List[str]] = None + rpm_limit: Optional[int] = None + tpm_limit: Optional[int] = None + max_budget: Optional[float] = None + budget_duration: Optional[str] = None + metadata: Optional[Dict[str, Any]] = None + + +class LiteLLMClientError(Exception): + """LiteLLM 客户端错误""" + def __init__(self, message: str, status_code: Optional[int] = None, response: Optional[Dict] = None): + super().__init__(message) + self.status_code = status_code + self.response = response + + +class LiteLLMClient: + """LiteLLM Admin API 客户端 + + 用于管理 LiteLLM 的 team 和 key。 + + 使用示例: + ```python + client = LiteLLMClient() + + # 创建渠道时,同步创建 LiteLLM team + team = await client.create_team( + team_alias=f"channel-{channel_id}", + metadata={"channel_id": str(channel_id), "channel_name": channel_name} + ) + + # 分配模型给租户时,创建 LiteLLM key + key = await client.generate_key( + team_id=team.team_id, + models=["azure/gpt-4"], + rpm_limit=60, + tpm_limit=10000, + max_budget=100.0, + metadata={"tenant_id": str(tenant_id)} + ) + + # 充值时,更新 key 的 budget + await client.update_key( + key=key.key, + max_budget=200.0 + ) + ``` + """ + + def __init__( + self, + base_url: Optional[str] = None, + master_key: Optional[str] = None, + timeout: float = 30.0 + ): + """初始化 LiteLLM 客户端 + + Args: + base_url: LiteLLM Gateway URL,默认从配置读取 + master_key: LiteLLM Master Key,默认从配置读取 + timeout: 请求超时时间(秒) + """ + self.base_url = (base_url or settings.litellm_url).rstrip("/") + self.master_key = master_key or settings.litellm_api_key + self.timeout = timeout + + # 初始化加密器(用于加密存储 key) + self._init_encryption() + + def _init_encryption(self): + """初始化加密器""" + # 使用配置的加密密钥生成 Fernet key + key_bytes = settings.encryption_key.encode() + # 使用 SHA256 生成 32 字节的 key,然后 base64 编码 + key_hash = hashlib.sha256(key_bytes).digest() + fernet_key = base64.urlsafe_b64encode(key_hash) + self._fernet = Fernet(fernet_key) + + def encrypt_key(self, key: str) -> str: + """加密 API Key + + Args: + key: 原始 API key + + Returns: + 加密后的 key(base64 编码) + """ + return self._fernet.encrypt(key.encode()).decode() + + def decrypt_key(self, encrypted_key: str) -> str: + """解密 API Key + + Args: + encrypted_key: 加密的 key + + Returns: + 原始 API key + """ + return self._fernet.decrypt(encrypted_key.encode()).decode() + + async def _request( + self, + method: str, + endpoint: str, + json: Optional[Dict] = None, + params: Optional[Dict] = None + ) -> Dict[str, Any]: + """发送 HTTP 请求到 LiteLLM + + Args: + method: HTTP 方法 + endpoint: API 端点(不含 base_url) + json: 请求体 + params: 查询参数 + + Returns: + 响应 JSON + + Raises: + LiteLLMClientError: 请求失败时抛出 + """ + url = f"{self.base_url}{endpoint}" + headers = { + "Authorization": f"Bearer {self.master_key}", + "Content-Type": "application/json" + } + + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.request( + method=method, + url=url, + json=json, + params=params, + headers=headers + ) + + if response.status_code >= 400: + error_data = None + try: + error_data = response.json() + except Exception: + pass + + logger.error( + f"LiteLLM API 错误: {method} {endpoint} -> {response.status_code}", + extra={"response": error_data} + ) + raise LiteLLMClientError( + message=f"LiteLLM API 错误: {response.status_code}", + status_code=response.status_code, + response=error_data + ) + + return response.json() + + except httpx.TimeoutException as e: + logger.error(f"LiteLLM 请求超时: {method} {endpoint}") + raise LiteLLMClientError(f"请求超时: {str(e)}") + except httpx.RequestError as e: + logger.error(f"LiteLLM 请求错误: {method} {endpoint} -> {str(e)}") + raise LiteLLMClientError(f"请求错误: {str(e)}") + + # ==================== Team 管理 ==================== + + async def create_team( + self, + team_alias: str, + metadata: Optional[Dict[str, Any]] = None, + models: Optional[List[str]] = None, + max_budget: Optional[float] = None + ) -> LiteLLMTeam: + """创建 LiteLLM Team + + 创建渠道时调用此方法,同步创建 LiteLLM team。 + + Args: + team_alias: Team 别名(建议使用 channel-{channel_id}) + metadata: 元数据(如 channel_id, channel_name) + models: 允许的模型列表(可选,通常在 key 级别限制) + max_budget: 最大预算(可选) + + Returns: + LiteLLMTeam 对象 + """ + payload = { + "team_alias": team_alias, + } + + if metadata: + payload["metadata"] = metadata + if models: + payload["models"] = models + if max_budget is not None: + payload["max_budget"] = max_budget + + logger.info(f"创建 LiteLLM Team: {team_alias}") + + data = await self._request("POST", "/team/new", json=payload) + + return LiteLLMTeam( + team_id=data.get("team_id"), + team_alias=team_alias, + metadata=metadata or {} + ) + + async def get_team(self, team_id: str) -> Dict[str, Any]: + """获取 Team 信息 + + Args: + team_id: Team ID + + Returns: + Team 信息 + """ + return await self._request("GET", f"/team/info", params={"team_id": team_id}) + + async def update_team( + self, + team_id: str, + team_alias: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + models: Optional[List[str]] = None, + max_budget: Optional[float] = None + ) -> Dict[str, Any]: + """更新 Team 信息 + + Args: + team_id: Team ID + team_alias: 新的别名 + metadata: 新的元数据 + models: 新的模型列表 + max_budget: 新的最大预算 + + Returns: + 更新后的 Team 信息 + """ + payload = {"team_id": team_id} + + if team_alias: + payload["team_alias"] = team_alias + if metadata: + payload["metadata"] = metadata + if models: + payload["models"] = models + if max_budget is not None: + payload["max_budget"] = max_budget + + logger.info(f"更新 LiteLLM Team: {team_id}") + + return await self._request("POST", "/team/update", json=payload) + + async def delete_team(self, team_id: str) -> Dict[str, Any]: + """删除 Team + + 删除渠道时调用此方法,同步删除 LiteLLM team。 + + Args: + team_id: Team ID + + Returns: + 删除结果 + """ + logger.info(f"删除 LiteLLM Team: {team_id}") + + return await self._request("POST", "/team/delete", json={"team_ids": [team_id]}) + + # ==================== Key 管理 ==================== + + async def generate_key( + self, + team_id: str, + models: List[str], + rpm_limit: Optional[int] = None, + tpm_limit: Optional[int] = None, + max_budget: Optional[float] = None, + budget_duration: Optional[str] = "monthly", + key_name: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None + ) -> LiteLLMKey: + """生成 LiteLLM API Key + + 分配模型给租户时调用此方法,创建绑定 team + model + 配额的 key。 + + Args: + team_id: Team ID(渠道的 litellm_team_id) + models: 允许的模型列表(如 ["azure/gpt-4"]) + rpm_limit: 每分钟请求数限制 + tpm_limit: 每分钟 Token 数限制 + max_budget: 最大预算 + budget_duration: 预算周期(monthly, total) + key_name: Key 名称 + metadata: 元数据(如 tenant_id, channel_id, model) + + Returns: + LiteLLMKey 对象 + """ + payload = { + "team_id": team_id, + "models": models, + } + + if rpm_limit is not None: + payload["rpm_limit"] = rpm_limit + if tpm_limit is not None: + payload["tpm_limit"] = tpm_limit + if max_budget is not None: + payload["max_budget"] = max_budget + if budget_duration: + payload["budget_duration"] = budget_duration + if key_name: + payload["key_name"] = key_name + if metadata: + payload["metadata"] = metadata + + logger.info(f"生成 LiteLLM Key: team={team_id}, models={models}") + + data = await self._request("POST", "/key/generate", json=payload) + + return LiteLLMKey( + key=data.get("key"), + key_name=key_name, + team_id=team_id, + models=models, + rpm_limit=rpm_limit, + tpm_limit=tpm_limit, + max_budget=max_budget, + budget_duration=budget_duration, + metadata=metadata + ) + + async def get_key_info(self, key: str) -> Dict[str, Any]: + """获取 Key 信息 + + Args: + key: API Key + + Returns: + Key 信息 + """ + return await self._request("GET", "/key/info", params={"key": key}) + + async def update_key( + self, + key: str, + models: Optional[List[str]] = None, + rpm_limit: Optional[int] = None, + tpm_limit: Optional[int] = None, + max_budget: Optional[float] = None, + budget_duration: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """更新 Key 配置 + + 修改配额或充值时调用此方法。 + + Args: + key: API Key + models: 新的模型列表 + rpm_limit: 新的 RPM 限制 + tpm_limit: 新的 TPM 限制 + max_budget: 新的最大预算 + budget_duration: 新的预算周期 + metadata: 新的元数据 + + Returns: + 更新结果 + """ + payload = {"key": key} + + if models is not None: + payload["models"] = models + if rpm_limit is not None: + payload["rpm_limit"] = rpm_limit + if tpm_limit is not None: + payload["tpm_limit"] = tpm_limit + if max_budget is not None: + payload["max_budget"] = max_budget + if budget_duration is not None: + payload["budget_duration"] = budget_duration + if metadata is not None: + payload["metadata"] = metadata + + logger.info(f"更新 LiteLLM Key: {key[:20]}...") + + return await self._request("POST", "/key/update", json=payload) + + async def delete_key(self, key: str) -> Dict[str, Any]: + """删除 Key + + 取消模型分配时调用此方法。 + + Args: + key: API Key + + Returns: + 删除结果 + """ + logger.info(f"删除 LiteLLM Key: {key[:20]}...") + + return await self._request("POST", "/key/delete", json={"keys": [key]}) + + # ==================== 用量查询 ==================== + + async def get_spend_logs( + self, + api_key: Optional[str] = None, + team_id: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None + ) -> Dict[str, Any]: + """查询用量日志 + + Args: + api_key: 按 API Key 过滤 + team_id: 按 Team ID 过滤 + start_date: 开始日期(YYYY-MM-DD) + end_date: 结束日期(YYYY-MM-DD) + + Returns: + 用量日志 + """ + params = {} + + if api_key: + params["api_key"] = api_key + if team_id: + params["team_id"] = team_id + if start_date: + params["start_date"] = start_date + if end_date: + params["end_date"] = end_date + + return await self._request("GET", "/spend/logs", params=params) + + async def get_key_spend(self, key: str) -> Dict[str, Any]: + """获取 Key 的消费统计 + + Args: + key: API Key + + Returns: + 消费统计 + """ + return await self._request("GET", "/key/info", params={"key": key}) + + # ==================== 健康检查 ==================== + + async def health_check(self) -> bool: + """检查 LiteLLM Gateway 健康状态 + + Returns: + 是否健康 + """ + try: + await self._request("GET", "/health") + return True + except LiteLLMClientError: + return False + + async def get_models(self) -> List[Dict[str, Any]]: + """获取可用模型列表 + + Returns: + 模型列表 + """ + data = await self._request("GET", "/model/info") + return data.get("data", []) + + +# 全局客户端实例 +_litellm_client: Optional[LiteLLMClient] = None + + +def get_litellm_client() -> LiteLLMClient: + """获取 LiteLLM 客户端单例 + + Returns: + LiteLLMClient 实例 + """ + global _litellm_client + if _litellm_client is None: + _litellm_client = LiteLLMClient() + return _litellm_client diff --git a/services/mcp-server/app/routes/admin.py b/services/mcp-server/app/routes/admin.py index 89da535..0286a46 100644 --- a/services/mcp-server/app/routes/admin.py +++ b/services/mcp-server/app/routes/admin.py @@ -792,6 +792,8 @@ async def create_channel( ): """ 创建渠道(super_admin 和 billing_admin 可用) + + 同时在 LiteLLM 中创建对应的 team,用于管理该渠道下租户的模型访问权限。 """ _verify_write_permission(principal) @@ -822,12 +824,49 @@ async def create_channel( await db.commit() await db.refresh(channel) + # 在 LiteLLM 中创建对应的 team + litellm_team_id = None + litellm_error = None + try: + from app.litellm_client import get_litellm_client, LiteLLMClientError + litellm_client = get_litellm_client() + + team = await litellm_client.create_team( + team_alias=f"channel-{channel.id}", + metadata={ + "channel_id": str(channel.id), + "channel_name": channel.name, + "channel_email": channel.email, + } + ) + + # 保存 LiteLLM team_id 到渠道记录 + channel.litellm_team_id = team.team_id + await db.commit() + litellm_team_id = team.team_id + + logger.info(f"渠道 {channel.name} 创建成功,LiteLLM team_id: {team.team_id}") + + except LiteLLMClientError as e: + # LiteLLM 创建失败,记录错误但不影响渠道创建 + litellm_error = str(e) + logger.warning(f"创建渠道 {channel.name} 时 LiteLLM team 创建失败: {e}") + except Exception as e: + litellm_error = str(e) + logger.warning(f"创建渠道 {channel.name} 时 LiteLLM 连接失败: {e}") + + response_data = { + "id": str(channel.id), + "name": channel.name, + "email": channel.email, + "litellmTeamId": litellm_team_id, + } + + if litellm_error: + response_data["litellmWarning"] = f"LiteLLM team 创建失败: {litellm_error}" + return SuccessResponse( - data={ - "id": str(channel.id), - "name": channel.name, - "email": channel.email, - }, + data=response_data, message="渠道创建成功" ) @@ -904,6 +943,8 @@ async def delete_channel( ): """ 删除渠道(软删除,super_admin 和 billing_admin 可用) + + 同时删除 LiteLLM 中对应的 team。 """ _verify_write_permission(principal) @@ -937,12 +978,33 @@ async def delete_channel( detail=f"渠道下有 {tenant_count} 个活跃租户,无法删除。请先移除或停用所有租户。" ) + # 删除 LiteLLM 中对应的 team + litellm_error = None + if channel.litellm_team_id: + try: + from app.litellm_client import get_litellm_client, LiteLLMClientError + litellm_client = get_litellm_client() + + await litellm_client.delete_team(channel.litellm_team_id) + logger.info(f"渠道 {channel.name} 的 LiteLLM team {channel.litellm_team_id} 已删除") + + except LiteLLMClientError as e: + litellm_error = str(e) + logger.warning(f"删除渠道 {channel.name} 时 LiteLLM team 删除失败: {e}") + except Exception as e: + litellm_error = str(e) + logger.warning(f"删除渠道 {channel.name} 时 LiteLLM 连接失败: {e}") + # 软删除:标记为不活跃 channel.status = "inactive" await db.commit() + response_data = {"id": str(channel.id)} + if litellm_error: + response_data["litellmWarning"] = f"LiteLLM team 删除失败: {litellm_error}" + return SuccessResponse( - data={"id": str(channel.id)}, + data=response_data, message="渠道已删除" ) diff --git a/services/mcp-server/app/routes/channel.py b/services/mcp-server/app/routes/channel.py index 3b5d92c..702fcff 100644 --- a/services/mcp-server/app/routes/channel.py +++ b/services/mcp-server/app/routes/channel.py @@ -16,7 +16,7 @@ from models import ( BillingRecord, RechargeRecord, Application, ModelProvider, ChannelProviderAccess, ProviderApplication, TenantCustomAgentQuota, ChannelCustomAgentQuota, ResourceApplication, PlatformAgentQuota, - AgentBillingRecord, PlatformAgentTemplateConfig + AgentBillingRecord, PlatformAgentTemplateConfig, TenantModelKey ) from app.auth import require_auth, get_password_hash from app.permissions import has_permission @@ -1121,6 +1121,594 @@ async def get_tenant_custom_agent_quota( ) +# ============= 租户模型分配(LiteLLM 集成)============= + +@router.put("/tenants/{tenant_id}/models", response_model=SuccessResponse) +async def allocate_model_to_tenant( + tenant_id: str, + model_name: str = Query(..., description="模型名称,如 azure/gpt-4"), + rpm_limit: int = Query(60, ge=0, description="每分钟请求数限制"), + tpm_limit: int = Query(10000, ge=0, description="每分钟 Token 数限制"), + max_budget: float = Query(100.0, ge=0, description="最大预算"), + budget_duration: str = Query("monthly", pattern="^(monthly|total)$", description="预算周期"), + channel_id_param: Optional[str] = Query(None, alias="channel_id", description="渠道ID(超级管理员必填)"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 分配模型给租户(创建 LiteLLM Key) + + 在 LiteLLM 中为租户创建 API Key,绑定指定的模型和配额。 + 租户的 Agent 启动时会使用此 Key 访问模型。 + + 权限:manage:resources (channel_admin, billing_admin, super_admin) + + 注意: + - 渠道必须先拥有该模型的权限(通过 ResourceAllocation 分配) + - 每个租户每个模型只能有一个 Key + - 超级管理员必须提供 channel_id 参数 + """ + _verify_permission(principal, "manage:resources") + role = _get_role(principal) + user_channel_id = _get_channel_id(principal) + + # 确定目标渠道ID + if role == "super_admin": + if not channel_id_param: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="超级管理员必须提供 channel_id 参数" + ) + try: + channel_id = uuid.UUID(channel_id_param) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无效的渠道ID格式" + ) + elif user_channel_id: + channel_id = user_channel_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无法获取渠道ID" + ) + + # 验证租户存在且属于指定渠道 + tenant_result = await db.execute( + select(User).where( + and_( + User.id == tenant_id, + User.channel_id == channel_id + ) + ) + ) + tenant = tenant_result.scalar_one_or_none() + + if not tenant: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="租户不存在或不属于该渠道" + ) + + # 获取渠道信息 + channel_result = await db.execute( + select(Channel).where(Channel.id == channel_id) + ) + channel = channel_result.scalar_one_or_none() + + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="渠道不存在" + ) + + # 验证渠道是否有该模型的权限 + model_allocation_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 + ) + ) + ) + model_allocation = model_allocation_result.scalar_one_or_none() + + if not model_allocation: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"渠道没有模型 '{model_name}' 的权限" + ) + + # 检查租户是否已有该模型的 Key + existing_key_result = await db.execute( + select(TenantModelKey).where( + and_( + TenantModelKey.tenant_id == tenant_id, + TenantModelKey.model_name == model_name + ) + ) + ) + existing_key = existing_key_result.scalar_one_or_none() + + if existing_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"租户已有模型 '{model_name}' 的 Key,请使用更新配额接口" + ) + + # 检查渠道是否有 LiteLLM team_id + if not channel.litellm_team_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="渠道尚未关联 LiteLLM team,请联系管理员" + ) + + # 在 LiteLLM 中创建 Key + try: + from app.litellm_client import get_litellm_client, LiteLLMClientError + litellm_client = get_litellm_client() + + key = await litellm_client.generate_key( + team_id=channel.litellm_team_id, + models=[model_name], + rpm_limit=rpm_limit, + tpm_limit=tpm_limit, + max_budget=max_budget, + budget_duration=budget_duration, + key_name=f"tenant-{tenant_id}-{model_name}", + metadata={ + "tenant_id": str(tenant_id), + "tenant_name": tenant.name, + "channel_id": str(channel_id), + "channel_name": channel.name, + "model": model_name, + } + ) + + # 加密存储 Key + encrypted_key = litellm_client.encrypt_key(key.key) + + # 保存到数据库 + tenant_key = TenantModelKey( + tenant_id=tenant_id, + channel_id=channel_id, + model_name=model_name, + litellm_key_id=key.key, + litellm_key_hash=encrypted_key, + rpm_limit=rpm_limit, + tpm_limit=tpm_limit, + max_budget=max_budget, + budget_duration=budget_duration, + status="active", + ) + db.add(tenant_key) + + # 同时记录到 ResourceAllocation + tenant_allocation = ResourceAllocation( + target_id=tenant_id, + target_type="tenant", + resource_type="model", + resource_id=model_name, + rpm=rpm_limit, + tpm=tpm_limit, + ) + db.add(tenant_allocation) + + await db.commit() + + logger.info( + f"为租户 {tenant.name} 分配模型 {model_name} 成功", + extra={"tenant_id": tenant_id, "model": model_name} + ) + + return SuccessResponse( + data={ + "tenantId": str(tenant_id), + "tenantName": tenant.name, + "modelName": model_name, + "rpmLimit": rpm_limit, + "tpmLimit": tpm_limit, + "maxBudget": max_budget, + "budgetDuration": budget_duration, + "status": "active", + }, + message=f"模型 '{model_name}' 分配成功" + ) + + except LiteLLMClientError as e: + logger.error(f"LiteLLM Key 创建失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"LiteLLM Key 创建失败: {str(e)}" + ) + except Exception as e: + logger.error(f"模型分配失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"模型分配失败: {str(e)}" + ) + + +@router.delete("/tenants/{tenant_id}/models/{model_name}", response_model=SuccessResponse) +async def revoke_model_from_tenant( + tenant_id: str, + model_name: str, + channel_id_param: Optional[str] = Query(None, alias="channel_id", description="渠道ID(超级管理员必填)"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 取消租户的模型分配(删除 LiteLLM Key) + + 删除租户在 LiteLLM 中的 API Key,租户将无法再使用该模型。 + + 权限:manage:resources (channel_admin, billing_admin, super_admin) + + 注意: + - 超级管理员必须提供 channel_id 参数 + - 删除后租户正在运行的 Agent 将无法继续使用该模型 + """ + _verify_permission(principal, "manage:resources") + role = _get_role(principal) + user_channel_id = _get_channel_id(principal) + + # 确定目标渠道ID + if role == "super_admin": + if not channel_id_param: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="超级管理员必须提供 channel_id 参数" + ) + try: + channel_id = uuid.UUID(channel_id_param) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无效的渠道ID格式" + ) + elif user_channel_id: + channel_id = user_channel_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无法获取渠道ID" + ) + + # 验证租户存在且属于指定渠道 + tenant_result = await db.execute( + select(User).where( + and_( + User.id == tenant_id, + User.channel_id == channel_id + ) + ) + ) + tenant = tenant_result.scalar_one_or_none() + + if not tenant: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="租户不存在或不属于该渠道" + ) + + # 查找租户的模型 Key + key_result = await db.execute( + select(TenantModelKey).where( + and_( + TenantModelKey.tenant_id == tenant_id, + TenantModelKey.model_name == model_name + ) + ) + ) + tenant_key = key_result.scalar_one_or_none() + + if not tenant_key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"租户没有模型 '{model_name}' 的分配记录" + ) + + # 在 LiteLLM 中删除 Key + litellm_error = None + try: + from app.litellm_client import get_litellm_client, LiteLLMClientError + litellm_client = get_litellm_client() + + await litellm_client.delete_key(tenant_key.litellm_key_id) + logger.info(f"LiteLLM Key 删除成功: {tenant_key.litellm_key_id[:20]}...") + + except LiteLLMClientError as e: + litellm_error = str(e) + logger.warning(f"LiteLLM Key 删除失败: {e}") + except Exception as e: + litellm_error = str(e) + logger.warning(f"LiteLLM 连接失败: {e}") + + # 删除数据库记录 + await db.delete(tenant_key) + + # 删除 ResourceAllocation 记录 + allocation_result = await db.execute( + select(ResourceAllocation).where( + and_( + ResourceAllocation.target_id == tenant_id, + ResourceAllocation.target_type == "tenant", + ResourceAllocation.resource_type == "model", + ResourceAllocation.resource_id == model_name + ) + ) + ) + allocation = allocation_result.scalar_one_or_none() + if allocation: + await db.delete(allocation) + + await db.commit() + + response_data = { + "tenantId": str(tenant_id), + "tenantName": tenant.name, + "modelName": model_name, + } + + if litellm_error: + response_data["litellmWarning"] = f"LiteLLM Key 删除失败: {litellm_error}" + + return SuccessResponse( + data=response_data, + message=f"模型 '{model_name}' 分配已取消" + ) + + +@router.put("/tenants/{tenant_id}/models/{model_name}/quota", response_model=SuccessResponse) +async def update_tenant_model_quota( + tenant_id: str, + model_name: str, + rpm_limit: Optional[int] = Query(None, ge=0, description="每分钟请求数限制"), + tpm_limit: Optional[int] = Query(None, ge=0, description="每分钟 Token 数限制"), + max_budget: Optional[float] = Query(None, ge=0, description="最大预算"), + budget_duration: Optional[str] = Query(None, pattern="^(monthly|total)$", description="预算周期"), + channel_id_param: Optional[str] = Query(None, alias="channel_id", description="渠道ID(超级管理员必填)"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 更新租户的模型配额(更新 LiteLLM Key) + + 更新租户在 LiteLLM 中的 API Key 配额,包括 RPM、TPM 和预算限制。 + 更新后立即生效,无需重启任何服务。 + + 权限:manage:resources (channel_admin, billing_admin, super_admin) + + 注意: + - 超级管理员必须提供 channel_id 参数 + - 只更新提供的参数,未提供的参数保持不变 + """ + _verify_permission(principal, "manage:resources") + role = _get_role(principal) + user_channel_id = _get_channel_id(principal) + + # 确定目标渠道ID + if role == "super_admin": + if not channel_id_param: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="超级管理员必须提供 channel_id 参数" + ) + try: + channel_id = uuid.UUID(channel_id_param) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无效的渠道ID格式" + ) + elif user_channel_id: + channel_id = user_channel_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无法获取渠道ID" + ) + + # 验证租户存在且属于指定渠道 + tenant_result = await db.execute( + select(User).where( + and_( + User.id == tenant_id, + User.channel_id == channel_id + ) + ) + ) + tenant = tenant_result.scalar_one_or_none() + + if not tenant: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="租户不存在或不属于该渠道" + ) + + # 查找租户的模型 Key + key_result = await db.execute( + select(TenantModelKey).where( + and_( + TenantModelKey.tenant_id == tenant_id, + TenantModelKey.model_name == model_name + ) + ) + ) + tenant_key = key_result.scalar_one_or_none() + + if not tenant_key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"租户没有模型 '{model_name}' 的分配记录" + ) + + # 在 LiteLLM 中更新 Key + try: + from app.litellm_client import get_litellm_client, LiteLLMClientError + litellm_client = get_litellm_client() + + await litellm_client.update_key( + key=tenant_key.litellm_key_id, + rpm_limit=rpm_limit, + tpm_limit=tpm_limit, + max_budget=max_budget, + budget_duration=budget_duration, + ) + + # 更新数据库记录 + if rpm_limit is not None: + tenant_key.rpm_limit = rpm_limit + if tpm_limit is not None: + tenant_key.tpm_limit = tpm_limit + if max_budget is not None: + tenant_key.max_budget = max_budget + if budget_duration is not None: + tenant_key.budget_duration = budget_duration + + # 同时更新 ResourceAllocation + allocation_result = await db.execute( + select(ResourceAllocation).where( + and_( + ResourceAllocation.target_id == tenant_id, + ResourceAllocation.target_type == "tenant", + ResourceAllocation.resource_type == "model", + ResourceAllocation.resource_id == model_name + ) + ) + ) + allocation = allocation_result.scalar_one_or_none() + if allocation: + if rpm_limit is not None: + allocation.rpm = rpm_limit + if tpm_limit is not None: + allocation.tpm = tpm_limit + + await db.commit() + + logger.info( + f"租户 {tenant.name} 的模型 {model_name} 配额更新成功", + extra={"tenant_id": tenant_id, "model": model_name} + ) + + return SuccessResponse( + data={ + "tenantId": str(tenant_id), + "tenantName": tenant.name, + "modelName": model_name, + "rpmLimit": tenant_key.rpm_limit, + "tpmLimit": tenant_key.tpm_limit, + "maxBudget": float(tenant_key.max_budget) if tenant_key.max_budget else None, + "budgetDuration": tenant_key.budget_duration, + "status": tenant_key.status, + }, + message="模型配额更新成功,立即生效" + ) + + except LiteLLMClientError as e: + logger.error(f"LiteLLM Key 更新失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"LiteLLM Key 更新失败: {str(e)}" + ) + except Exception as e: + logger.error(f"配额更新失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"配额更新失败: {str(e)}" + ) + + +@router.get("/tenants/{tenant_id}/models", response_model=SuccessResponse) +async def get_tenant_models( + tenant_id: str, + channel_id_param: Optional[str] = Query(None, alias="channel_id", description="渠道ID(超级管理员必填)"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取租户的模型分配列表 + + 返回租户已分配的所有模型及其配额信息。 + + 权限:view:resources (channel_admin, billing_admin, operations_admin, super_admin) + """ + _verify_permission(principal, "view:resources") + role = _get_role(principal) + user_channel_id = _get_channel_id(principal) + + # 确定目标渠道ID + if role == "super_admin": + if not channel_id_param: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="超级管理员必须提供 channel_id 参数" + ) + try: + channel_id = uuid.UUID(channel_id_param) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无效的渠道ID格式" + ) + elif user_channel_id: + channel_id = user_channel_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="无法获取渠道ID" + ) + + # 验证租户存在且属于指定渠道 + tenant_result = await db.execute( + select(User).where( + and_( + User.id == tenant_id, + User.channel_id == channel_id + ) + ) + ) + tenant = tenant_result.scalar_one_or_none() + + if not tenant: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="租户不存在或不属于该渠道" + ) + + # 获取租户的所有模型 Key + keys_result = await db.execute( + select(TenantModelKey).where( + TenantModelKey.tenant_id == tenant_id + ) + ) + keys = keys_result.scalars().all() + + data = [] + for key in keys: + data.append({ + "modelName": key.model_name, + "rpmLimit": key.rpm_limit, + "tpmLimit": key.tpm_limit, + "maxBudget": float(key.max_budget) if key.max_budget else None, + "budgetDuration": key.budget_duration, + "status": key.status, + "createdAt": key.created_at.isoformat() if key.created_at else None, + "updatedAt": key.updated_at.isoformat() if key.updated_at else None, + }) + + return SuccessResponse( + data={ + "tenantId": str(tenant_id), + "tenantName": tenant.name, + "models": data, + } + ) + + # ============= 管理员管理 ============= @router.post("/admins/create", response_model=SuccessResponse) diff --git a/services/mcp-server/app/routes/user.py b/services/mcp-server/app/routes/user.py index 3cec69c..e20d8fb 100644 --- a/services/mcp-server/app/routes/user.py +++ b/services/mcp-server/app/routes/user.py @@ -13,7 +13,7 @@ from database import get_db from models import ( User, Agent, Tool, GatewayAPI, DataTemplate, Workflow, BillingRecord, RechargeRecord, TenantCustomAgentQuota, - PlatformAgentQuota, AgentBillingRecord + PlatformAgentQuota, AgentBillingRecord, TenantModelKey ) from app.billing import get_agent_billing_stats from app.auth import require_auth @@ -866,6 +866,155 @@ async def delete_user_workflow( ) +# ============= 模型使用(LiteLLM 集成)============= + +@router.get("/models/available", response_model=SuccessResponse) +async def get_available_models( + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取当前用户可用的模型列表 + + 返回渠道分配给该租户的模型及其配额信息。 + 这些模型可以在创建 Agent 时使用。 + """ + user_id = principal.get("user_id") + + # 查询分配给该租户的模型 Key + result = await db.execute( + select(TenantModelKey).where( + and_( + TenantModelKey.tenant_id == user_id, + TenantModelKey.status == "active" + ) + ) + ) + keys = result.scalars().all() + + models = [] + for key in keys: + models.append({ + "modelName": key.model_name, + "rpmLimit": key.rpm_limit, + "tpmLimit": key.tpm_limit, + "maxBudget": float(key.max_budget) if key.max_budget else None, + "budgetDuration": key.budget_duration, + "status": key.status, + "allocatedAt": key.created_at.isoformat() if key.created_at else None, + }) + + return SuccessResponse( + data={ + "models": models, + "count": len(models), + } + ) + + +@router.get("/models/usage/stats", response_model=SuccessResponse) +async def get_model_usage_stats( + model_name: Optional[str] = Query(None, description="模型名称,不传则返回所有模型"), + principal: dict = Depends(require_auth), + db: AsyncSession = Depends(get_db) +): + """ + 获取模型使用统计 + + 从 LiteLLM 获取用量数据,包括: + - 请求数 + - Token 使用量 + - 费用 + """ + user_id = principal.get("user_id") + + # 查询租户的模型 Key + query = select(TenantModelKey).where( + and_( + TenantModelKey.tenant_id == user_id, + TenantModelKey.status == "active" + ) + ) + + if model_name: + query = query.where(TenantModelKey.model_name == model_name) + + result = await db.execute(query) + keys = result.scalars().all() + + if not keys: + return SuccessResponse( + data={ + "models": [], + "totalSpend": 0, + } + ) + + # 尝试从 LiteLLM 获取用量数据 + usage_data = [] + total_spend = 0 + + try: + from app.litellm_client import get_litellm_client, LiteLLMClientError + litellm_client = get_litellm_client() + + for key in keys: + try: + # 获取该 Key 的用量 + spend_logs = await litellm_client.get_spend_logs( + api_key=key.litellm_key_id + ) + + # 汇总数据 + model_spend = sum(log.get("spend", 0) for log in spend_logs) + model_tokens = sum(log.get("total_tokens", 0) for log in spend_logs) + model_requests = len(spend_logs) + + total_spend += model_spend + + usage_data.append({ + "modelName": key.model_name, + "requests": model_requests, + "totalTokens": model_tokens, + "spend": model_spend, + "rpmLimit": key.rpm_limit, + "tpmLimit": key.tpm_limit, + "maxBudget": float(key.max_budget) if key.max_budget else None, + "budgetRemaining": float(key.max_budget) - model_spend if key.max_budget else None, + }) + + except LiteLLMClientError as e: + # 单个模型查询失败,记录但继续 + usage_data.append({ + "modelName": key.model_name, + "requests": 0, + "totalTokens": 0, + "spend": 0, + "error": str(e), + }) + + except Exception as e: + # LiteLLM 不可用,返回基本信息 + for key in keys: + usage_data.append({ + "modelName": key.model_name, + "requests": 0, + "totalTokens": 0, + "spend": 0, + "rpmLimit": key.rpm_limit, + "tpmLimit": key.tpm_limit, + "maxBudget": float(key.max_budget) if key.max_budget else None, + "note": "LiteLLM 服务暂不可用,无法获取用量数据", + }) + + return SuccessResponse( + data={ + "models": usage_data, + "totalSpend": total_spend, + } + ) + + # ============= 计费与资源 ============= @router.get("/billing/balance", response_model=SuccessResponse) @@ -1335,8 +1484,14 @@ async def create_custom_agent( 用户需要提供自己的终结点、密钥等配置。 会检查用户的 CPU/内存配额。 + + 如果用户指定了模型(通过 req.model),会自动注入 LiteLLM 相关环境变量: + - OPENAI_API_BASE: LiteLLM 网关地址 + - OPENAI_API_KEY: 租户的 LiteLLM API Key + - MODEL_NAME: 模型名称 """ from app.agent_manager_client import get_agent_manager_client, AgentConfig, AgentManagerError + from config import settings user_id = principal.get("user_id") channel_id = principal.get("channel_id") @@ -1381,6 +1536,51 @@ async def create_custom_agent( detail=f"内存配额不足。剩余: {remaining_memory:.2f} GB,请求: {memory_request:.2f} GB" ) + # 构建环境变量 + env_vars = req.envConfig or {} + if req.endpoint: + env_vars["ENDPOINT"] = req.endpoint + if req.apiKey: + env_vars["API_KEY"] = req.apiKey + + # 如果指定了模型,查询租户的 LiteLLM Key 并注入环境变量 + model_name = getattr(req, 'model', None) + if model_name: + tenant_key_result = await db.execute( + select(TenantModelKey).where( + and_( + TenantModelKey.tenant_id == user_id, + TenantModelKey.model_name == model_name, + TenantModelKey.status == "active" + ) + ) + ) + tenant_key = tenant_key_result.scalar_one_or_none() + + if not tenant_key: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"您没有使用模型 '{model_name}' 的权限,请联系渠道管理员分配" + ) + + # 解密 LiteLLM Key 并注入环境变量 + try: + from app.litellm_client import get_litellm_client + litellm_client = get_litellm_client() + decrypted_key = litellm_client.decrypt_key(tenant_key.litellm_key_hash) + + # 注入 LiteLLM 相关环境变量 + env_vars["OPENAI_API_BASE"] = settings.litellm_url + env_vars["OPENAI_API_KEY"] = decrypted_key + env_vars["MODEL_NAME"] = model_name + env_vars["LITELLM_MODEL"] = model_name + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取模型密钥失败: {str(e)}" + ) + try: client = get_agent_manager_client() @@ -1393,13 +1593,6 @@ async def create_custom_agent( memory_limit=req.memoryLimit or req.memoryRequest, ) - # 构建环境变量 - env_vars = req.envConfig or {} - if req.endpoint: - env_vars["ENDPOINT"] = req.endpoint - if req.apiKey: - env_vars["API_KEY"] = req.apiKey - # 创建自定义 Agent result = await client.create_custom_agent( name=req.name, @@ -1436,6 +1629,7 @@ async def create_custom_agent( "status": result.status, "servicePort": result.service_port, "accessInfo": result.access_info, + "modelInjected": model_name is not None, "quotaRemaining": { "cpu": remaining_cpu - cpu_request, "memory": remaining_memory - memory_request, diff --git a/services/mcp-server/config.py b/services/mcp-server/config.py index 3c7d66a..01a1b21 100644 --- a/services/mcp-server/config.py +++ b/services/mcp-server/config.py @@ -43,8 +43,16 @@ class Settings(BaseSettings): nats_max_reconnect_attempts: int = 10 # LiteLLM网关设置 - litellm_url: str = os.getenv("LITELLM_URL", "http://litellm-gateway:4000") - litellm_api_key: str = os.getenv("LITELLM_API_KEY", "sk-taiji-master-key") + litellm_url: str = os.getenv("LITELLM_URL", "http://4.144.175.186") + litellm_api_key: str = os.getenv("LITELLM_API_KEY", "sk-1f06b8f0d2e34c9b8a9f3d75a1c4e9b7-7e3a2c6bd9f441d8") + litellm_master_key: str = os.getenv("LITELLM_MASTER_KEY", "sk-1f06b8f0d2e34c9b8a9f3d75a1c4e9b7-7e3a2c6bd9f441d8") + + # LiteLLM Key 加密密钥(用于加密存储租户的 API Key) + # 必须是 32 字节的 base64 编码字符串,用于 Fernet 加密 + litellm_key_encryption_key: str = os.getenv( + "LITELLM_KEY_ENCRYPTION_KEY", + "dGFpamktYWktcGFkLWxpdGVsbG0ta2V5LWVuY3J5cHQ=" # 默认密钥,生产环境必须更换 + ) # MCP协议设置 mcp_timeout: int = 30 # 秒 diff --git a/services/mcp-server/database.py b/services/mcp-server/database.py index 37ceadc..9dd807d 100644 --- a/services/mcp-server/database.py +++ b/services/mcp-server/database.py @@ -136,12 +136,16 @@ async def create_initial_data(): pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") admin_user = User( + name="系统管理员", # 必填字段 username="admin", email="admin@taiji-ai.com", + password_hash=pwd_context.hash("admin123"), # 必填字段 hashed_password=pwd_context.hash("admin123"), full_name="系统管理员", + role="super_admin", # 设置为超级管理员 is_active=True, - is_admin=True + is_admin=True, + status="active", ) session.add(admin_user) diff --git a/services/mcp-server/migrations/011_add_litellm_integration.sql b/services/mcp-server/migrations/011_add_litellm_integration.sql new file mode 100644 index 0000000..3604fa5 --- /dev/null +++ b/services/mcp-server/migrations/011_add_litellm_integration.sql @@ -0,0 +1,73 @@ +-- 迁移脚本:添加 LiteLLM 集成相关表和字段 +-- 版本:011 +-- 日期:2026-01-07 +-- 描述:实现模型供应商与租户模型使用设计方案 + +-- 1. 给 channels 表添加 litellm_team_id 字段 +ALTER TABLE channels ADD COLUMN IF NOT EXISTS litellm_team_id VARCHAR(100); + +-- 添加索引 +CREATE INDEX IF NOT EXISTS idx_channel_litellm_team ON channels(litellm_team_id); + +-- 2. 创建 tenant_model_keys 表 +CREATE TABLE IF NOT EXISTS tenant_model_keys ( + 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, + + -- LiteLLM Key 信息 + litellm_key_id VARCHAR(255) NOT NULL, -- LiteLLM 返回的完整 key + litellm_key_hash TEXT NOT NULL, -- 加密存储 + + -- 配额配置(与 LiteLLM 同步) + rpm_limit INTEGER DEFAULT 0, + tpm_limit INTEGER DEFAULT 0, + max_budget NUMERIC(12, 2), + budget_duration VARCHAR(20) DEFAULT 'monthly', + + -- 状态 + status VARCHAR(20) DEFAULT 'active', + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- 唯一约束:每个租户每个模型只能有一个 Key + CONSTRAINT uq_tenant_model UNIQUE(tenant_id, model_name) +); + +-- 添加索引 +CREATE INDEX IF NOT EXISTS idx_tenant_model_key_tenant ON tenant_model_keys(tenant_id); +CREATE INDEX IF NOT EXISTS idx_tenant_model_key_model ON tenant_model_keys(model_name); +CREATE INDEX IF NOT EXISTS idx_tenant_model_key_status ON tenant_model_keys(status); + +-- 3. 添加注释 +COMMENT ON TABLE tenant_model_keys IS '租户模型 Key 表,存储租户在 LiteLLM 中的 API Key 信息'; +COMMENT ON COLUMN tenant_model_keys.model_name IS '模型名称,如 azure/gpt-4, gemini/gemini-pro'; +COMMENT ON COLUMN tenant_model_keys.litellm_key_id IS 'LiteLLM 返回的完整 API Key'; +COMMENT ON COLUMN tenant_model_keys.litellm_key_hash IS '加密存储的 Key(用于解密后注入到 Agent)'; +COMMENT ON COLUMN tenant_model_keys.rpm_limit IS '每分钟请求数限制'; +COMMENT ON COLUMN tenant_model_keys.tpm_limit IS '每分钟 Token 数限制'; +COMMENT ON COLUMN tenant_model_keys.max_budget IS '最大预算'; +COMMENT ON COLUMN tenant_model_keys.budget_duration IS '预算周期:monthly(每月)或 total(总计)'; +COMMENT ON COLUMN tenant_model_keys.status IS '状态:active(活跃)、suspended(暂停)、expired(过期)'; + +COMMENT ON COLUMN channels.litellm_team_id IS 'LiteLLM team ID,创建渠道时同步创建'; + +-- 4. 创建更新时间触发器 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 为 tenant_model_keys 表添加更新时间触发器 +DROP TRIGGER IF EXISTS update_tenant_model_keys_updated_at ON tenant_model_keys; +CREATE TRIGGER update_tenant_model_keys_updated_at + BEFORE UPDATE ON tenant_model_keys + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/services/mcp-server/migrations/run_011_migration.py b/services/mcp-server/migrations/run_011_migration.py new file mode 100644 index 0000000..fcd703a --- /dev/null +++ b/services/mcp-server/migrations/run_011_migration.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +迁移脚本:添加 LiteLLM 集成相关表和字段 + +运行方式: + cd services/mcp-server + python migrations/run_011_migration.py +""" + +import asyncio +import os +import sys + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sqlalchemy import text +from database import engine + + +async def run_migration(): + """执行迁移""" + + # 读取 SQL 文件 + sql_file = os.path.join(os.path.dirname(__file__), "011_add_litellm_integration.sql") + + with open(sql_file, "r", encoding="utf-8") as f: + sql_content = f.read() + + # 分割 SQL 语句(按分号分割,但忽略函数体内的分号) + statements = [] + current_statement = [] + in_function = False + + for line in sql_content.split("\n"): + stripped = line.strip() + + # 跳过注释 + if stripped.startswith("--"): + continue + + # 检测函数开始 + if "AS $$" in line or "AS $" in line: + in_function = True + + # 检测函数结束 + if in_function and ("$$ language" in line.lower() or "$$ LANGUAGE" in line): + in_function = False + + current_statement.append(line) + + # 如果不在函数内且行以分号结尾,则完成一条语句 + if not in_function and stripped.endswith(";"): + statement = "\n".join(current_statement).strip() + if statement and not statement.startswith("--"): + statements.append(statement) + current_statement = [] + + # 处理最后一条语句 + if current_statement: + statement = "\n".join(current_statement).strip() + if statement and not statement.startswith("--"): + statements.append(statement) + + print("=" * 60) + print("LiteLLM 集成迁移脚本") + print("=" * 60) + print(f"共 {len(statements)} 条 SQL 语句待执行") + print() + + async with engine.begin() as conn: + for i, statement in enumerate(statements, 1): + # 显示语句摘要 + first_line = statement.split("\n")[0][:60] + print(f"[{i}/{len(statements)}] 执行: {first_line}...") + + try: + await conn.execute(text(statement)) + print(f" ✓ 成功") + except Exception as e: + error_msg = str(e) + # 忽略 "already exists" 类型的错误 + if "already exists" in error_msg.lower(): + print(f" ⚠ 已存在,跳过") + else: + print(f" ✗ 失败: {error_msg}") + raise + + print() + print("=" * 60) + print("迁移完成!") + print("=" * 60) + print() + print("新增内容:") + print(" - channels 表添加 litellm_team_id 字段") + print(" - 创建 tenant_model_keys 表") + print(" - 添加相关索引和触发器") + + +if __name__ == "__main__": + asyncio.run(run_migration()) diff --git a/services/mcp-server/models.py b/services/mcp-server/models.py index 6b6a026..73e3f36 100644 --- a/services/mcp-server/models.py +++ b/services/mcp-server/models.py @@ -315,6 +315,11 @@ class Channel(BaseModel, Base): 渠道可以分配两种类型的 Agent 资源给租户: 1. 平台端 Agent:由管理员分配给渠道,渠道再分配给租户 2. 自定义 Agent 配额:渠道分配 CPU/内存配额上限给租户,租户在配额内创建多个自定义 Agent + + LiteLLM 集成: + - 每个渠道对应 LiteLLM 中的一个 team + - 创建渠道时同步创建 LiteLLM team + - litellm_team_id 存储 LiteLLM 返回的 team_id """ __tablename__ = "channels" @@ -325,6 +330,9 @@ class Channel(BaseModel, Base): commission_rate = Column(sa.Numeric(5, 2), default=0) channel_credit = Column(sa.Numeric(12, 2), default=0) # 渠道授信额度 + # LiteLLM 集成 + litellm_team_id = Column(String(100)) # LiteLLM team ID,创建渠道时同步创建 + # 自定义 Agent 资源配额上限(渠道可分配给租户的总配额) # 注意:这是配额上限,不是默认值。租户可以在配额内创建多个自定义 Agent custom_agent_cpu_quota = Column(sa.Numeric(12, 2), default=0) # 自定义 Agent CPU 配额上限(核心数) @@ -1225,3 +1233,47 @@ class PlatformAgentTemplateConfig(BaseModel, Base): Index("idx_template_config_name", template_name), Index("idx_template_config_enabled", is_enabled), ) + + +class TenantModelKey(BaseModel, Base): + """租户模型 Key 表 + + 存储租户在 LiteLLM 中的 API Key 信息。 + 每个租户可以有多个模型的 Key,每个 Key 对应一个模型。 + + 设计说明: + - 渠道分配模型给租户时,在 LiteLLM 创建 key 并保存到此表 + - Agent 启动时,从此表获取 key 注入到环境变量 + - 充值时,更新 LiteLLM key 的 max_budget + """ + __tablename__ = "tenant_model_keys" + + tenant_id = Column(GUID(), ForeignKey("users.id"), nullable=False) + channel_id = Column(GUID(), ForeignKey("channels.id")) + + # 模型信息 + model_name = Column(String(100), nullable=False) # 如 "azure/gpt-4", "gemini/gemini-pro" + + # LiteLLM Key 信息 + litellm_key_id = Column(String(255), nullable=False) # LiteLLM 返回的完整 key + litellm_key_hash = Column(Text, nullable=False) # 加密存储的 key + + # 配额配置(与 LiteLLM 同步) + rpm_limit = Column(Integer, default=0) # 每分钟请求数限制 + tpm_limit = Column(Integer, default=0) # 每分钟 Token 数限制 + max_budget = Column(sa.Numeric(12, 2)) # 最大预算 + budget_duration = Column(String(20), default="monthly") # 预算周期: monthly, total + + # 状态 + status = Column(String(20), default="active") # active, suspended, expired + + # 关联关系 + tenant = relationship("User") + channel = relationship("Channel") + + __table_args__ = ( + Index("idx_tenant_model_key_tenant", tenant_id), + Index("idx_tenant_model_key_model", model_name), + Index("idx_tenant_model_key_status", status), + UniqueConstraint("tenant_id", "model_name", name="uq_tenant_model"), + ) diff --git a/services/mcp-server/schemas.py b/services/mcp-server/schemas.py index 881d8db..062355a 100644 --- a/services/mcp-server/schemas.py +++ b/services/mcp-server/schemas.py @@ -515,12 +515,22 @@ class PaginatedResponse(BaseModel, Generic[T]): # ========== 系统状态 ========== +class ServiceStatus(BaseModel): + """服务状态""" + status: str + latency: int = 0 + error: Optional[str] = None + code: Optional[int] = None + + class HealthCheck(BaseModel): """健康检查响应""" status: str timestamp: datetime - services: Dict[str, str] + services: Dict[str, ServiceStatus] version: str = "1.0.0" + score: Optional[int] = None + uptime_seconds: Optional[float] = None class SystemMetrics(BaseModel):