@apinggithub/mydsh-auto-model-router 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,6 +116,48 @@ npm install @apinggithub/mydsh-auto-model-router
116
116
  dsh plugin --profile web add @apinggithub/mydsh-auto-model-router
117
117
  ```
118
118
 
119
+ ## Host compatibility
120
+
121
+ DSH treats a profile's plugin tree as one composition: a bundle whose patch
122
+ fails can stop the whole Harness from starting, and a plugin entry that throws
123
+ is reported as *did not activate* — the router simply disappears from the
124
+ running session. This plugin is therefore written to **mount, degrade and
125
+ log** rather than fail on a host whose surface moved.
126
+
127
+ | Host line | Status |
128
+ | --- | --- |
129
+ | 0.1.x – 0.3.x | Tested (`0.2.1-rc.2` is the reference host) |
130
+ | 0.4.x – 1.x | Allowed to mount, reported as *untested* in the log and in `GET /api/mydsh-auto-model-router/status` |
131
+
132
+ At mount, `compat.mjs` probes every API the plugin touches
133
+ (`llm.registerAdapter`, `llm.listModels`, `llm.stream`, scoped `ctx.inject`,
134
+ `webServer.register`, the optional `tokenMeter`) and records the result in the
135
+ status payload under `host.compat`. What that buys you on an unknown host:
136
+
137
+ - **No hard dependency on `tokenMeter`** — it is looked up softly and token
138
+ spend falls back to the plugin's own estimator.
139
+ - **No virtual-provider requirement** — without `llm.registerAdapter` the
140
+ plugin still mounts and logs one warning; the `Auto` model simply does not
141
+ appear in the picker.
142
+ - **HTTP route aliases** — `webServer.register()`, `route()` or `addRoute()`
143
+ are all accepted; without any of them the settings page shows its "no report
144
+ yet" state instead of breaking.
145
+ - **Payload tolerance** — event handlers accept the current `{ agent }` /
146
+ `(session, event)` shapes *and* a bare-agent / `{ session, event }` payload,
147
+ and a handler that throws hands the host's own proposal back untouched. A
148
+ router bug can no longer fail a user request.
149
+ - **No process globals** — the adapter is created per mount and closes over
150
+ that mount's router, so several profiles in one process cannot overwrite
151
+ each other.
152
+ - **Version report** — when the host exposes its version (environment, app
153
+ service, or a resolvable manifest), a host outside `>=0.1.0-0 <0.4.0` logs
154
+ one warning naming both versions. Capability probes run regardless, so a
155
+ host that hides its version still reports what it actually provides.
156
+
157
+ The peer ranges (`>=0.1.0-0 <1.0.0`) and `engines.dsh` are deliberately wider
158
+ than the tested range so an upgrade does not hard-block installation; the
159
+ runtime warning is what tells you to file an issue.
160
+
119
161
  ## Configuration
120
162
 
121
163
  Add to your profile's `cordis.patch.yml`:
@@ -370,88 +412,111 @@ See [tier-cost/README.md](tier-cost/README.md) for details.
370
412
  ## License
371
413
 
372
414
  MIT
373
-
374
- ### Context-window guard (contextGuard)
375
-
376
- A level may declare its model's `contextWindow`. When the session's live context
377
- (the input side of the last request, including cache reads/writes) exceeds
378
- `window × headroom` (default 0.85) the level is **skipped** and the router
379
- drifts up to the nearest level that fits; if no declared window fits, the
380
- original level stands (fail open) so the provider answers with a real error the
381
- fallback chain can handle. Each skip is recorded once per (level, session) and
382
- visible in `/status` under `contextGuard`. With no window declared anywhere the
383
- layer is inert.
384
-
385
- ```yaml
386
- levels:
387
- L0: { provider: bailian, model: qwen-max, contextWindow: 32768 } # guard small windows
388
- contextGuard: { enabled: true, headroom: 0.85 }
415
+
416
+ ### Context-window guard (contextGuard)
417
+
418
+ A level may declare its model's `contextWindow`. When the session's live context
419
+ (the input side of the last request, including cache reads/writes) exceeds
420
+ `window × headroom` (default 0.85) the level is **skipped** and the router
421
+ drifts up to the nearest level that fits; if no declared window fits, the
422
+ original level stands (fail open) so the provider answers with a real error the
423
+ fallback chain can handle. Each skip is recorded once per (level, session) and
424
+ visible in `/status` under `contextGuard`. With no window declared anywhere the
425
+ layer is inert.
426
+
427
+ ```yaml
428
+ levels:
429
+ L0: { provider: bailian, model: qwen-max, contextWindow: 32768 } # guard small windows
430
+ contextGuard: { enabled: true, headroom: 0.85 }
389
431
  ```
