mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
README 从 642 行降到 131 行,只保留简介、核心能力、最小快速开始、文档索引、 改名说明、目录结构与测试入口。其余内容迁到 docs/,中英文索引一一对应: docs/features.md 实现与完整特性清单(原 README 的"实现/特性") docs/configuration.md 配置文件、自动探测模型、多密钥与配额 docs/routing.md 模型路由、AUTO 链、冷却与半冷却、源/适配器、源模板 docs/performance.md 实测内存占用与调优、thinking 预算 docs/webui.md WebUI 使用 docs/adapters.md Lua 适配器协议与内置适配器清单 docs/gui.md 桌面端 Headless/Desktop 两种产物与构建 docs/development.md 目录职责、开发协作、测试 docs/deployment.md 部署、发行包、目录权限、llmsproxy 迁移与验证清单 中性化:README / README_EN / GUI 注释里的 `Clash Verge` 类比全部改为 「桌面壳 / desktop shell」,仓库内(源码与文档)已无该表述。
9.4 KiB
9.4 KiB
配置
本文档由 README 拆分而来:网关配置、模型自动探测、多密钥。 返回 README。
配置文件
首次运行在 -config 路径自动生成默认配置(默认 ./config.yaml),核心字段:
listen: 127.0.0.1:8080 # 网关监听地址(建议绑内网/回环)
gateway_keys: [sk-gw-<随机>] # 初始 admin 密钥(每次安装随机生成,仅首启写入运行时存储用)
default_model: AUTO # model 无法路由时自动按优先级链选源
adapter_dir: adapters # Lua 适配目录;目录不存在时首启创建并 seed 内置,存在则只读
runtime_file: runtime.json # WebUI 编辑的源/密钥/AUTO 链持久化到此文件
sources:
- name: deepseek
base_url: https://api.deepseek.com
api_key: sk-... # 也可用 api_key_env: SOME_ENV 引用环境变量(不落盘明文)
adapter: deepseek
max_concurrent: 8
models:
- id: deepseek-v4-flash
priority: 100 # YAML 源首次启动会 seed 进 AUTO 链的初值
kind: chat
- id: deepseek-v4-pro
priority: 60
kind: chat
# 静态请求头(比适配器默认优先)
headers: { X-Tenant: prod }
# 传给 Lua build_headers 的透传元数据
meta: { app_id: x, app_secret: y }
temperature: 0.7
max_tokens: 4096
timeout: 120s # 请求超时,默认 120s
自动探测模型列表
不想手抄模型 id 时,只填 URL 和 key,让网关去扫:
# 只探测,不写任何东西
curl -X POST http://127.0.0.1:8080/api/sources/discover \
-H "Authorization: Bearer sk-gw-…" -H "Content-Type: application/json" \
-d '{"base_url":"https://api.vendor.com/v1","api_key":"sk-…"}'
# → {"ok":true,"base_url":"…","endpoint":"","models":["gpt-x",…],
# "image_models":["gpt-image-2"],"models_path":"/models","latency_ms":1387,…}
# 探测后直接落库
curl -X POST http://127.0.0.1:8080/api/sources/discover/apply \
-H "Authorization: Bearer sk-gw-…" -H "Content-Type: application/json" \
-d '{"base_url":"https://api.vendor.com/v1","api_key":"sk-…","name":"vendor"}'
WebUI 源编辑框里也有「自动探测模型」按钮:填好 URL 和 key 点一下,列表自动 填好,仍需点保存才生效。
几点值得知道:
- URL 形态不用猜。 厂商对「
/v1放 base 还是放 path」没有共识,探测会 依次尝试若干候选并返回答对的那个,同时给出归一化后的base_url+endpoint(endpoint平时留空,因为 openai adapter 自带/chat/completions)。 image_endpoint是另一套规则。 网关的图像默认路径是绝对的/v1/images/generations,而 chat 默认是相对的。所以base_url带/v1时 必须显式给image_endpoint: /images/generations,否则图像请求 404。 探测会替你填好。- 图像模型只看上游自己声明的 modality,绝不从模型名猜。
gpt-image-2这类 id 也出现在同时提供对话服务的目录上,猜错会让 AUTO 链选中一个只支持 图像生成的模型,其后每一层都被它拖垮。 - 不会逐模型探活。 34 个模型的目录跑一轮要花钱且耗时数分钟,
/models是足够便宜的信号;模型是否真能应答留给正常请求去发现。 - 对已存在的源,必须显式
replace_models: true才会覆盖模型列表。重新 探测顺手抹掉为 AUTO 链手工调过的 priority,是最难排查的一类数据丢失。 api_key留空表示沿用已存值,不会把凭据清空。
config.yaml、runtime.json、master.key、adapters/ 均不提交版本库(见
.gitignore),请随备份带走——master.key 丢失后运行时密文无法解密。
运行文件生成在 -config 所在目录:adapter_dir / runtime_file 自动指向
配置文件旁边的 adapters/ 与 runtime.json,从任意工作目录启动都可用。
运行时文件(runtime_file)中的敏感字段自动加密:
- 加密算法 AES-256-GCM,格式
enc:v1:<base64>。 - 主密钥来源:环境变量
MODELROUTER_MASTER_KEY(64 位 hex);否则读取runtime_file同目录的master.key;都不存在时首次启动自动生成master.key(0600)。 - 升级时旧明文文件自动兼容:首次运行正常读取,任何 UI 保存操作触发全文件加密迁移。
- 注意:master.key 丢失后密文无法解密,请随配置一起备份;切勿提交到版本库。
网关密钥(多密钥)
网关鉴权采用「多密钥 + 角色 + 模型范围」架构,密钥持久化在 runtime_file 的
keys 字段(加密存储):
gateway_keys配置只是初始 admin 密钥种子:首次启动迁移为运行时 admin key,之后不再参与鉴权管理。- 重要:首次启动后请在 WebUI「密钥」页更换管理员密钥——初始密钥明文写在
config.yaml里(每次安装随机生成),继续使用存在被盗风险;用新密钥登录后删除初始密钥。 - WebUI 密钥页可创建/删除密钥;每个密钥可指定
admin(管理全部)或user(仅看自己的 key)角色,并配置模型范围(模型 + 源 + token 配额 + 重置周期)。 - 客户端用任意一个已授权的密钥明文作为 Bearer(
Authorization: Bearer <key>)。 - 删除密钥即从运行时存储移除,立即失效。
密钥用量配额
配额按模型单独设置:每个密钥的 models[] 里,每一条模型范围各自带一份
token 配额与请求数配额。一个模型用满只影响该模型,同一密钥的其它模型照常工作。
keys:
- key: sk-gw-<hex>
role: user
name: agent-alice
models:
- model: deepseek-v4-flash
token_quota: 1000000 # 本周期内该 key 用这个模型的 token 预算
req_quota: 20000 # 本周期内的请求次数
period: nhour # "" | hour | week | month | nhour
hours: 6 # 仅 nhour
- model: AUTO
token_quota: 5000000 # AUTO 也是一条独立配额
period: day # ← 这种写法会被拒绝(词表只有 hour/week/month/nhour)
- model: kimi-k3 # 未列配额 = 无限
- 没有「整钥总配额」:这是刻意的设计。整钥总额会让「把 A 模型的额度挪给 B 模型」变成一次全局重分配;按模型独立计费则每个模型各自可控,运维可以 看出哪个模型吃掉了预算。
period词表:空 = 永不过期(累计总量),hour/week/month= 固定窗口,nhour+hours= 自定义小时数。拼错的周期在写入时就被拒,不会静默变成 永不过期。- 配额严格按密钥隔离,且同一密钥内按模型隔离:A 密钥用满
m1不会消耗 B 密钥的额度,同一密钥的m2也不受影响。 - 配额统计含聊天、流式、生图,跨重启从审计日志回放(保留 40 天,覆盖最长的 month 窗口)。
- 配额耗尽返回 429 +
Retry-After(rate_limit_exceeded),消息里点名是哪个 模型用满了,客户端可等窗口重置后自动恢复;模型越权才是 403。 admin 密钥永不受配额限制(其 scope 上的配额也不强制),避免把管理员 锁在门外。 - 窗口用量按整点小时分桶统计,实际释放比配置窗口最多晚 1 小时(配额宁可晚释放 也不超发)。
PUT /api/keys/{key}提交models即同时提交它们的配额(配额就是 scope 的一部分, 不存在会与模型列表脱节的第二份预算)。显式0= 解除该模型的限制。
配额拒绝 vs 容量拒绝:两种「拒绝」含义不同
配额在入口检查,不占用任何上游槽位,因此是廉价拒绝;容量不足则要走
busyWait 有界等待后才返回 503。实测(容量 8、0.6s/请求、100 并发):
| 情况 | 状态码 | 延迟 | 是否打到上游 |
|---|---|---|---|
配额用尽(req_quota 50 → 100 并发) |
429 rate_limit_exceeded + Retry-After |
19–21ms | 否 |
| 容量打满(同一拓扑,未限配额) | 503 upstream_error |
~2.6s(busyWait) |
超出部分否 |
客户端因此可以区分「等窗口重置」与「等上游腾出容量」:前者只需等待,后者 通常该降并发或换源。
配额的开销
配额桶按 (密钥, 模型, 整点小时) 分桶保留 40 天,实测(AMD 7840HS):
- 窗口查询按窗口长度而非保留总量扫描:1h 窗口 49ns、24h 窗口 55ns、 30d 窗口 3.9μs(此前全扫保留总量,960 桶时 5.9μs)。
- 记录一条请求:283ns、3 分配(与引入配额前相同,分配来自 ring buffer)。
- 内存:生产形态(7 密钥 × 8 模型 × 2 源 × 满 40 天 retention)约 3.7MB。
按源 pin 的
source::model桶惰性创建——只有当某条配额真的 pin 了 某个源时才维护,否则每条记录多写一份桶,在 20 密钥 × 8 模型 × 3 源下会 多占 18MB。 代价:配置 pinned 配额之前的历史用量无法事后按源拆分,所以 pinned 配额的 首个窗口可能少算。
# 配额耗尽时客户端看到(点名了具体模型)
HTTP/1.1 429 Too Many Requests
Retry-After: 2100
{"error":{"type":"rate_limit_exceeded","message":"token quota exceeded for \"deepseek-v4-flash\" (5000000/5000000)"}}