Files
ModelRouter/docs/configuration.md
JianFeeeee f7e926ccb7 docs: README 拆分为主题文档 + 中性化竞品表述
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」,仓库内(源码与文档)已无该表述。
2026-10-26 22:28:24 +08:00

9.4 KiB
Raw Blame History

配置

本文档由 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)"}}