390
-
391
- ## Deploying to another machine
392
-
393
- One machine = two commands (this repository ships both plugins):
394
-
395
- ```powershell
396
- git clone https://github.com/apinggithub/mydsh-auto-model-router
397
- cd mydsh-auto-model-router; cd ..
398
- # On 0.2.x hosts the plugin's stale pinned peers make the installer refuse; grant the
399
- # exact-version exemption first (per new plugin version):
400
- dsh plugin --profile desktop allow-version '@apinggithub/mydsh-auto-model-router@0.1.1' --dsh-version 0.2.0-rc.2 --accept-risk
401
- dsh plugin --profile desktop add ./mydsh-auto-model-router
402
- dsh plugin --profile desktop add ./mydsh-auto-model-router/tier-cost # optional: per-tier cost panel
403
- ```
404
-
405
- Then write a full levels block into that machine's `profiles/<profile>/cordis.patch.yml` —
406
- provider/model must be routes **that machine** can actually use (do not copy ids across machines):
407
-
408
- ```yaml
409
- - id: mydsh-auto-model-router-runtime
410
- name: '@apinggithub/mydsh-auto-model-router'
411
- config:
412
- levels:
413
- L0: { provider: ..., model: ..., reasoningEffort: off, contextWindow: ..., freeQuota: true }
414
- L1: { provider: ..., model: ..., reasoningEffort: low, contextWindow: ..., freeQuota: true }
415
- L2: { provider: ..., model: ..., reasoningEffort: high, contextWindow: ..., freeQuota: true }
416
- L3: { provider: ..., model: ..., reasoningEffort: max, contextWindow: ... }
417
- contextGuard: { enabled: true, headroom: 0.85 }
418
- freeQuota: { enabled: true, demoteOnError: true, notify: true }
419
- ```
420
-
421
- ⚠️ The built-in default levels carry legacy values (`deepseek-v4-flash` + `medium`) that are
422
- both invalid on 0.2.x hosts — a fresh machine must ship its own levels block or Auto fails
423
- outright. `tier-cost` has zero hard dependencies beyond the host: without the router every
424
- turn folds into manual/unknown, without the billing plugin tokens are counted and cost
432
+
433
+ ## Deploying to another machine
434
+
435
+ One machine = two commands (this repository ships both plugins):
436
+
437
+ ```powershell
438
+ git clone https://github.com/apinggithub/mydsh-auto-model-router
439
+ cd mydsh-auto-model-router; cd ..
440
+ # On 0.2.x hosts the plugin's stale pinned peers make the installer refuse; grant the
441
+ # exact-version exemption first (per new plugin version):
442
+ dsh plugin --profile desktop allow-version '@apinggithub/mydsh-auto-model-router@0.1.1' --dsh-version 0.2.0-rc.2 --accept-risk
443
+ dsh plugin --profile desktop add ./mydsh-auto-model-router
444
+ dsh plugin --profile desktop add ./mydsh-auto-model-router/tier-cost # optional: per-tier cost panel
445
+ ```
446
+
447
+ Then write a full levels block into that machine's `profiles/<profile>/cordis.patch.yml` —
448
+ provider/model must be routes **that machine** can actually use (do not copy ids across machines):
449
+
450
+ ```yaml
451
+ - id: mydsh-auto-model-router-runtime
452
+ name: '@apinggithub/mydsh-auto-model-router'
453
+ config:
454
+ levels:
455
+ L0: { provider: ..., model: ..., reasoningEffort: off, contextWindow: ..., freeQuota: true }
456
+ L1: { provider: ..., model: ..., reasoningEffort: low, contextWindow: ..., freeQuota: true }
457
+ L2: { provider: ..., model: ..., reasoningEffort: high, contextWindow: ..., freeQuota: true }
458
+ L3: { provider: ..., model: ..., reasoningEffort: max, contextWindow: ... }
459
+ contextGuard: { enabled: true, headroom: 0.85 }
460
+ freeQuota: { enabled: true, demoteOnError: true, notify: true }
461
+ ```
462
+
463
+ ⚠️ The built-in default levels carry legacy values (`deepseek-v4-flash` + `medium`) that are
464
+ both invalid on 0.2.x hosts — a fresh machine must ship its own levels block or Auto fails
465
+ outright. `tier-cost` has zero hard dependencies beyond the host: without the router every
466
+ turn folds into manual/unknown, without the billing plugin tokens are counted and cost
425
467
  stays 0 — both cases self-report in the panel notes.
