mydsh-auto-model-router
Verified@apinggithub/mydsh-auto-model-router · v0.3.4 · MIT · Web UI
Hybrid model router for DeepSeek Harness: declarative rules, user-configurable cost control (cost-first / quality-first / balanced), optional LLM task classifier, and failure fallback chains.
Install
dsh plugin add @apinggithub/mydsh-auto-model-router Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
mydsh-auto-model-router
一个适配 DeepSeek Harness(DSH)的混合式自动模型路由插件。通过 L0–L3 四个能力层级为每个 agent 请求选择模型——日常问答、代码测试、编写审查、复合多步任务——支持用户可配置的成本策略、可选的 LLM 任务分类器、以及失败降级链。
设计思路参考 open-world-project/model-router(Hermes Agent)与 opencode-model-router(fast/medium/heavy 分层):层级化路由 + 成本模式决定模糊时的偏移方向。
致谢
本插件的思路来自以下开源项目:
open-world-project/model-router — Hermes Agent 的自动模型路由器,含关键词评分和成本感知分层选择
opencode-model-router — OpenCode 的 fast/medium/heavy 分层委托插件
openclaw-routing-yaml — OpenClaw 的声明式 YAML 按任务路由
openmarkai/openclaw-router — 基于基准测试驱动的模型路由
MoeAisaka/openclaw-model-policy-router — OpenClaw 的策略驱动 fail-closed 路由
NanmiCoder/dsh-auto-mode — DSH 的自动权限模式;客户端设置页实现参考
DeepSeek Harness — 本插件所适配的宿主平台
中文(本文档)
工作原理
用户消息
│
▼
① agent/pre-step 任务文本 + token 记账
│
▼
② system-prompt/assemble 让 {{model}} 与路由结果保持一致
│
▼
③ agent/request ★ 决策点:
│
├─ Tier 1 规则: 子串 / 正则匹配 → 层级(L0…L3)——永远优先
├─ Tier 2 成本: 预算耗尽 → 钉到最便宜层级;
│ 未命中规则 → 模式决定默认层级
├─ Tier 3 分类器: 可选便宜 LLM → 层级;层级缺模型 → 按模式漂移
└─ Tier 4 降级: 请求失败 → 沿链切换模型并重试
│
▼
LLM 分发
四个层级
| 层级 | 典型任务 | 模型建议 |
|---|---|---|
L0 |
日常问答、快速回答 | 最便宜/最快 |
L1 |
小改动、简单测试 | 普通 |
L2 |
写代码、审查、重构 | 较强 |
L3 |
复杂 BUG 修复、多步任务、汇报 | 最强 |
每个层级是一个用户自行指定的 provider/model 路由,可选附带
reasoningEffort:不写(或写 null)= 不发送该字段——GLM/Qwen flash 这类没有
思考模式的模型必须这样,否则宿主会拒绝请求;off = 显式关闭思考;high/max
= 思考强度。层级可以留空不配置;路由会落到最近的已配置层级(方向由成本模式决定)。
模型选择框中的 Auto 模式
插件注册了一个虚拟的 auto provider,模型选择框会出现 Auto Router 分组,
内含一个 Auto 模型:
Auto Router
└─ Auto
deepseek-official
├─ deepseek-v4-flash
└─ deepseek-v4-flash
- 选 Auto → 插件接管,按 L0–L3 层级自动路由。
- 选具体模型 → 插件原样放行,你手选的模型优先,路由不干预。
设置界面页签(浏览器端)
浏览器端会在 DSH 设置面板(齿轮图标 → 侧栏)注册一个 Auto Router
页签,展示当前路由策略——即 host 在会话开始时注入的
<mydsh-auto-model-router-status> 状态报告——并附提示文案:如需修改配置,
直接在对话中告诉 AI(例如"把 L2 改成 deepseek-v4-flash 且 max 思考"或
"成本模式切换为 cost-first"),AI 会帮你编辑 cordis.patch.yml,
重启 DSH 后生效。
在模型选择框选 Auto 立即生效,不再有确认弹窗。
无需额外构建步骤:client.js 是本仓库维护的自包含 ModuleLoader bundle
(pnpm run build:client 可从 client/ 源码重新生成)。
安装
dsh plugin --profile web add ./mydsh-auto-model-router
或通过已发布的 npm 包安装:
npm install @apinggithub/mydsh-auto-model-router
dsh plugin --profile web add @apinggithub/mydsh-auto-model-router
宿主兼容性
DSH 把 profile 的插件树当成一次组合:补丁失败的 bundle 可能让整个 Harness 起不来, 而抛错的插件条目会被标记为「did not activate」——路由器从会话里直接消失。 所以本插件的行为约定是能挂载、能降级、能打日志,而不是在接口变动时直接失败。
| 宿主版本线 | 状态 |
|---|---|
| 0.1.x – 0.3.x | 已实测(0.2.1-rc.2 为基准宿主) |
| 0.4.x – 1.x | 允许挂载,但在日志与 GET /api/mydsh-auto-model-router/status 中标记为「未实测」 |
挂载时 compat.mjs 会探测插件用到的每个接口(llm.registerAdapter、
llm.listModels、llm.stream、scoped ctx.inject、webServer.register、
可选的 tokenMeter),结果记录在状态接口的 host.compat 字段里。在未知宿主上:
tokenMeter不再是硬依赖——软查找,缺失时改用插件自带的估算器。- 没有
registerAdapter也能挂载——只记一条警告,模型选择框里不出现Auto,路由逻辑本身不受影响。 - HTTP 路由方法适配——
webServer.register()、route()、addRoute()都可接受;全都没有时设置页显示「暂无报告」,而不是报错。 - 事件负载容错——既接受当前的
{ agent }/(session, event)形状,也接受 裸 agent /{ session, event }形状;处理函数抛错时原样返回宿主自己的提案, 路由器的 bug 不会再让用户请求失败。 - 不再使用进程级全局变量——适配器按每次挂载创建并闭包引用该次挂载的 router,同进程多 profile 不会互相覆盖。
- 版本报告——当宿主愿意暴露版本(环境变量、app 服务或可解析的 manifest)时,
超出
>=0.1.0-0 <0.4.0会打一条同时含宿主版本与实测范围的警告。能力探测始终 执行,因此即使宿主隐藏版本,也能从状态接口看到它实际提供了哪些接口。
peer 范围(>=0.1.0-0 <1.0.0)和 engines.dsh 故意比实测范围宽,避免宿主升级后
直接拒装;真正的提示是运行时那条警告——看到它就请提 issue。
配置
在 profile 的 cordis.patch.yml 中添加:
- insert:
- id: mydsh-auto-model-router
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
modelRouter: true
config:
- id: mydsh-auto-model-router-runtime
name: '@apinggithub/mydsh-auto-model-router'
config:
# ── 四个层级(出厂默认)────────────────────────────────────
# reasoningEffort:off = 无思考,medium/high/max = 思考强度
levels:
L0: # 日常问答
provider: deepseek-official
model: deepseek-v4-flash
reasoningEffort: off
L1: # 代码与测试
provider: deepseek-official
model: deepseek-v4-flash
reasoningEffort: medium
L2: # 编写与审查
provider: deepseek-official
model: deepseek-v4-flash
reasoningEffort: high
L3: # 复合多步任务
provider: deepseek-official
model: deepseek-v4-flash
reasoningEffort: max
# 关键词规则仅在全自动模式生效,默认为空
rules: []
# ── 模式A:字符统计锁(零延迟)────────────────────────────
heuristic:
windowSize: 3 # 统计最近 N 次用户输入
counting: cjk2 # 中文计2字符/其他计1;可选 tokens
thresholds: # 宽度 → 层级(用户可改)
- maxChars: 10
level: L0
- maxChars: 100
level: L1 # 锁顶;超过 100 弹窗询问用户
# ── 模式C:关键词积分(全自动)────────────────────────────
scoring:
weights: # 每次命中积分(按层级)
L1: 1
L2: 2
L3: 3
bands: # 积分 → 层级(用户可改)
- maxScore: 0
level: L0
- maxScore: 5
level: L1
- maxScore: 15
level: L2
- maxScore: null
level: L3
# ── Tier 2:成本控制(用户可配置)─────────────────────────
costControl:
enabled: true
mode: balanced # cost-first | quality-first | balanced
defaultLevel: L1 # 未命中规则时使用的层级
tokenBudgetPerSession: 0 # 0 = 不设预算(默认);
# 设置正值即按会话封顶
# 会话预算耗尽时的行为:
# silent — 静默切到最便宜层级(不告知用户)
# notify — 注入用户可见消息说明降级原因
# ask — 弹窗询问;用户可选"继续用当前模型"
# (本会话豁免预算)
downgradeBehavior: notify
# ── LLM 分类器(全自动模式兜底;默认开启)───────────────────
llmClassifier:
enabled: true
model:
provider: deepseek-official
model: deepseek-v4-flash
requestTimeoutMs: 10000
# ── Tier 3:免费额度优先(可选)──────────────────────────
# 声明哪些路由是「免费额度」:优先走它们,额度用尽/报错自动换线。
freeQuota:
enabled: true # 没有任何免费声明时自动失效
routes: # 不在 levels 里的额外免费路由
- provider: glm
model: glm-4.7-flash
maxLevel: L1 # 这一档及以下的活可以交给它(含),默认 L1
reasoningEffort: off
demoteOnError: true # 该路由一报错即在本会话内降级到付费路由
notify: true # 降级时插入一次可见提示
# 固定层级模式每 N 次用户输入重新弹窗询问
askEveryInputs: 3
# ── Tier 4:失败降级链(模型级)───────────────────────────
fallbackChain:
- provider: deepseek-official
model: deepseek-v4-flash
maxRetries: 1
costControl 三种模式
| 模式 | 默认层级(未命中规则) | 模糊时偏移方向 |
|---|---|---|
cost-first |
最低已配置层级 | 向下(更便宜) |
quality-first |
最高已配置层级 | 向上(更强) |
balanced |
costControl.defaultLevel |
不动 |
规则命中永远优先于成本模式:用户显式匹配到的层级不会被成本层降级。
预算降级可见
tokenBudgetPerSession 默认为 0(不设预算)——成本层不会因预算耗尽而强制
降级,downgradeBehavior 此时不生效。设置一个正值即可开启按会话封顶:
- 例如
tokenBudgetPerSession: 300000:会话用满 30 万 token 后降级。 - 降级行为由
downgradeBehavior控制(默认notify):notify:注入一条用户可见消息,说明预算耗尽、剩余请求使用哪个模型、 如何调高上限。ask:弹窗询问"切到便宜模型还是继续用当前模型";选"继续"则本会话 豁免预算。silent:静默切换(不推荐——用户会疑惑回答质量为何变化)。
免费额度优先(freeQuota)
很多 provider 会送免费额度(例如智谱的 glm-4.7-flash 长期免费),但免费通道
同时意味着限流、额度耗尽、参数上限更严。这一层让「免费」变成可声明的路由属性,
并在它不可用时自动换线,而不是把整轮对话做废。
声明方式有两种,可以混用:
levels:
L0: { provider: glm, model: glm-4.7-flash, reasoningEffort: off, freeQuota: true }
L1: { provider: glm, model: glm-4.7-flash, reasoningEffort: low, freeQuota: true }
L2: { provider: deepseek-account, model: deepseek-flash, reasoningEffort: high }
L3: { provider: deepseek-account, model: deepseek-flash, reasoningEffort: max }
# 或者:层级仍指向付费模型,另外声明免费路由来「抢」这些档位的活
freeQuota:
routes:
- { provider: glm, model: glm-4.7-flash, maxLevel: L1, reasoningEffort: off }
行为:
- 优先——决策落在
maxLevel及以下的档位时,走声明的免费路由(免费路由 总是带着显式reasoningEffort,默认off,不会掉进适配器的默认思考档)。 - 失败即降级——免费路由上任何一次请求失败(额度耗尽、429 限流、鉴权被拒、 参数被拒……)都会把该路由在本会话内标记为已耗尽,请求立刻重试到下一跳 可用路由(通常就是该档位原本的付费路由)。
- 不再重试坏路由——降级后,fallbackChain 的游走会跳过所有已耗尽的免费路由, 同一个会话不会反复撞同一堵墙。
- 可见——
notify: true时插入一条说明「哪个免费路由、为什么、现在改用什么」 的消息,每个会话最多一条。 - 会话级——降级只在当前会话内生效,下个会话重新尝试免费路由;
freeQuota完全未声明时整层失效,老配置行为不变。
注意:免费的视觉模型通常有更小的输出上限(例如
glm-4v-flash只允许max_tokens ≤ 1024)。这类限制要在 provider 的models条目上写maxTokens修正,路由器无法替你猜——遇到INVALID_REQUEST: max_tokens参数非法时按提示 给该模型加maxTokens即可。
加载时询问(无需客户端界面)
安装或热插拔插件后的第一个会话,DSH 会:
- 注入一条状态汇报消息(当前层级→模型映射、成本模式、预算、降级链)。
语言跟随
settings.yaml的locale.preference(zh/en,默认en)。 - 弹窗询问三个问题(每轮插件加载只问一次):
| 问题 | 选项 |
|---|---|
| Q1 匹配规则 (T1) | 保持现有规则 / 查看现有规则(注入规则清单) |
| Q2 成本模式 (T2) | 保持当前 / cost-first / balanced / quality-first(选择立即生效) |
| Q3 其他配置 | 跳过 / 检查某项配置(输入 budget、fallbackChain、classifier 等,注入当前值) |
Q1/Q3 注入的清单与当前值让用户决定是否调整;持久修改仍落在
cordis.patch.yml(消息中会说明)。headless 或无可询问通道时自动降级为
只汇报、不询问,不会阻塞。
规则语法
- 纯文本:
match: 'refactor'—— 大小写不敏感的子串匹配。 - 正则字面量:
match: '/\\bdebug\\b/'—— 首尾斜杠界定正则,末尾可加标志(如/.../i)。 - 规则按顺序求值,第一个命中生效。
match可匹配用户消息文本、工具调用结果文本,或会话cwd(以cwd:<path>匹配)。
挂载点
| DSH 事件 | 作用 |
|---|---|
agent/session-start |
重置每会话路由状态 |
agent/pre-step |
token 记账(预算跟踪) |
system-prompt/assemble |
保持 {{model}} 提示词变量一致 |
agent/request |
路由决策点 —— 替换 provider/model |
agent/request-error |
降级链 → { kind: 'retry' } |
session/event |
从 assistant 消息统计 token 消耗 |
测试
npm test # node --test *.test.mjs
无需外部服务——所有层级均以 fixture 做单元测试。
已知问题
- 全自动模式延迟:开启 LLM 分类器后,每个新用户输入的第一步会增加一次分类器调用(LLM 请求)。虽然同一输入内的后续步骤已缓存、无额外开销,但多步任务的整体响应时间仍比直接选择模型慢。欢迎贡献优化方案——例如跨会话缓存分类结果,或用轻量启发式作为 LLM 分类的前置过滤。
配套插件:按档位拆账
tier-cost/ 里是本仓库附带的第二个插件 @apinggithub/dsh-tier-cost:
它把会话日志按路由档位折叠成 L0–L3 的 token 与花费(金额向计费插件
@kenz1117/dsh-ui-usage-billing 借 per-turn 数据,本身不维护价目表),
在设置页多出一栏「档位花费」——因为四个档位常常共用同一个模型,计费面板
按模型分账时看不出是哪一档花的钱。
dsh plugin --profile desktop add ./tier-cost
License
MIT
上下文窗口守卫(contextGuard)
档位可声明其模型的 contextWindow。会话实际上下文(最近一次请求的输入侧
token,含缓存读写)超过 window × headroom(默认 0.85)时,该档位被跳过,
路由向上漂到最近的装得下的档位;没有任何声明窗口装得下时保持原档(fail-open),
让 provider 用真实错误说话、交给降级链处理。跳过每 (档位, 会话) 记录一次,
/status 的 contextGuard 字段可见。未声明任何窗口时整层不生效。
levels:
L0: { provider: bailian, model: qwen-max, contextWindow: 32768 } # 小窗口模型的保护
contextGuard: { enabled: true, headroom: 0.85 }
部署到其它电脑
一台机器 = 三条命令(本仓库含两个插件):
# 1. 从 npm 装(推荐;peer 范围已放宽为宿主自适应,不需要 allow-version 豁免)
dsh plugin --profile desktop add '@apinggithub/[email protected]'
# 2. 可选:按档位拆账
dsh plugin --profile desktop add '@apinggithub/[email protected]'
# 3. 改完 cordis.patch.yml 后先校验(见下),再重启
node scripts/check-patch.mjs --profile desktop
# 4. 重启 DSH(bundle 只在启动时组合;配置改动用热加载即可)
改配置前必读:先校验,再重启
cordis.patch.yml 里一个少的换行就能让宿主启动失败:
DesktopHostFatalError: failed to parse overlay … YAMLException: bad indentation (454:44)
更麻烦的是崩溃对话框里有 "Disable third-party plugins" 按钮——点它会把所有第三方 bundle 从 profile 里摘掉,插件(含 Auto 入口)随之消失,需要手工恢复。
所以每次改完都跑一次(会用宿主自己的解析器校验,并清理临时 profile):
node scripts/check-patch.mjs --profile desktop
# PASS profile "desktop" composes (194 top-level entries)
# FAIL … 会打印宿主的原始错误
参数:--home <DSH_HOME>、--profile <name>、--file <patch>(只做静态检查)、--keep(保留临时 profile 便于排查)。
也可以从源码装:git clone … 后 dsh plugin --profile desktop add ./mydsh-auto-model-router。
每台机器必须自带档位块
插件的出厂默认档位是历史值(deepseek-official/deepseek-flash),这台机器上不一定可用。
装完在该机器 profiles/<profile>/cordis.patch.yml 末尾写自己的档位块,
provider/model 必须是那条机器真实可用的路由,别照抄 id:
- id: mydsh-auto-model-router-runtime
name: '@apinggithub/mydsh-auto-model-router'
config:
levels:
# reasoningEffort: null = 该模型没有思考能力(Qwen/GLM flash 都是),必须写 null,
# 否则宿主会以 UNSUPPORTED_REASONING_EFFORT 拒绝请求。
L0: { provider: zhipu, model: glm-4.7-flash, reasoningEffort: null, freeQuota: true }
L1: { provider: zhipu, model: glm-4.5-flash, reasoningEffort: null, freeQuota: true }
L2: { provider: zhipu, model: glm-4-flash, reasoningEffort: null }
L3: { provider: deepseek-official, model: deepseek-flash, reasoningEffort: max }
freeQuota: { enabled: true, demoteOnError: true, notify: true }
contextGuard: { enabled: true, headroom: 0.85 }
quotaWatch: { enabled: true, provider: bailian-dell-vostro, balanceFloorCny: 10 }
provider 本身要在 cordis.patch.yml 里先定义好(llm-pi-ai 的 providers: 段),
密钥通过 DSH 凭据库或 apiKeyEnv 提供。
多机同账号的注意点:额度与余额都在服务端,所以本机累计的 token 数不参与判断——
quotaWatch 读的是云端账单/余额。但每台机器都要各自配置一次(凭据库不跨机同步)。
额度看门狗(quotaWatch)
它解决什么问题
免费额度耗尽后,服务商未必报错——可能静默转按量计费。那时 demoteOnError 无错可捕,
路由会一直烧钱(见文末「静默计费陷阱」)。看门狗改读账单,一旦某个"免费"模型出现花费,
30 分钟内把它摘掉。
它能看见什么
| 数据源 | 能拿到 | 限制 |
|---|---|---|
QueryAccountBalance |
账户可用余额(¥) | 准实时 |
DescribeInstanceBill |
按模型拆分的当期花费 | T+1 延迟 |
QueryResourcePackageInstances |
资源包余量 | 无资源包时为空 |
DeepSeek GET /user/balance |
开放平台余额 | 准实时 |
没有"查询免费额度剩余"的公开接口——那个进度条只在服务商控制台里。 所以看门狗的语义是发现即降级,不是提前拦截;真正的超前拦截仍要靠服务商侧的「用完即停」。
配置
quotaWatch:
enabled: true
provider: bailian-dell-vostro # 账单描述的是哪个已配置 provider(要摘的路由)
productCode: sfm # 百炼在阿里云账单里的商品码
pollMs: 1800000 # 轮询间隔,最小 60000
balanceFloorCny: 10 # 阿里云余额低于此值 → 暂停所有付费档;0 = 关闭
demoteOnSpend: true # 某模型出现花费 → 摘掉它的路由
deepseekBalanceFloor: 10 # DeepSeek 平台余额低于此值 → 只暂停 deepseek-official;0 = 关闭
deepseekApiKeyRef: DEEPSEEK_API_KEY
accessKeyIdRef: ALIBABA_CLOUD_ACCESS_KEY_ID
accessKeySecretRef: ALIBABA_CLOUD_ACCESS_KEY_SECRET
timeoutMs: 20000
阿里云 RAM 子账号怎么开(重要)
不要用主账号 AccessKey。 正确的做法:
- 阿里云控制台 → RAM 访问控制 → 用户 → 创建用户,勾选只允许编程访问;
- 给它只读账单权限:直接附加系统策略
AliyunBSSReadOnlyAccess; 想更小可用自定义策略,只允许这几个 Action:bss:QueryAccountBalance、bss:DescribeInstanceBill、bss:QueryResourcePackageInstances; - 创建 AccessKey,把 ID/Secret 存进 DSH 凭据库(见下),不要再给它任何写权限;
- 验证(只读,不花钱):
$ak = (Resolve the ID); $sk = (Resolve the secret)
# 期望返回 {"Success":true,"Data":{"AvailableAmount":"..."}}
AccessKey Secret 不要写进
cordis.patch.yml,也不要提交进仓库——配置文件里只有accessKeyIdRef/accessKeySecretRef这样的引用名。
凭据放哪里
看门狗通过 ctx.credentials.resolve(ref) 读取,所以密钥存在 DSH 凭据库
($DSH_HOME/.credentials.yaml,与 provider 的 API key 同一个文件),形如:
refs:
ALIBABA_CLOUD_ACCESS_KEY_ID: LTAI...
ALIBABA_CLOUD_ACCESS_KEY_SECRET: ...
DEEPSEEK_API_KEY: sk-...
两种写入方式,任选:
- GUI:插件 → 内置插件设置 → Models / 凭据,按 API key 的方式存(键名用上面的 ref 名);
- 命令行(secret 不进 shell 历史,也不进对话):
$sec = Read-Host -AsSecureString 'Paste AccessKey Secret'
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec))
# 写入 $DSH_HOME/.credentials.yaml 的 refs 段后,立即删除任何临时明文文件
⚠️ 多机部署:每台机器都要各自配置一次凭据(凭据库不跨机同步)。 额度/余额在服务端,所以多机读到的数字一致,不存在方案 A 那种"各机各算、互不知情"的偏差。
状态接口
GET /api/mydsh-auto-model-router/status 的 quotaWatch 字段:
{
"enabled": true,
"lastCheckAt": "2026-10-22T09:57:18Z",
"lastError": null,
"balanceCny": 28.21,
"cycle": "2026-10",
"spendByModel": { "glm-5.3": 62.5611, "qwen3.8-flash": 8.0478 },
"totalCny": 70.67,
"exhausted": [],
"balanceLow": false,
"deepseekBalanceCny": 158.25,
"deepseekBlocked": false,
"deepseekError": null
}
任何一环失败都只写进 lastError / deepseekError,不会影响路由本身。
发布到 npm
目录新鲜度守卫(modelsGuard)
模型版本升级后,provider 不再公布的 id 会变成「过期路由」。守卫在激活时与每
refreshMs(默认 10 分钟)用 ctx.llm.listModels(provider) 读取各 provider
实际公布的模型:过期档位的请求自动漂移到最近的鲜活档位(绝不静默打死),
状态报告列出过期档位与同族候选 id,改档位仍是一行编辑的事。目录读不到或
为空时一律 fail-open(不判过期)。未注册 listModels 的环境整层惰性。
modelsGuard: { enabled: true, refreshMs: 600000 }
静态列表本身仍是配置(
cordis.patch.yml)——这是刻意的:插件不会替你决定 换成哪个模型(窗口/单价/能力都变了),它只保证坏档位不再被选中并告诉你该换什么。
发布到 npm
两个插件同一仓库、各自独立发布(root 与本目录的 package.json 都可直接 publish):
npm publish # @apinggithub/mydsh-auto-model-router(root)
cd tier-cost && npm publish # @apinggithub/dsh-tier-cost
0.2.0 起 peerDependencies 已放宽为宿主自适应范围,新机器安装不再需要
allow-version 豁免(豁免仅 0.1.1 及更早需要)。发布前各自 npm login --scope=apinggithub。
免费额度的静默计费陷阱(真实事故 · 守则)
本仓库作者机器上的实录事故:L1 指向百炼免费模型 glm-5.3(freeQuota: true),额度耗尽后
服务商没有按「用完即停」返回 403(批量开关未覆盖到该模型/生效延迟),调用照常成功——
于是 demoteOnError 无错可捕,glm-5.3 静默转按量计费跑了一整天,直到账单通知才被发现。
三条守则,缺一不可:
- 先在服务商侧逐模型确认「用完即停」真的生效(发一条超额调用看是否返回 403,别信批量开关)。 路由器只能在看得见错误时换线;额度耗尽若被服务商静默转按量,插件无从得知。
costControl.defaultLevel指向稳定付费档(本文各处示例的defaultLevel: L1若 L1 是免费档, 正是本次事故形态)。免费档只放最低层(L0),或至少每个免费档都有明确的付费替代。- 用计费面板按档核账:任何"免费"档的费用行持续大于 0 且不归零,立刻怀疑额度已尽、 在按量扣费——把该档从免费路由摘下来。
另一个易错点:免费额度池按 provider+model 独立计数。同一个模型 id 出现在多个 provider (如
qwen3.8-flash同时在 bailian 与 qianwen)是不同的池子,额度与计费互不相干。