@apinggithub/mydsh-auto-model-router 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +456 -2
- package/README.zh.md +413 -0
- package/auto-adapter.mjs +104 -0
- package/classifier.mjs +86 -0
- package/client.js +123 -0
- package/config.mjs +458 -0
- package/cordis.patch.yml +11 -0
- package/cost-control.mjs +159 -0
- package/fallback.mjs +62 -0
- package/index.mjs +307 -0
- package/notify.mjs +653 -0
- package/package.json +70 -4
- package/router.mjs +751 -0
- package/rules.mjs +59 -0
- package/scoring.mjs +69 -0
- package/text-width.mjs +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-model-router contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,457 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mydsh-auto-model-router
|
|
2
|
+
|
|
3
|
+
A hybrid auto model-router plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH). Routes each agent request through **four capability levels (L0–L3)** with three user-facing routing modes: a zero-latency **heuristic lock** (text-width thresholds), a **fixed-level** mode, and **fully-auto** mode (keyword scoring + optional LLM classifier). Failure fallback chains and budget control round it out.
|
|
4
|
+
|
|
5
|
+
> Design inspired by [open-world-project/model-router](https://github.com/open-world-project/model-router) (Hermes Agent) and [opencode-model-router](https://github.com/marco-jardim/opencode-model-router) (fast/medium/heavy tiers): tiered routing plus a cost mode that decides the drift direction on ambiguity.
|
|
6
|
+
|
|
7
|
+
## Acknowledgements
|
|
8
|
+
|
|
9
|
+
This plugin builds on ideas from the following open-source projects:
|
|
10
|
+
|
|
11
|
+
- [open-world-project/model-router](https://github.com/open-world-project/model-router) — Hermes Agent's auto model-router with keyword scoring and cost-aware tier selection
|
|
12
|
+
- [opencode-model-router](https://github.com/marco-jardim/opencode-model-router) — OpenCode's fast/medium/heavy tier delegation plugin
|
|
13
|
+
- [openclaw-routing-yaml](https://github.com/glasshousehq-os/openclaw-routing-yaml) — OpenClaw's declarative per-task model routing via YAML
|
|
14
|
+
- [openmarkai/openclaw-router](https://github.com/openmarkai/openclaw-router) — Benchmark-driven model routing with real evaluation data
|
|
15
|
+
- [MoeAisaka/openclaw-model-policy-router](https://github.com/MoeAisaka/openclaw-model-policy-router) — Policy-driven fail-closed model routing for OpenClaw
|
|
16
|
+
- [NanmiCoder/dsh-auto-mode](https://github.com/NanmiCoder/dsh-auto-mode) — DSH's auto permission mode; inspired the client settings section pattern
|
|
17
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — The host platform this plugin is built for
|
|
18
|
+
|
|
19
|
+
- English (this file)
|
|
20
|
+
- [中文](README.zh.md)
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
One decision is made **per user input** (not per agent step). Steps within the
|
|
25
|
+
same input reuse the decision — the LLM classifier is never called per step.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
user input (new)
|
|
29
|
+
│
|
|
30
|
+
▼
|
|
31
|
+
┌─ current mode?
|
|
32
|
+
│
|
|
33
|
+
│ MODE A heuristic lock (default)
|
|
34
|
+
│ text width of the last N inputs (CJK=2, other=1)
|
|
35
|
+
│ ├─ ≤10 → L0
|
|
36
|
+
│ ├─ ≤100 → L1 (locked)
|
|
37
|
+
│ └─ >100 → ask once: pin a fixed level? enable fully-auto?
|
|
38
|
+
│
|
|
39
|
+
│ MODE B fixed level
|
|
40
|
+
│ use the pinned level; re-ask every askEveryInputs inputs
|
|
41
|
+
│ window falls back ≤100 → return to MODE A
|
|
42
|
+
│
|
|
43
|
+
│ MODE C fully-auto
|
|
44
|
+
│ ① keyword scoring: L1 hits +1, L2 +2, L3 +3 (weights configurable)
|
|
45
|
+
│ 0 → L0 · 1-5 → L1 · 6-15 → L2 · 16+ → L3 (bands configurable)
|
|
46
|
+
│ ② score 0 → LLM classifier on 本次+上次输入+上次回复
|
|
47
|
+
│ ③ window falls back ≤100 → return to MODE A
|
|
48
|
+
└──────────────────────────────────────────────────────
|
|
49
|
+
│
|
|
50
|
+
▼
|
|
51
|
+
agent/request reuses the decision for every step of this input
|
|
52
|
+
│
|
|
53
|
+
▼
|
|
54
|
+
LLM dispatch
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### The four levels
|
|
58
|
+
|
|
59
|
+
| Level | Typical tasks | Model guidance |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `L0` | everyday chat, quick answers | cheapest/fastest |
|
|
62
|
+
| `L1` | small edits, simple tests | regular |
|
|
63
|
+
| `L2` | writing code, review, refactoring | strong |
|
|
64
|
+
| `L3` | complex bug fixes, multi-step tasks, reports | most powerful |
|
|
65
|
+
|
|
66
|
+
Each level is a user-assigned provider/model route with a **required**
|
|
67
|
+
`reasoningEffort` (defaults to `off` when omitted): `off` = no thinking mode,
|
|
68
|
+
`high`/`max` = thinking intensity. Levels may be left unconfigured; the router
|
|
69
|
+
then falls to the nearest configured level (direction depends on the cost
|
|
70
|
+
mode).
|
|
71
|
+
|
|
72
|
+
## Auto mode in the model selector
|
|
73
|
+
|
|
74
|
+
The plugin registers a virtual `auto` provider, so the model selector shows an
|
|
75
|
+
**Auto Router** group with a single **Auto** model:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Auto Router
|
|
79
|
+
└─ Auto
|
|
80
|
+
deepseek-official
|
|
81
|
+
├─ deepseek-v4-flash
|
|
82
|
+
└─ deepseek-v4-flash
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- Pick **Auto** → this plugin routes the session through the L0–L3 levels.
|
|
86
|
+
- Pick any **real model** → the plugin passes the request through untouched;
|
|
87
|
+
your explicit choice wins and the router stays out of the way.
|
|
88
|
+
|
|
89
|
+
### Settings section (browser)
|
|
90
|
+
|
|
91
|
+
The browser half registers an **Auto Router** page in DSH's settings panel
|
|
92
|
+
(gear icon → sidebar). It renders the current routing policy — the same
|
|
93
|
+
`<mydsh-auto-model-router-status>` report the host injects on session start —
|
|
94
|
+
with a hint telling you to change the configuration through AI conversation:
|
|
95
|
+
just tell the agent in chat (e.g. *"change L2 to deepseek-v4-flash with max
|
|
96
|
+
reasoning"* or *"switch cost mode to cost-first"*), and the AI edits
|
|
97
|
+
`cordis.patch.yml` for you; restart DSH to apply.
|
|
98
|
+
|
|
99
|
+
Selecting **Auto** in the model picker applies immediately — there is no
|
|
100
|
+
confirmation dialog.
|
|
101
|
+
|
|
102
|
+
No client build step is needed: `client.js` is a self-contained ModuleLoader
|
|
103
|
+
bundle maintained in this repository (`pnpm run build:client` regenerates it
|
|
104
|
+
from `client/`).
|
|
105
|
+
|
|
106
|
+
## Install
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
dsh plugin --profile web add ./mydsh-auto-model-router
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or via the published npm package:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm install @apinggithub/mydsh-auto-model-router
|
|
116
|
+
dsh plugin --profile web add @apinggithub/mydsh-auto-model-router
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
Add to your profile's `cordis.patch.yml`:
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
- insert:
|
|
125
|
+
- id: mydsh-auto-model-router
|
|
126
|
+
name: '@deepseek-ai/cordis-plugin-group'
|
|
127
|
+
group: true
|
|
128
|
+
isolate:
|
|
129
|
+
modelRouter: true
|
|
130
|
+
config:
|
|
131
|
+
- id: mydsh-auto-model-router-runtime
|
|
132
|
+
name: '@apinggithub/mydsh-auto-model-router'
|
|
133
|
+
config:
|
|
134
|
+
# ── four levels (the shipped defaults) ─────────────────────
|
|
135
|
+
# reasoningEffort: off = no thinking, medium/high/max = intensity.
|
|
136
|
+
levels:
|
|
137
|
+
L0: # everyday chat
|
|
138
|
+
provider: deepseek-official
|
|
139
|
+
model: deepseek-v4-flash
|
|
140
|
+
reasoningEffort: off
|
|
141
|
+
L1: # code & tests
|
|
142
|
+
provider: deepseek-official
|
|
143
|
+
model: deepseek-v4-flash
|
|
144
|
+
reasoningEffort: medium
|
|
145
|
+
L2: # writing & review
|
|
146
|
+
provider: deepseek-official
|
|
147
|
+
model: deepseek-v4-flash
|
|
148
|
+
reasoningEffort: high
|
|
149
|
+
L3: # complex multi-step
|
|
150
|
+
provider: deepseek-official
|
|
151
|
+
model: deepseek-v4-flash
|
|
152
|
+
reasoningEffort: max
|
|
153
|
+
|
|
154
|
+
# Keyword rules only take part in fully-auto mode. Empty by default.
|
|
155
|
+
rules: []
|
|
156
|
+
|
|
157
|
+
# ── MODE A: heuristic lock (zero latency) ──────────────────
|
|
158
|
+
heuristic:
|
|
159
|
+
windowSize: 3 # count the last N user inputs
|
|
160
|
+
counting: cjk2 # CJK char = 2, others = 1; or 'tokens'
|
|
161
|
+
thresholds: # width → level (user-adjustable)
|
|
162
|
+
- maxChars: 10
|
|
163
|
+
level: L0
|
|
164
|
+
- maxChars: 100
|
|
165
|
+
level: L1 # lock ceiling; >100 asks the user
|
|
166
|
+
|
|
167
|
+
# ── MODE C: keyword scoring (fully-auto) ───────────────────
|
|
168
|
+
scoring:
|
|
169
|
+
weights: # per-hit score per level
|
|
170
|
+
L1: 1
|
|
171
|
+
L2: 2
|
|
172
|
+
L3: 3
|
|
173
|
+
bands: # score → level (user-adjustable)
|
|
174
|
+
- maxScore: 0
|
|
175
|
+
level: L0
|
|
176
|
+
- maxScore: 5
|
|
177
|
+
level: L1
|
|
178
|
+
- maxScore: 15
|
|
179
|
+
level: L2
|
|
180
|
+
- maxScore: null
|
|
181
|
+
level: L3
|
|
182
|
+
|
|
183
|
+
# ── Tier 2: cost control (user-configurable) ───────────────
|
|
184
|
+
costControl:
|
|
185
|
+
enabled: true
|
|
186
|
+
mode: balanced # cost-first | quality-first | balanced
|
|
187
|
+
defaultLevel: L1 # level used when no rule matches
|
|
188
|
+
tokenBudgetPerSession: 0 # 0 = no budget limit (default);
|
|
189
|
+
# positive value caps per-session spend
|
|
190
|
+
# What happens when the session budget is exhausted:
|
|
191
|
+
# silent — switch to the cheapest level without telling the user
|
|
192
|
+
# notify — inject a user-visible notice explaining the switch
|
|
193
|
+
# ask — pop a dialog; user may keep the current model
|
|
194
|
+
# (waives the budget for the rest of the session)
|
|
195
|
+
downgradeBehavior: notify
|
|
196
|
+
|
|
197
|
+
# ── LLM classifier (fully-auto fallback; enabled by default) ──
|
|
198
|
+
llmClassifier:
|
|
199
|
+
enabled: true
|
|
200
|
+
model:
|
|
201
|
+
provider: deepseek-official
|
|
202
|
+
model: deepseek-v4-flash
|
|
203
|
+
requestTimeoutMs: 10000
|
|
204
|
+
|
|
205
|
+
# ── Tier 3: free-quota preference (optional) ───────────────
|
|
206
|
+
# Declare which routes are free-quota routes: they are preferred,
|
|
207
|
+
# and a failed one is demoted for the session.
|
|
208
|
+
freeQuota:
|
|
209
|
+
enabled: true # auto-disabled when nothing is declared
|
|
210
|
+
routes: # extra free routes that are not levels
|
|
211
|
+
- provider: glm
|
|
212
|
+
model: glm-4.7-flash
|
|
213
|
+
maxLevel: L1 # highest level it serves (inclusive), default L1
|
|
214
|
+
reasoningEffort: off
|
|
215
|
+
demoteOnError: true # one failure demotes it for the session
|
|
216
|
+
notify: true # say so once per session
|
|
217
|
+
|
|
218
|
+
# Fixed-level mode re-asks every N user inputs.
|
|
219
|
+
askEveryInputs: 3
|
|
220
|
+
|
|
221
|
+
# ── Tier 4: failure fallback chain (model-level) ───────────
|
|
222
|
+
fallbackChain:
|
|
223
|
+
- provider: deepseek-official
|
|
224
|
+
model: deepseek-v4-flash
|
|
225
|
+
|
|
226
|
+
maxRetries: 1
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### costControl modes
|
|
230
|
+
|
|
231
|
+
| mode | default level (no rule hit) | ambiguity drift |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `cost-first` | lowest configured level | **down** (cheaper) |
|
|
234
|
+
| `quality-first` | highest configured level | **up** (stronger) |
|
|
235
|
+
| `balanced` | `costControl.defaultLevel` | stay put |
|
|
236
|
+
|
|
237
|
+
A rule hit always wins over the cost mode: a level the user explicitly matched
|
|
238
|
+
is never downgraded by the cost layer.
|
|
239
|
+
|
|
240
|
+
### Budget downgrade is visible
|
|
241
|
+
|
|
242
|
+
`tokenBudgetPerSession` **defaults to 0 (no budget)** — the cost layer never
|
|
243
|
+
forces a downgrade from budget exhaustion, and `downgradeBehavior` is inert.
|
|
244
|
+
Set a positive value to cap spend per session:
|
|
245
|
+
|
|
246
|
+
- e.g. `tokenBudgetPerSession: 300000`: downgrade once the session uses 300k tokens.
|
|
247
|
+
- The downgrade behavior is controlled by `downgradeBehavior` (default `notify`):
|
|
248
|
+
- `notify`: inject a user-visible message explaining the budget is exhausted,
|
|
249
|
+
which model the remaining requests use, and how to raise the cap.
|
|
250
|
+
- `ask`: pop a dialog asking "switch to the cheaper model or keep the current
|
|
251
|
+
one?"; choosing "keep" waives the budget for the rest of the session.
|
|
252
|
+
- `silent`: switch silently (not recommended — the user wonders why answers
|
|
253
|
+
got worse).
|
|
254
|
+
|
|
255
|
+
### Free-quota preference (freeQuota)
|
|
256
|
+
|
|
257
|
+
Many providers hand out free quota (Zhipu's `glm-4.7-flash`, for instance, is
|
|
258
|
+
permanently free), and a free route usually comes with the fine print: rate
|
|
259
|
+
limits, a quota that runs out, tighter parameter caps. This layer turns "free"
|
|
260
|
+
into a declarable route property and keeps a dead free route from failing the
|
|
261
|
+
whole turn.
|
|
262
|
+
|
|
263
|
+
Two declaration forms, which can be mixed:
|
|
264
|
+
|
|
265
|
+
```yaml
|
|
266
|
+
levels:
|
|
267
|
+
L0: { provider: glm, model: glm-4.7-flash, reasoningEffort: off, freeQuota: true }
|
|
268
|
+
L1: { provider: glm, model: glm-4.7-flash, reasoningEffort: low, freeQuota: true }
|
|
269
|
+
L2: { provider: deepseek-account, model: deepseek-flash, reasoningEffort: high }
|
|
270
|
+
L3: { provider: deepseek-account, model: deepseek-flash, reasoningEffort: max }
|
|
271
|
+
|
|
272
|
+
# Or: keep the levels paid and let a declared free route claim their work.
|
|
273
|
+
freeQuota:
|
|
274
|
+
routes:
|
|
275
|
+
- { provider: glm, model: glm-4.7-flash, maxLevel: L1, reasoningEffort: off }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
What it does:
|
|
279
|
+
|
|
280
|
+
1. **Preference** — a decision at or below `maxLevel` runs on the declared free
|
|
281
|
+
route (free routes always carry an explicit `reasoningEffort`, defaulting to
|
|
282
|
+
`off`, so they never fall into the adapter's own default thinking level).
|
|
283
|
+
2. **Demotion on failure** — any failure on a free route (quota exhausted, 429
|
|
284
|
+
rate limit, rejected credentials, rejected parameters) marks that route as
|
|
285
|
+
burned **for this session** and the request is retried on the next viable
|
|
286
|
+
route — normally the paid route that level was configured with.
|
|
287
|
+
3. **No retrying a dead route** — after a demotion the fallback walk skips every
|
|
288
|
+
burned free route, so one session never hits the same wall twice.
|
|
289
|
+
4. **Visible** — with `notify: true` the plugin injects one message saying which
|
|
290
|
+
free route failed, why, and what is used instead (at most once per session).
|
|
291
|
+
5. **Session-scoped** — the demotion lasts for the current session only; the
|
|
292
|
+
next session tries the free route again. With nothing declared free the whole
|
|
293
|
+
layer is inert, so existing profiles behave exactly as before.
|
|
294
|
+
|
|
295
|
+
> Note: free **vision** models often have a much smaller output cap (e.g.
|
|
296
|
+
> `glm-4v-flash` allows `max_tokens ≤ 1024`). Fix those on the provider's
|
|
297
|
+
> `models` entry with `maxTokens` — the router cannot guess them for you. If you
|
|
298
|
+
> see `INVALID_REQUEST: max_tokens …`, add `maxTokens` to that model.
|
|
299
|
+
|
|
300
|
+
### On-load consultation (no client UI)
|
|
301
|
+
|
|
302
|
+
On the **first session** after installing or hot-reloading the plugin, DSH:
|
|
303
|
+
|
|
304
|
+
1. Injects a status report message (level→model mapping, cost mode, budget,
|
|
305
|
+
fallback chain). The language follows `settings.yaml`'s
|
|
306
|
+
`locale.preference` (`zh`/`en`, default `en`).
|
|
307
|
+
2. Pops a dialog with three questions (asked once per plugin load):
|
|
308
|
+
|
|
309
|
+
| Question | Options |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| Q1 Tier-1 rules | Keep current rules / **View current rules** (injects the rule list) |
|
|
312
|
+
| Q2 Tier-2 cost mode | Keep current / cost-first / balanced / quality-first (applies immediately) |
|
|
313
|
+
| Q3 Other config | Skip / **Check a setting** (type budget, fallbackChain, classifier, …; injects its current value) |
|
|
314
|
+
|
|
315
|
+
The injected lists and values let the user **decide** whether to adjust;
|
|
316
|
+
persistent changes still live in `cordis.patch.yml` (the messages say so).
|
|
317
|
+
Headless or no-question-channel setups degrade to report-only and never block.
|
|
318
|
+
|
|
319
|
+
## Rule syntax
|
|
320
|
+
|
|
321
|
+
- Plain text: `match: 'refactor'` — case-insensitive substring.
|
|
322
|
+
- Regex literal: `match: '/\\bdebug\\b/'` — slashes delimit the pattern, optional flags after the last slash (`/.../i`).
|
|
323
|
+
- Rules are evaluated **in order**; the first match wins.
|
|
324
|
+
- `match` can target user message text, tool-call results text, or the session `cwd` (matched as `cwd:<path>`).
|
|
325
|
+
|
|
326
|
+
## What the plugin hooks
|
|
327
|
+
|
|
328
|
+
| DSH event | Purpose |
|
|
329
|
+
| --- | --- |
|
|
330
|
+
| `agent/session-start` | reset per-session routing state |
|
|
331
|
+
| `agent/pre-step` | token accounting for budget tracking |
|
|
332
|
+
| `system-prompt/assemble` | keep `{{model}}` prompt variables consistent |
|
|
333
|
+
| `agent/request` | **the routing decision** — replace provider/model |
|
|
334
|
+
| `agent/request-error` | fallback chain → `{ kind: 'retry' }` |
|
|
335
|
+
| `session/event` | track spend from assistant messages |
|
|
336
|
+
|
|
337
|
+
## Testing
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npm test # node --test *.test.mjs
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
No external services are required — all layers are unit-tested against fixtures.
|
|
344
|
+
|
|
345
|
+
## Known Issues
|
|
346
|
+
|
|
347
|
+
- **Fully-auto mode latency**: When the LLM classifier is enabled, the first
|
|
348
|
+
step of each new user input incurs an additional LLM call (classification).
|
|
349
|
+
While subsequent steps within the same input are cached and fast, the overall
|
|
350
|
+
round-trip for multi-step tasks is noticeably slower than directly selecting
|
|
351
|
+
a model. Contributions to optimize this are welcome — the classifier could
|
|
352
|
+
potentially be cached across sessions, or a lighter heuristic could be used
|
|
353
|
+
as a first pass before falling back to the LLM.
|
|
354
|
+
|
|
355
|
+
## Companion plugin: cost by tier
|
|
356
|
+
|
|
357
|
+
`tier-cost/` ships the second plugin of this repository,
|
|
358
|
+
**`@apinggithub/dsh-tier-cost`**: it folds session logs into per-level (L0–L3)
|
|
359
|
+
token usage and spend, borrowing per-turn cost from the billing plugin
|
|
360
|
+
`@kenz1117/dsh-ui-usage-billing` (it owns no price table of its own) and adding a
|
|
361
|
+
"Tier Cost" settings section — because the four levels usually share one model,
|
|
362
|
+
a per-model panel cannot tell you which tier paid.
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
dsh plugin --profile desktop add ./tier-cost
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
See [tier-cost/README.md](tier-cost/README.md) for details.
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
MIT
|
|
2
373
|
|
|
3
|
-
|
|
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 }
|
|
389
|
+
```
|
|
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
|
|
425
|
+
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
|
|
457
|
+
`npm login --scope=apinggithub` once per package account setup.
|