426
-
427
- ### Catalog freshness guard (modelsGuard)
428
-
429
- When a provider retires a model id, the level pointing at it goes stale. The
430
- guard reads what each provider actually advertises (`ctx.llm.listModels`) at
431
- activation and every `refreshMs` (default 10 min): a stale level's requests
432
- **drift to the nearest fresh level** instead of failing, and the status report
433
- names same-family candidate ids so the fix stays a one-line edit. Unreadable or
434
- empty catalogs fail open (never marked stale); environments without a catalog
435
- reader leave the layer inert.
436
-
437
- ```yaml
438
- modelsGuard: { enabled: true, refreshMs: 600000 }
439
- ```
440
-
441
- > The static list remains configuration (`cordis.patch.yml`) on purpose: the
442
- > plugin never decides *which* new model to adopt (windows, prices and
443
- > capabilities all change) — it guarantees a dead tier is not selected and
444
- > tells you what to switch to.
445
-
446
- ## Publishing to npm
447
-
448
- Both plugins live in this repository and publish independently:
449
-
450
- ```bash
451
- npm publish # @apinggithub/mydsh-auto-model-router (root)
452
- cd tier-cost && npm publish # @apinggithub/dsh-tier-cost
453
- ```
454
-
455
- From 0.2.0 the peerDependencies are host-adaptive ranges, so fresh machines no
456
- longer need the allow-version exemption (only 0.1.1 and earlier did). Run
468
+
469
+ ### Catalog freshness guard (modelsGuard)
470
+
471
+ When a provider retires a model id, the level pointing at it goes stale. The
472
+ guard reads what each provider actually advertises (`ctx.llm.listModels`) at
473
+ activation and every `refreshMs` (default 10 min): a stale level's requests
474
+ **drift to the nearest fresh level** instead of failing, and the status report
475
+ names same-family candidate ids so the fix stays a one-line edit. Unreadable or
476
+ empty catalogs fail open (never marked stale); environments without a catalog
477
+ reader leave the layer inert.
478
+
479
+ ```yaml
480
+ modelsGuard: { enabled: true, refreshMs: 600000 }
481
+ ```
482
+
483
+ > The static list remains configuration (`cordis.patch.yml`) on purpose: the
484
+ > plugin never decides *which* new model to adopt (windows, prices and
485
+ > capabilities all change) — it guarantees a dead tier is not selected and
486
+ > tells you what to switch to.
487
+
488
+ ## Publishing to npm
489
+
490
+ Both plugins live in this repository and publish independently:
491
+
492
+ ```bash
493
+ npm publish # @apinggithub/mydsh-auto-model-router (root)
494
+ cd tier-cost && npm publish # @apinggithub/dsh-tier-cost
495
+ ```
496
+
497
+ From 0.2.0 the peerDependencies are host-adaptive ranges, so fresh machines no
498
+ longer need the allow-version exemption (only 0.1.1 and earlier did). Run
457
499
  `npm login --scope=apinggithub` once per package account setup.
