# 配置 > 本文档由 README 拆分而来:网关配置、模型自动探测、多密钥。 > 返回 [README](../README.md)。 ## 配置文件 首次运行在 `-config` 路径自动生成默认配置(默认 `./config.yaml`),核心字段: ```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,让网关去扫: ```bash # 只探测,不写任何东西 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:`。 - 主密钥来源:环境变量 `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 `)。 - 删除密钥即从运行时存储移除,立即失效。 ### 密钥用量配额 **配额按模型单独设置**:每个密钥的 `models[]` 里,每一条模型范围各自带一份 token 配额与请求数配额。一个模型用满只影响该模型,同一密钥的其它模型照常工作。 ```yaml keys: - key: sk-gw- 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 配额的 首个窗口可能少算**。 ```bash # 配额耗尽时客户端看到(点名了具体模型) 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)"}} ```