# lucky_router **Repository Path**: agent_26/lucky_router ## Basic Information - **Project Name**: lucky_router - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-22 - **Last Updated**: 2026-07-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # lucky_router > 用 Go 从零搭建的 LLM API 网关 —— 把多个厂商的 API 收口到一个平台,支持一键切换厂商、统一管理密钥、成本控制、限流护栏、可观测性。 架构上参考了 Bifrost(Provider 抽象、插件化)、New API/One API(三级路由、渠道管理、用户/额度)、 LiteLLM(成本计算),但**所有代码均为原创实现**,不存在协议兼容问题。对外暴露标准 OpenAI 兼容接口, 业务侧用现成的 OpenAI SDK 改个 `base_url` 就能接入。 ## ✨ 核心特性 - 🔀 **厂商聚合 + 一键切换**:统一 `Provider` 接口,已接入 OpenAI / Anthropic / Gemini;OpenAI 兼容厂商(DeepSeek/Qwen/GLM/MiniMax 等)改 `base_url` 免代码接入 - 🔑 **密钥收口**:业务侧只持有虚拟密钥 `sk-lucky-xxx`,永不接触厂商真实 Key;渠道真实密钥 AES-256-GCM 加密落库 - 🧭 **三级路由 + 故障转移**:模型别名 → 渠道组 → 渠道,渠道级熔断 + 自动故障转移;支持加权随机 / 最低延迟(EWMA)/ 最低成本三种自适应策略 - 💰 **成本控制**:按 Token 计价、按虚拟 Key 记账、预算超限拒绝;用户账户余额 + 充值 + 兑换码 + 分组计费倍率 - 👥 **用户体系 + RBAC**:用户/分组/余额,管理后台账号分 admin(可写)/viewer(只读),服务端 session 登录(密码 PBKDF2 哈希) - 🛡️ **护栏 + 治理**:关键词拦截、PII 脱敏、RPM 限流 - ⚡ **响应缓存**:精确 + 简化版语义(trigram)缓存,命中直接返回不走上游、不计费 - 📊 **可观测性**:Prometheus 指标(`/metrics`)、渠道主动健康检查、Web 管理后台(概览看板 + 模型广场) - 🧩 **扩展模块**:插件生命周期钩子、告警、集群心跳、Prompt 版本库、MCP 网关、技能库、模型评测 ## 🚀 快速开始 ```bash # 纯内存模式(零依赖,重启数据丢失,适合本地试跑) export ADMIN_TOKEN=admin-secret-token # 管理接口令牌(必填) export OPENAI_API_KEY=sk-xxx # 至少配一个厂商 key go run ./cmd/server # 默认监听 :8080 # 用标准 OpenAI 格式调用(用启动日志里打印的测试虚拟密钥) curl -X POST http://localhost:8080/v1/chat/completions \ -H "Authorization: Bearer <虚拟密钥>" -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}' ``` 管理后台(React + Vite):`cd web && npm install && npm run dev`,浏览器打开 5173,用 `ADMIN_USERNAME`/`ADMIN_PASSWORD`(默认 `admin` / `ADMIN_TOKEN` 的值)登录。 数据库模式(持久化账单/用量)见下方「运行」与「数据持久化」章节。 ## 📁 项目结构 ``` lucky_router/ ├── core/providers/ # Provider 接口 + 厂商适配器(openai / anthropic / gemini) ├── core/schemas/ # 统一请求/响应结构(OpenAI 兼容) ├── router/ # 三级路由 + 熔断/故障转移 + 自适应策略 ├── gateway/ # HTTP 入口,串联鉴权/缓存/路由/计费/指标 ├── billing/ # 价格表 + 预算账本 ├── account/ # 用户/分组/余额/充值/兑换码 ├── admin/ adminauth/ # 虚拟密钥收口 / 管理员账号 + RBAC + session ├── adminapi/ # 管理后台 HTTP 接口(/admin/*) ├── cache/ metrics/ # 响应缓存 / Prometheus 指标 ├── healthcheck/ # 渠道主动健康检查 ├── guardrail/ governance/ alerting/ plugins/ # 护栏 / 限流 / 告警 / 插件 ├── prompts/ mcp/ skills/ evals/ cluster/ # Prompt库 / MCP / 技能 / 评测 / 集群 ├── store/ # MySQL 持久化(GORM) ├── web/ # React + Vite 管理后台(概览/模型广场/各管理页) └── cmd/server/main.go # 服务入口 ``` ## 🧪 测试 ```bash go test ./... # 全部单元测试(20+ 包) go test -race ./gateway/ # 关键并发路径的竞态检测 bash concurrent_test.sh 10 5 # 压测:每模型 10 请求、5 并发,输出 P50/P90/P95/P99 bash stream_test.sh # 流式测试:TTFB / chunk 数 / [DONE] 校验 ``` ## 已实现能力 - **厂商 API 聚合**:统一 `Provider` 接口,已接入 OpenAI、Anthropic、Google Gemini 三家,新增厂商只需实现接口 + 注册;OpenAI 兼容厂商(DeepSeek/Qwen/GLM/MiniMax 等)无需写代码,直接复用 OpenAI 适配器 + 配置 `base_url` 即可 - **密钥收口**:虚拟密钥体系(`admin.KeyStore`),业务侧只持有 `sk-lucky-xxx`,从不接触厂商真实 Key - **一键切换厂商**:业务调用模型别名(如 `gpt-4o`),后台可随时改绑定的渠道/厂商,改配置即时生效 - **故障转移**:渠道级熔断(连续失败自动熔断,冷却后半开探测恢复),请求失败自动切换备用渠道重试 - **成本控制**:按 Token 用量计价、按虚拟 Key 维度记账、预算超限自动拒绝请求 - **用户体系**:用户 + 用户分组 + 账户余额(美元制),虚拟密钥可归属到用户;支持管理员手动充值、批量生成兑换码充值;分组可配计费倍率(转售加价场景:扣用户余额 = 上游成本 × 倍率) ## 管理 API `adminapi` 包提供了虚拟密钥和渠道的管理接口,挂在 `/admin/` 前缀下,和业务侧虚拟密钥完全隔离, 业务密钥泄露不会影响管理接口安全。 鉴权支持两种方式: - **主令牌**:`Authorization: Bearer $ADMIN_TOKEN`(环境变量 `ADMIN_TOKEN`,必填),始终视为 admin 角色,方便脚本调用,也作为忘记密码时的兜底入口。 - **账号 + 服务端 session**:用用户名密码登录(`/admin/auth/login`)换取有时效的 session token,后续请求带这个 token。支持 **RBAC**:`admin` 可读可写,`viewer` 只读(写操作一律 403)。首次启动会自动创建默认管理员(用户名 `ADMIN_USERNAME`,默认 `admin`;密码 `ADMIN_PASSWORD`,未设置则等于 `ADMIN_TOKEN`)。 | 方法 | 路径 | 说明 | |---|---|---| | POST | `/admin/auth/login` | 用户名+密码登录,返回 session token 和角色(不鉴权) | | POST | `/admin/auth/logout` | 注销当前 session | | GET | `/admin/auth/me` | 查询当前登录者的用户名和角色 | | GET | `/admin/admins` | 列出管理员账号(仅 admin) | | POST | `/admin/admins` | 新建管理员账号(仅 admin,`username`/`password`/`role`) | | DELETE | `/admin/admins/{username}` | 删除管理员账号(仅 admin,不能删自己) | | GET | `/admin/keys` | 列出所有虚拟密钥(不回显完整 Key) | | POST | `/admin/keys` | 签发新虚拟密钥,响应里只有这一次会返回完整 Key | | GET | `/admin/keys/{id}` | 查看单个虚拟密钥详情及用量 | | POST | `/admin/keys/{id}/revoke` | 吊销 | | POST | `/admin/keys/{id}/enable` | 重新启用 | | POST | `/admin/keys/{id}/budget` | 设置预算上限 | | POST | `/admin/keys` (带 `user_id`) | 签发密钥时可归属到某个用户,该用户的余额/启用状态会参与请求校验 | | GET | `/admin/users` | 列出所有用户 | | POST | `/admin/users` | 新建用户(`username` 必填,可选 `display_name`/`group_id`) | | GET | `/admin/users/{id}` | 查看用户详情(余额、分组、启用状态) | | POST | `/admin/users/{id}/enable` \| `/disable` | 启用/禁用用户(禁用后其名下所有密钥请求被拒) | | POST | `/admin/users/{id}/recharge` | 管理员手动充值(`amount_usd`,可选 `note`) | | POST | `/admin/users/{id}/group` | 划入/移出分组(`group_id`,空表示移出) | | POST | `/admin/users/{id}/redeem` | 用户核销兑换码(`code`),成功后余额增加 | | GET | `/admin/groups` | 列出所有用户分组 | | POST | `/admin/groups` | 新建分组(`name`,可选 `description`/`billing_multiplier`) | | GET | `/admin/redemptions` | 列出兑换码(code 掩码显示,不回显完整码) | | POST | `/admin/redemptions` | 批量生成兑换码(`amount_usd`/`count`),完整 code 仅这一次返回 | | GET | `/admin/channels` | 列出所有渠道(不回显厂商真实密钥) | | POST | `/admin/channels` | 新增渠道并绑定到模型别名(一键切换厂商的写入侧) | | GET | `/admin/channels/{id}` | 查看单个渠道详情 | | POST | `/admin/channels/{id}/enable` | 启用 | | POST | `/admin/channels/{id}/disable` | 禁用 | | POST | `/admin/channels/{id}/test` | 主动探测单个渠道(发最小请求),返回延迟/是否可用,并更新其熔断状态 | | GET | `/admin/channels/health` | 查看所有渠道最近一次健康检查结果 | | POST | `/admin/channels/health` | 一键探测所有渠道 | | GET | `/admin/cache/stats` | 查看响应缓存命中率/条目数(需开启缓存) | | POST | `/admin/cache/clear` | 清空响应缓存 | | GET | `/admin/usage/logs` | 调用明细分页查询,支持 `virtual_key_id`/`channel_id`/`model`/`start_time`/`end_time`/`limit`/`offset` 筛选(时间用 RFC3339 格式) | | GET | `/admin/usage/summary` | 按天聚合的成本/调用次数,筛选参数同上(除分页) | 示例: ```bash export ADMIN_TOKEN=admin-secret-token # 签发一个虚拟密钥 curl -X POST http://localhost:8080/admin/keys \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"name":"团队B","allow_models":["gpt-4o"],"budget_usd":20}' # 新增一个厂商渠道并绑定到模型别名 curl -X POST http://localhost:8080/admin/channels \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"id":"ch-deepseek-1","provider":"openai","api_key":"sk-xxx","base_url":"https://api.deepseek.com/v1","aliases":["deepseek-chat"]}' ``` 用户体系典型流程(建分组 → 建用户 → 充值 → 签发归属该用户的密钥): ```bash # 1. 建一个计费倍率 1.5 的分组(转售加价 50%) curl -X POST http://localhost:8080/admin/groups \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"name":"经销商A","billing_multiplier":1.5}' # 2. 建用户(把上一步返回的分组 id 填到 group_id) curl -X POST http://localhost:8080/admin/users \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"username":"alice","display_name":"Alice","group_id":""}' # 3a. 管理员直接充值 $50 curl -X POST http://localhost:8080/admin/users//recharge \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"amount_usd":50,"note":"首充"}' # 3b. 或者:生成兑换码,再让用户核销充值 curl -X POST http://localhost:8080/admin/redemptions \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"amount_usd":20,"count":5}' curl -X POST http://localhost:8080/admin/users//redeem \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"code":"lr-xxxx"}' # 4. 签发归属该用户的虚拟密钥,之后用它调用时会校验用户余额、并按分组倍率扣费 curl -X POST http://localhost:8080/admin/keys \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -d '{"name":"alice-prod","user_id":"","allow_models":["gpt-4o"]}' ``` ## 管理前端 `web/` 目录是一个独立的 React + Vite + TypeScript 项目,提供以下管理页面:虚拟密钥、渠道、账单、 插件、警报、治理(限流)、护栏、集群配置、提示存储库。 ```bash cd web npm install npm run dev # 默认监听 5173,通过 vite proxy 把 /admin 请求转发到 8080 的 Go 后端 ``` 打开 http://localhost:5173,输入 `ADMIN_TOKEN` 登录(令牌存在浏览器 localStorage,不做服务端 session)。 生产部署时执行 `npm run build` 产出 `web/dist` 静态文件,建议和 Go 后端同域部署(反向代理层做路径分发), 避免额外处理 CORS。 ## 新增能力模块(插件/警报/治理/护栏/集群/提示存储库) 参考 Bifrost 的架构设计,补充了六个企业级能力模块,均已接入网关请求链路(不是摆设配置)。 | 模块 | 包 | 作用 | 管理接口 | |---|---|---|---| | 插件 | `plugins` | 请求生命周期钩子框架,内置 `logging` 示范插件,记录每次请求的处理过程 | `/admin/plugins` | | 警报 | `alerting` | 预算使用率超阈值、渠道熔断时记录告警历史(5 分钟冷却,不刷屏) | `/admin/alerts/*` | | 治理 | `governance` | 速率限制(RPM),支持全局规则或按虚拟密钥单独限流 | `/admin/governance/ratelimits` | | 护栏 | `guardrail` | 关键词拦截(直接拒绝)、PII 脱敏(邮箱/手机号替换后放行) | `/admin/guardrails` | | 集群配置 | `cluster` | 多实例心跳注册,管理后台可看到哪些实例在线 | `/admin/cluster/nodes` | | 提示存储库 | `prompts` | Prompt 模板版本化存储,同名再次保存产生新版本,历史永久保留 | `/admin/prompts` | | 自适应路由 | `router`(内置) | 三种调度策略:加权随机(默认)、延迟最低优先(EWMA 估计)、成本最低优先(按价格表) | `/admin/routing/strategy` | | MCP 网关 | `mcp` | 统一注册管理外部工具服务器,提供工具调用代理转发(简化版协议,非完整 MCP 规范) | `/admin/mcp/servers` | | 技能库 | `skills` | 给"MCP 服务器+工具"起名并集中管理,方便复用 | `/admin/skills` | | 评价 | `evals` | 同一个 Prompt 在多个模型别名上跑一遍,横向对比输出/延迟/成本,支持人工打分 | `/admin/evals/*` | | 用户体系 | `account` | 用户/分组/账户余额/充值/兑换码,密钥归属用户,按分组倍率扣费(参考 New API 的用户+额度模型,计价改用美元制) | `/admin/users`、`/admin/groups`、`/admin/redemptions` | | 健康检查 | `healthcheck` | 渠道主动探测(手动或后台定时),提前发现挂掉的渠道、加速熔断渠道恢复,补充原有的被动熔断 | `/admin/channels/{id}/test`、`/admin/channels/health` | | 管理员账号 | `adminauth` | 管理后台账号 + 角色(RBAC:admin 可写/viewer 只读)+ 服务端 session 登录,密码 PBKDF2 哈希;主令牌 ADMIN_TOKEN 仍作为 admin 兜底 | `/admin/auth/*`、`/admin/admins` | | 响应缓存 | `cache` | 简化版语义缓存:精确命中(归一化哈希)+ 模糊命中(trigram 相似度),命中直接返回不走上游、不计费,降本降延迟 | `/admin/cache/stats`、`/admin/cache/clear` | | 可观测性 | `metrics` | 请求数/延迟分位/token/成本/缓存命中/渠道失败等指标,以 Prometheus 文本格式暴露在 `/metrics`(无外部依赖) | `/metrics`(挂在业务侧 mux,非 /admin) | 请求处理顺序:鉴权 → 模型权限 → 预算检查 → **用户账户检查(启用状态 + 余额)** → **限流(治理)** → **内容安全(护栏)** → **插件 PreRequest** → **缓存查询(命中直接返回,不走上游/不计费)** → 路由调用 → 计费 → **用户余额扣减(按分组倍率)** → **写入缓存** → **预算告警检测** → **插件 PostResponse** → (全程) **指标采集到 `/metrics`**。 ## 尚未实现(下一步) - 更多厂商适配器(Azure OpenAI、AWS Bedrock 等,协议差异较大的) - 管理后台已有 RBAC(admin/viewer 两级)+ 服务端 session,但权限只到"读/写"粗粒度,没有按资源/操作的细粒度权限;业务侧用户(account)也还没有自助登录(仍由管理员代管) - session 是每实例内存态,多实例部署时不共享(未接入 Redis 等集中式会话存储) - 语义缓存目前是 trigram 相似度的简化版(零依赖、零额外调用),不是 embedding 级别的真语义匹配;且缓存是每实例内存态,多实例不共享(未接入 Redis 等分布式缓存) - 可观测性目前是自研的 Prometheus 文本指标(`/metrics`),尚未接入 OTel 分布式追踪 - 渠道删除接口(目前只支持新增/启用/禁用,没有硬删除,避免误删丢失历史绑定关系) - 前端已按角色隐藏"管理员账号"页,viewer 登录后顶部显示只读提示条,且所有写操作在 API 层被统一拦截(服务端仍是真正的权限边界,双重保险);更精细的"按资源/操作"权限尚未做 - 告警目前只记录历史,没有对接邮件/Webhook 等实际通知渠道 - MCP 网关是简化版协议(JSON POST 代理转发),不是完整的 MCP 规范实现(缺少 SSE 长连接、能力协商等) - 评价模块的调用不经过业务侧鉴权/限流/护栏,是管理员直接触发的离线对比,不代表真实线上请求路径 ## 数据持久化 已接入 MySQL(GORM),表结构定义在 `store/models.go`: | 表 | 用途 | |---|---| | `virtual_keys` | 虚拟密钥(密钥收口) | | `channels` | 渠道配置(厂商、真实密钥、模型映射) | | `model_group_bindings` | 模型别名 -> 渠道的绑定关系(一键切换厂商靠这张表) | | `usage_ledgers` | 按虚拟 Key 的用量/预算汇总 | | `usage_logs` | 每次调用的成本明细(账单/审计用) | | `users` | 用户/租户(账户余额、归属分组、启用状态) | | `user_groups` | 用户分组(计费倍率) | | `recharge_logs` | 账户余额变动流水(手动充值/兑换码充值,对账审计用) | | `redemptions` | 兑换码(充值卡),一码一次,事务核销防并发重复使用 | | `admin_users` | 管理后台账号(用户名、PBKDF2 密码哈希、角色);session 不落库,是每实例内存态 | ## 运行 设置 `DB_DSN` 环境变量即可接入数据库持久化;不设置则退化为纯内存模式(重启数据丢失,适合本地快速试跑,单测也走的是这个模式)。 ```bash # 本地起一个 MySQL(如果还没有的话) docker run -d --name lucky_router_mysql \ -e MYSQL_ROOT_PASSWORD=lucky_router_pw \ -e MYSQL_DATABASE=lucky_router \ -p 127.0.0.1:3306:3306 \ mysql:8.0 export DB_DSN="root:lucky_router_pw@tcp(127.0.0.1:3306)/lucky_router?charset=utf8mb4&parseTime=True&loc=Local" export CHANNEL_ENCRYPTION_KEY="一段任意长度的强随机字符串,妥善保管,丢失后已加密的渠道密钥将无法恢复" export OPENAI_API_KEY=sk-xxx export ANTHROPIC_API_KEY=sk-ant-xxx export GEMINI_API_KEY=xxx # 可选:开启渠道后台自动健康检查(不设则关闭,只能手动 /admin/channels/{id}/test 触发) export HEALTHCHECK_INTERVAL=5m # 可选:开启响应缓存(默认关闭),CACHE_TTL 可选覆盖默认存活时间 export CACHE_ENABLED=true export CACHE_TTL=10m # 可选:自定义默认管理员账号(不设则用户名 admin、密码等于 ADMIN_TOKEN) export ADMIN_USERNAME=admin export ADMIN_PASSWORD=change-me-strong go run ./cmd/server ``` 首次启动会自动建表(AutoMigrate)并写入一份内置渠道配置(OpenAI/Anthropic/Gemini);之后重启会直接从数据库加载已有渠道和虚拟密钥,不会重复创建。 ## 渠道密钥加密 `DB_DSN` 设置后,`CHANNEL_ENCRYPTION_KEY` 是必填项(未设置会直接启动失败),避免有人忘记配置导致密钥明文落库。 - 加密算法:AES-256-GCM,密钥经 SHA-256 摘要派生自 `CHANNEL_ENCRYPTION_KEY`,所以该变量长度不限 - 每次加密使用随机 nonce,同一密钥多次加密结果不同,防止密文模式分析 - 兼容历史明文数据:读取时如果发现值不带 `enc:` 前缀,会原样返回而不是解密失败,方便从明文阶段平滑升级 - `CHANNEL_ENCRYPTION_KEY` 一旦更换,旧密文将无法解密,需要重新在渠道管理里录入密钥(后续加管理后台后,可以配套做密钥轮换脚本) 调用示例: ```bash curl -X POST http://localhost:8080/v1/chat/completions \ -H "Authorization: Bearer <上面打印的虚拟密钥>" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}' # 切换到 Gemini,只需改 model 别名,业务侧代码不用动 curl -X POST http://localhost:8080/v1/chat/completions \ -H "Authorization: Bearer <上面打印的虚拟密钥>" \ -H "Content-Type: application/json" \ -d '{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"hi"}]}' ``` ## 测试 ```bash go test ./... ```