500
+
501
+ ## The silent-billing trap of free quota (real incident · rules)
502
+
503
+ A true story from the author's machine: L1 pointed at the Bailian free model `glm-5.3`
504
+ (`freeQuota: true`). When the quota ran out the provider did **not** return the promised
505
+ 403 (`stop-when-exhausted` had not actually taken effect for that model) — calls kept
506
+ succeeding, now silently at pay-as-you-go rates, for a full day until a billing alert
507
+ arrived. `demoteOnError` had no error to catch.
508
+
509
+ Three rules, all required:
510
+
511
+ 1. **Verify "stop when exhausted" per model on the provider side** (make one over-quota
512
+ call and confirm a 403). The router can only switch lines on errors it can see;
513
+ a provider that silently converts a free route into paid billing is invisible to it.
514
+ 2. **Keep `costControl.defaultLevel` on a stable paid tier.** In this file's examples a
515
+ `defaultLevel: L1` where L1 is a free tier is exactly the incident shape. Prefer free
516
+ only at L0, or keep an explicit paid replacement for every free tier.
517
+ 3. **Watch the billing panel per tier.** A "free" tier whose cost line keeps growing and
518
+ never zeroes is burning money — drop it from the free routes immediately.
519
+
520
+ > Another sharp edge: quota pools are counted per **provider+model**. The same id on two
521
+ > providers (e.g. `qwen3.8-flash` on both `bailian` and `qianwen`) are **separate pools**;
522
+ > quota and billing never cross over.
package/README.zh.md CHANGED
@@ -99,6 +99,38 @@ npm install @apinggithub/mydsh-auto-model-router
99
99
  dsh plugin --profile web add @apinggithub/mydsh-auto-model-router
100
100
  ```
101
101
 
102
+ ## 宿主兼容性
103
+
104
+ DSH 把 profile 的插件树当成一次组合:补丁失败的 bundle 可能让整个 Harness 起不来,
105
+ 而抛错的插件条目会被标记为「did not activate」——路由器从会话里直接消失。
106
+ 所以本插件的行为约定是**能挂载、能降级、能打日志**,而不是在接口变动时直接失败。
107
+
108
+ | 宿主版本线 | 状态 |
109
+ | --- | --- |
110
+ | 0.1.x – 0.3.x | 已实测(`0.2.1-rc.2` 为基准宿主) |
111
+ | 0.4.x – 1.x | 允许挂载,但在日志与 `GET /api/mydsh-auto-model-router/status` 中标记为「未实测」 |
112
+
113
+ 挂载时 `compat.mjs` 会探测插件用到的每个接口(`llm.registerAdapter`、
114
+ `llm.listModels`、`llm.stream`、scoped `ctx.inject`、`webServer.register`、
115
+ 可选的 `tokenMeter`),结果记录在状态接口的 `host.compat` 字段里。在未知宿主上:
116
+
117
+ - **`tokenMeter` 不再是硬依赖**——软查找,缺失时改用插件自带的估算器。
118
+ - **没有 `registerAdapter` 也能挂载**——只记一条警告,模型选择框里不出现
119
+ `Auto`,路由逻辑本身不受影响。
120
+ - **HTTP 路由方法适配**——`webServer.register()`、`route()`、`addRoute()`
121
+ 都可接受;全都没有时设置页显示「暂无报告」,而不是报错。
122
+ - **事件负载容错**——既接受当前的 `{ agent }` / `(session, event)` 形状,也接受
123
+ 裸 agent / `{ session, event }` 形状;处理函数抛错时原样返回宿主自己的提案,
124
+ 路由器的 bug 不会再让用户请求失败。
125
+ - **不再使用进程级全局变量**——适配器按每次挂载创建并闭包引用该次挂载的
126
+ router,同进程多 profile 不会互相覆盖。
127
+ - **版本报告**——当宿主愿意暴露版本(环境变量、app 服务或可解析的 manifest)时,
128
+ 超出 `>=0.1.0-0 <0.4.0` 会打一条同时含宿主版本与实测范围的警告。能力探测始终
129
+ 执行,因此即使宿主隐藏版本,也能从状态接口看到它实际提供了哪些接口。
130
+
131
+ peer 范围(`>=0.1.0-0 <1.0.0`)和 `engines.dsh` 故意比实测范围宽,避免宿主升级后
132
+ 直接拒装;真正的提示是运行时那条警告——看到它就请提 issue。
133
+
102
134
  ## 配置
103
135
 
104
136
  在 profile 的 `cordis.patch.yml` 中添加:
@@ -336,78 +368,96 @@ dsh plugin --profile desktop add ./tier-cost
336
368
  ## License
337
369
 
338
370
  MIT
339
-
340
- ### 上下文窗口守卫(contextGuard)
341
-
342
- 档位可声明其模型的 `contextWindow`。会话实际上下文(最近一次请求的输入侧
343
- token,含缓存读写)超过 `window × headroom`(默认 0.85)时,**该档位被跳过**,
344
- 路由向上漂到最近的装得下的档位;没有任何声明窗口装得下时保持原档(fail-open),
345
- 让 provider 用真实错误说话、交给降级链处理。跳过每 (档位, 会话) 记录一次,
346
- `/status` 的 `contextGuard` 字段可见。未声明任何窗口时整层不生效。
347
-
348
- ```yaml
349
- levels:
350
- L0: { provider: bailian, model: qwen-max, contextWindow: 32768 } # 小窗口模型的保护
351
- contextGuard: { enabled: true, headroom: 0.85 }
371
+
372
+ ### 上下文窗口守卫(contextGuard)
373
+
374
+ 档位可声明其模型的 `contextWindow`。会话实际上下文(最近一次请求的输入侧
375
+ token,含缓存读写)超过 `window × headroom`(默认 0.85)时,**该档位被跳过**,
376
+ 路由向上漂到最近的装得下的档位;没有任何声明窗口装得下时保持原档(fail-open),
377
+ 让 provider 用真实错误说话、交给降级链处理。跳过每 (档位, 会话) 记录一次,
378
+ `/status` 的 `contextGuard` 字段可见。未声明任何窗口时整层不生效。
379
+
380
+ ```yaml
381
+ levels:
382
+ L0: { provider: bailian, model: qwen-max, contextWindow: 32768 } # 小窗口模型的保护
383
+ contextGuard: { enabled: true, headroom: 0.85 }
352
384
  ```
353
-
354
- ## 部署到其它电脑
355
-
356
- 一台机器 = 两条命令(本仓库含两个插件):
357
-
358
- ```powershell
359
- git clone https://github.com/apinggithub/mydsh-auto-model-router
360
- cd mydsh-auto-model-router; cd ..
361
- # 宿主 0.2.x 会因插件 peer 钉死旧版本而拒装,先授予精确版本豁免(每个新版本各授一次):
362
- dsh plugin --profile desktop allow-version '@apinggithub/mydsh-auto-model-router@0.1.1' --dsh-version 0.2.0-rc.2 --accept-risk
363
- dsh plugin --profile desktop add ./mydsh-auto-model-router
364
- dsh plugin --profile desktop add ./mydsh-auto-model-router/tier-cost # 可选:按档位拆账
365
- ```
366
-
367
- 然后在该机器的 `profiles/<profile>/cordis.patch.yml` 里写完整档位块,**provider/model 必须
368
- 是那条机器自己能用的路由,别照抄 id**:
369
-
370
- ```yaml
371
- - id: mydsh-auto-model-router-runtime
372
- name: '@apinggithub/mydsh-auto-model-router'
373
- config:
374
- levels:
375
- L0: { provider: ..., model: ..., reasoningEffort: off, contextWindow: ..., freeQuota: true }
376
- L1: { provider: ..., model: ..., reasoningEffort: low, contextWindow: ..., freeQuota: true }
377
- L2: { provider: ..., model: ..., reasoningEffort: high, contextWindow: ..., freeQuota: true }
378
- L3: { provider: ..., model: ..., reasoningEffort: max, contextWindow: ... }
379
- contextGuard: { enabled: true, headroom: 0.85 }
380
- freeQuota: { enabled: true, demoteOnError: true, notify: true }
381
- ```
382
-
383
- ⚠️ 内置默认 levels 是历史值(`deepseek-v4-flash` + `medium`),在 0.2.x 宿主上**两者都不是
384
- 合法值**——新机器必须自带档位块,否则选 Auto 会直接失败。
385
- `tier-cost` 对宿主其余部分零依赖:缺路由插件时全部归 manual/unknown,缺计费插件时只统计
385
+
386
+ ## 部署到其它电脑
387
+
388
+ 一台机器 = 两条命令(本仓库含两个插件):
389
+
390
+ ```powershell
391
+ git clone https://github.com/apinggithub/mydsh-auto-model-router
392
+ cd mydsh-auto-model-router; cd ..
393
+ # 宿主 0.2.x 会因插件 peer 钉死旧版本而拒装,先授予精确版本豁免(每个新版本各授一次):
394
+ dsh plugin --profile desktop allow-version '@apinggithub/mydsh-auto-model-router@0.1.1' --dsh-version 0.2.0-rc.2 --accept-risk
395
+ dsh plugin --profile desktop add ./mydsh-auto-model-router
396
+ dsh plugin --profile desktop add ./mydsh-auto-model-router/tier-cost # 可选:按档位拆账
397
+ ```
398
+
399
+ 然后在该机器的 `profiles/<profile>/cordis.patch.yml` 里写完整档位块,**provider/model 必须
400
+ 是那条机器自己能用的路由,别照抄 id**:
401
+
402
+ ```yaml
403
+ - id: mydsh-auto-model-router-runtime
404
+ name: '@apinggithub/mydsh-auto-model-router'
405
+ config:
406
+ levels:
407
+ L0: { provider: ..., model: ..., reasoningEffort: off, contextWindow: ..., freeQuota: true }
408
+ L1: { provider: ..., model: ..., reasoningEffort: low, contextWindow: ..., freeQuota: true }
409
+ L2: { provider: ..., model: ..., reasoningEffort: high, contextWindow: ..., freeQuota: true }
410
+ L3: { provider: ..., model: ..., reasoningEffort: max, contextWindow: ... }
411
+ contextGuard: { enabled: true, headroom: 0.85 }
412
+ freeQuota: { enabled: true, demoteOnError: true, notify: true }
413
+ ```
414
+
415
+ ⚠️ 内置默认 levels 是历史值(`deepseek-v4-flash` + `medium`),在 0.2.x 宿主上**两者都不是
416
+ 合法值**——新机器必须自带档位块,否则选 Auto 会直接失败。
417
+ `tier-cost` 对宿主其余部分零依赖:缺路由插件时全部归 manual/unknown,缺计费插件时只统计
386
418
  token、费用记 0,均会在面板 notes 里说明。
387
-
388
- ### 目录新鲜度守卫(modelsGuard)
389
-
390
- 模型版本升级后,provider 不再公布的 id 会变成「过期路由」。守卫在激活时与每
391
- `refreshMs`(默认 10 分钟)用 `ctx.llm.listModels(provider)` 读取各 provider
392
- 实际公布的模型:过期档位的请求**自动漂移到最近的鲜活档位**(绝不静默打死),
393
- 状态报告列出过期档位与**同族候选 id**,改档位仍是一行编辑的事。目录读不到或
394
- 为空时一律 fail-open(不判过期)。未注册 listModels 的环境整层惰性。
395
-
396
- ```yaml
397
- modelsGuard: { enabled: true, refreshMs: 600000 }
398
- ```
399
-
400
- > 静态列表本身仍是配置(`cordis.patch.yml`)——这是刻意的:插件不会替你决定
401
- > 换成哪个模型(窗口/单价/能力都变了),它只保证**坏档位不再被选中**并告诉你该换什么。
402
-
403
- ## 发布到 npm
404
-
405
- 两个插件同一仓库、各自独立发布(root 与本目录的 package.json 都可直接 publish):
406
-
407
- ```bash
408
- npm publish # @apinggithub/mydsh-auto-model-router(root)
409
- cd tier-cost && npm publish # @apinggithub/dsh-tier-cost
410
- ```
411
-
412
- 0.2.0 起 peerDependencies 已放宽为宿主自适应范围,新机器安装**不再需要**
419
+
420
+ ### 目录新鲜度守卫(modelsGuard)
421
+
422
+ 模型版本升级后,provider 不再公布的 id 会变成「过期路由」。守卫在激活时与每
423
+ `refreshMs`(默认 10 分钟)用 `ctx.llm.listModels(provider)` 读取各 provider
424
+ 实际公布的模型:过期档位的请求**自动漂移到最近的鲜活档位**(绝不静默打死),
425
+ 状态报告列出过期档位与**同族候选 id**,改档位仍是一行编辑的事。目录读不到或
426
+ 为空时一律 fail-open(不判过期)。未注册 listModels 的环境整层惰性。
427
+
428
+ ```yaml
429
+ modelsGuard: { enabled: true, refreshMs: 600000 }
430
+ ```
431
+
432
+ > 静态列表本身仍是配置(`cordis.patch.yml`)——这是刻意的:插件不会替你决定
433
+ > 换成哪个模型(窗口/单价/能力都变了),它只保证**坏档位不再被选中**并告诉你该换什么。
434
+
435
+ ## 发布到 npm
436
+
437
+ 两个插件同一仓库、各自独立发布(root 与本目录的 package.json 都可直接 publish):
438
+
439
+ ```bash
440
+ npm publish # @apinggithub/mydsh-auto-model-router(root)
441
+ cd tier-cost && npm publish # @apinggithub/dsh-tier-cost
442
+ ```
443
+
444
+ 0.2.0 起 peerDependencies 已放宽为宿主自适应范围,新机器安装**不再需要**
413
445
  allow-version 豁免(豁免仅 0.1.1 及更早需要)。发布前各自 `npm login --scope=apinggithub`。
446
+
447
+ ## 免费额度的静默计费陷阱(真实事故 · 守则)
448
+
449
+ 本仓库作者机器上的实录事故:L1 指向百炼免费模型 `glm-5.3`(`freeQuota: true`),额度耗尽后
450
+ 服务商**没有按「用完即停」返回 403**(批量开关未覆盖到该模型/生效延迟),调用照常成功——
451
+ 于是 `demoteOnError` 无错可捕,**glm-5.3 静默转按量计费跑了一整天**,直到账单通知才被发现。
452
+
453
+ 三条守则,缺一不可:
454
+
455
+ 1. **先在服务商侧逐模型确认「用完即停」真的生效**(发一条超额调用看是否返回 403,别信批量开关)。
456
+ 路由器只能在**看得见错误**时换线;额度耗尽若被服务商静默转按量,插件无从得知。
457
+ 2. **`costControl.defaultLevel` 指向稳定付费档**(本文各处示例的 `defaultLevel: L1` 若 L1 是免费档,
458
+ 正是本次事故形态)。免费档只放最低层(L0),或至少每个免费档都有明确的付费替代。
459
+ 3. **用计费面板按档核账**:任何"免费"档的费用行持续大于 0 且不归零,立刻怀疑额度已尽、
460
+ 在按量扣费——把该档从免费路由摘下来。
461
+
462
+ > 另一个易错点:免费额度池按 **provider+model 独立**计数。同一个模型 id 出现在多个 provider
463
+ > (如 `qwen3.8-flash` 同时在 bailian 与 qianwen)是**不同的池子**,额度与计费互不相干。