dsh-model-router 0.4.0 → 0.6.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/README.md +57 -1
- package/cordis.patch.yml +26 -0
- package/lib/client.js +356 -0
- package/lib/index.d.ts +74 -1
- package/lib/index.js +45 -6
- package/lib/index.js.map +1 -1
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -53,7 +53,8 @@ Routing is on by default. Two ways to switch it off:
|
|
|
53
53
|
- **GUI (Settings → Plugins → dsh-model-router):** the plugin registers a live
|
|
54
54
|
settings section; flip `enabled` off. It applies immediately (no restart),
|
|
55
55
|
persists in `settings.yaml` under `model-router:`, and unregisters the prompt
|
|
56
|
-
section and the skill too. Flip it back on and everything returns.
|
|
56
|
+
section and the skill too. Flip it back on and everything returns. The same
|
|
57
|
+
card also hosts the Vision switch (v0.6.0+, see below).
|
|
57
58
|
- **Patch row:** set `enabled: false` in the profile's `cordis.patch.yml` row
|
|
58
59
|
(takes effect on the next boot). `disabled: true` still skips the row entirely.
|
|
59
60
|
|
|
@@ -61,6 +62,61 @@ With the router off, requests use the session's selected model (your
|
|
|
61
62
|
`agent-default-model` setting or the base default) — the router is simply not
|
|
62
63
|
rewriting them.
|
|
63
64
|
|
|
65
|
+
## The GUI toggle (v0.5.0+)
|
|
66
|
+
|
|
67
|
+
Since v0.5.0 the package ships a browser half, and the harness serves it
|
|
68
|
+
automatically — no extra config. After installing (or updating to) v0.5.0+
|
|
69
|
+
and restarting the profile, **Settings → Plugins** shows a **Model router**
|
|
70
|
+
card with a live **Enabled** switch, an "Overridden" badge and **Reset to
|
|
71
|
+
default** button once you've changed it, and a read-only view of the current
|
|
72
|
+
planner/executor routes and mode. Flipping the switch applies immediately (no
|
|
73
|
+
restart) and persists in `settings.yaml` under `model-router:` — the same
|
|
74
|
+
mechanism the GUI toggle described above uses.
|
|
75
|
+
|
|
76
|
+
One harness-wide caveat (it applies to **all** settings pages — Models,
|
|
77
|
+
Plugins, everything — not to this plugin specifically): the harness serves
|
|
78
|
+
settings pages only to **loopback** browsers (`localhost` / `127.x`). A remote
|
|
79
|
+
browser sees "settings are unavailable" instead of the card. Fallbacks that
|
|
80
|
+
work everywhere:
|
|
81
|
+
|
|
82
|
+
- **Patch row** — set `enabled: false` in the profile's `cordis.patch.yml`
|
|
83
|
+
(takes effect on the next boot), or
|
|
84
|
+
- **`settings.yaml`** — add `model-router: { enabled: false }` under the
|
|
85
|
+
settings file the profile uses; this applies live, same as the GUI.
|
|
86
|
+
|
|
87
|
+
## Vision routing (v0.6.0+)
|
|
88
|
+
|
|
89
|
+
Since v0.6.0 the router can also handle image-bearing requests. **It is opt-in
|
|
90
|
+
and off by default** (`vision.enabled: false`). Two ways to turn it on:
|
|
91
|
+
|
|
92
|
+
- **GUI:** on the same **Model router** card, flip the **Vision** switch (live,
|
|
93
|
+
no restart, persists in `settings.yaml`), or
|
|
94
|
+
- **Patch row / settings:** set `vision.enabled: true` on the plugin's config.
|
|
95
|
+
|
|
96
|
+
When enabled, **any request whose messages carry an image is stamped with the
|
|
97
|
+
vision model** — `deepseek-v4-flash-vision-exp` from `deepseek-official` by
|
|
98
|
+
default — **from every role**: the root (planner) agent and all delegated
|
|
99
|
+
subagents. Everything else keeps the pro/flash role routing untouched. The
|
|
100
|
+
vision branch is checked first, so a subagent reading an image still lands on
|
|
101
|
+
the vision model, not on flash. Optional `vision.reasoningEffort` /
|
|
102
|
+
`vision.maxTokens` pins work exactly like the per-role ones.
|
|
103
|
+
|
|
104
|
+
The plugin ships the support in its own `cordis.patch.yml`:
|
|
105
|
+
|
|
106
|
+
- a **catalog entry** for the vision model on the `llm-deepseek` row (with
|
|
107
|
+
`inputModalities: [text, image]`, plus raised `contextWindow`/`maxTokens`
|
|
108
|
+
restated for the pro/flash rows), and
|
|
109
|
+
- **raised `attachment-local` image admission limits** so normal screenshots
|
|
110
|
+
(~8K, 15MB) attach without being rejected (`maxImageDimension: 8192`,
|
|
111
|
+
`maxImagePixels: 100000000`, `maxImageBytes: 15728640`).
|
|
112
|
+
|
|
113
|
+
Both are defaults you can override in your **profile**'s `cordis.patch.yml` —
|
|
114
|
+
the profile layer applies after the plugin layer, so a `patch:` targeting
|
|
115
|
+
`llm-deepseek` or `attachment-local` in the profile wins. One hard requirement
|
|
116
|
+
remains: the vision model **must be present in the catalog with image input
|
|
117
|
+
modality**, or the provider rejects the request at call time with
|
|
118
|
+
`UNSUPPORTED_CONTENT` — the shipped catalog row is what satisfies that.
|
|
119
|
+
|
|
64
120
|
## Tuning
|
|
65
121
|
|
|
66
122
|
All configuration lives on the plugin row. Patch it in the profile's `cordis.patch.yml`:
|
package/cordis.patch.yml
CHANGED
|
@@ -29,7 +29,33 @@
|
|
|
29
29
|
# falls back to the executor route to reserve pro for planning.
|
|
30
30
|
mode: strict
|
|
31
31
|
# enabled: false # off switch — GUI: Settings → Plugins → model-router (live, no restart); patch row needs a reboot
|
|
32
|
+
# vision.enabled: true # route image-bearing requests to deepseek-v4-flash-vision-exp
|
|
33
|
+
# GUI: Settings → Plugins → "Model router" card (v0.5.0+, loopback browser only)
|
|
32
34
|
# Publish the always-on routing convention prompt section.
|
|
33
35
|
promptSection: true
|
|
34
36
|
# Register the `pro-flash-routing` skill in the session catalog.
|
|
35
37
|
skill: true
|
|
38
|
+
# Vision support: the catalog must list the vision model with image input, and
|
|
39
|
+
# image admission limits are raised so normal screenshots (~8K, 15MB) attach.
|
|
40
|
+
- patch:
|
|
41
|
+
- id: llm-deepseek
|
|
42
|
+
config:
|
|
43
|
+
models:
|
|
44
|
+
- id: deepseek-v4-flash
|
|
45
|
+
name: DeepSeek-V4-Flash
|
|
46
|
+
contextWindow: 1000000
|
|
47
|
+
maxTokens: 256000
|
|
48
|
+
- id: deepseek-v4-pro
|
|
49
|
+
name: DeepSeek-V4-Pro
|
|
50
|
+
contextWindow: 1000000
|
|
51
|
+
maxTokens: 256000
|
|
52
|
+
- id: deepseek-v4-flash-vision-exp
|
|
53
|
+
name: DeepSeek-V4-Flash-Vision-Exp
|
|
54
|
+
contextWindow: 1000000
|
|
55
|
+
maxTokens: 256000
|
|
56
|
+
inputModalities: [text, image]
|
|
57
|
+
- id: attachment-local
|
|
58
|
+
config:
|
|
59
|
+
maxImageDimension: 8192
|
|
60
|
+
maxImagePixels: 100000000
|
|
61
|
+
maxImageBytes: 15728640
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-model-router",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
"use strict";
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
10
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
|
+
|
|
26
|
+
// src/client.tsx
|
|
27
|
+
var client_exports = {};
|
|
28
|
+
__export(client_exports, {
|
|
29
|
+
apply: () => apply,
|
|
30
|
+
inject: () => inject
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(client_exports);
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
var NS = "model-router";
|
|
35
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
36
|
+
var en = {
|
|
37
|
+
nav: "Model router",
|
|
38
|
+
title: "Model router",
|
|
39
|
+
description: "Role-based model routing: planning runs on deepseek-v4-pro, delegated executor subagents on deepseek-v4-flash.",
|
|
40
|
+
enabled: "Enabled",
|
|
41
|
+
enabledHint: "Off stops rewriting requests and unregisters the prompt section and skill; on restores them.",
|
|
42
|
+
on: "On",
|
|
43
|
+
off: "Off",
|
|
44
|
+
reset: "Reset to default",
|
|
45
|
+
overridden: "Overridden",
|
|
46
|
+
readOnly: "This deployment stores settings read-only in this browser; use the plugin row in the profile's cordis.patch.yml instead.",
|
|
47
|
+
plannerRoute: "Planner model",
|
|
48
|
+
executorRoute: "Executor model",
|
|
49
|
+
mode: "Mode",
|
|
50
|
+
modeStrict: "strict \u2014 the root agent is always the planner",
|
|
51
|
+
modePlan: "plan \u2014 the root agent is pro only while plan mode is active",
|
|
52
|
+
visionTitle: "Vision routing",
|
|
53
|
+
visionDesc: "Opt-in (default off): when enabled, any request containing an image routes to the vision model, from every role; other requests keep the pro/flash routing.",
|
|
54
|
+
visionSwitch: "Vision",
|
|
55
|
+
visionReset: "Reset vision to default",
|
|
56
|
+
visionRoute: "Vision model"
|
|
57
|
+
};
|
|
58
|
+
var zh = {
|
|
59
|
+
nav: "\u6A21\u578B\u8DEF\u7531",
|
|
60
|
+
title: "\u6A21\u578B\u8DEF\u7531",
|
|
61
|
+
description: "\u6309\u89D2\u8272\u5206\u914D\u6A21\u578B\uFF1A\u89C4\u5212\u5728 deepseek-v4-pro \u4E0A\u8FD0\u884C\uFF0C\u59D4\u6D3E\u7684\u6267\u884C\u5B50\u4EE3\u7406\u5728 deepseek-v4-flash \u4E0A\u8FD0\u884C\u3002",
|
|
62
|
+
enabled: "\u542F\u7528",
|
|
63
|
+
enabledHint: "\u5173\u95ED\u540E\u505C\u6B62\u6539\u5199\u8BF7\u6C42\uFF0C\u5E76\u6CE8\u9500\u63D0\u793A\u533A\u5757\u4E0E\u6280\u80FD\uFF1B\u91CD\u65B0\u6253\u5F00\u5373\u6062\u590D\u3002",
|
|
64
|
+
on: "\u5F00",
|
|
65
|
+
off: "\u5173",
|
|
66
|
+
reset: "\u6062\u590D\u9ED8\u8BA4",
|
|
67
|
+
overridden: "\u5DF2\u8986\u76D6",
|
|
68
|
+
readOnly: "\u5F53\u524D\u6D4F\u89C8\u5668\u4E2D\u8BE5\u90E8\u7F72\u7684\u8BBE\u7F6E\u4E3A\u53EA\u8BFB\uFF1B\u8BF7\u6539\u7528 profile \u7684 cordis.patch.yml \u4E2D\u7684\u63D2\u4EF6\u884C\u3002",
|
|
69
|
+
plannerRoute: "\u89C4\u5212\u6A21\u578B",
|
|
70
|
+
executorRoute: "\u6267\u884C\u6A21\u578B",
|
|
71
|
+
mode: "\u6A21\u5F0F",
|
|
72
|
+
modeStrict: "strict \u2014 \u6839 agent \u59CB\u7EC8\u4E3A\u89C4\u5212\u8005",
|
|
73
|
+
modePlan: "plan \u2014 \u6839 agent \u4EC5\u5728\u8BA1\u5212\u6A21\u5F0F\u6FC0\u6D3B\u65F6\u4F7F\u7528 pro",
|
|
74
|
+
visionTitle: "\u89C6\u89C9\u8DEF\u7531",
|
|
75
|
+
visionDesc: "\u53EF\u9009\uFF08\u9ED8\u8BA4\u5173\u95ED\uFF09\uFF1A\u542F\u7528\u540E\uFF0C\u4EFB\u4F55\u5305\u542B\u56FE\u7247\u7684\u8BF7\u6C42\u90FD\u4F1A\u8DEF\u7531\u5230\u89C6\u89C9\u6A21\u578B\uFF0C\u9002\u7528\u4E8E\u6240\u6709\u89D2\u8272\uFF1B\u5176\u4ED6\u8BF7\u6C42\u4FDD\u6301 pro/flash \u8DEF\u7531\u3002",
|
|
76
|
+
visionSwitch: "\u89C6\u89C9",
|
|
77
|
+
visionReset: "\u6062\u590D\u89C6\u89C9\u9ED8\u8BA4\u503C",
|
|
78
|
+
visionRoute: "\u89C6\u89C9\u6A21\u578B"
|
|
79
|
+
};
|
|
80
|
+
var ModelRouterCardController = class {
|
|
81
|
+
scope;
|
|
82
|
+
constructor(scope) {
|
|
83
|
+
this.scope = scope;
|
|
84
|
+
}
|
|
85
|
+
get store() {
|
|
86
|
+
return {
|
|
87
|
+
subscribe: (listener) => this.scope.subscribe(listener),
|
|
88
|
+
getSnapshot: () => this.scope.getSnapshot()
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Build the face the card's slot registration injects. */
|
|
92
|
+
inject() {
|
|
93
|
+
return {
|
|
94
|
+
hooks: { modelRouterCard: this.store },
|
|
95
|
+
setEnabled: async (next) => {
|
|
96
|
+
try {
|
|
97
|
+
await this.scope.set("enabled", next);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
reset: async () => {
|
|
102
|
+
try {
|
|
103
|
+
await this.scope.unset("enabled");
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
// Vision toggle merges into the current user-layer vision object so the
|
|
108
|
+
// provider/model base fields survive the write; reset drops the whole
|
|
109
|
+
// vision key from the user layer, restoring the schema defaults.
|
|
110
|
+
setVisionEnabled: async (next) => {
|
|
111
|
+
try {
|
|
112
|
+
const current = this.scope.getSnapshot().value?.vision;
|
|
113
|
+
await this.scope.set("vision", { ...current ?? {}, enabled: next });
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
resetVision: async () => {
|
|
118
|
+
try {
|
|
119
|
+
await this.scope.unset("vision");
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Stop the scope's mirror subscription when the fiber stops. */
|
|
126
|
+
dispose() {
|
|
127
|
+
void this.scope.dispose();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function routeRow(label, value) {
|
|
131
|
+
return (0, import_react.createElement)("div", { style: styles.routeRow }, [
|
|
132
|
+
(0, import_react.createElement)("dt", { style: styles.routeLabel }, label),
|
|
133
|
+
(0, import_react.createElement)("dd", { style: styles.routeValue }, value)
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
function modeLabel(t, mode) {
|
|
137
|
+
return mode === "plan" ? t("modePlan") : t("modeStrict");
|
|
138
|
+
}
|
|
139
|
+
function ModelRouterCard(props) {
|
|
140
|
+
const { t } = props;
|
|
141
|
+
const state = props.useModelRouterCard((snapshot) => snapshot);
|
|
142
|
+
const unavailable = !state.writable || state.mode !== "host";
|
|
143
|
+
const overridden = state.user?.enabled !== void 0;
|
|
144
|
+
const visionOverridden = state.user?.vision !== void 0;
|
|
145
|
+
return (0, import_react.createElement)("section", { style: styles.card }, [
|
|
146
|
+
(0, import_react.createElement)("div", { style: styles.headRow }, [
|
|
147
|
+
(0, import_react.createElement)("h3", { style: styles.title }, t("title")),
|
|
148
|
+
overridden ? (0, import_react.createElement)("span", { style: styles.badge }, t("overridden")) : null
|
|
149
|
+
]),
|
|
150
|
+
(0, import_react.createElement)("p", { style: styles.description }, t("description")),
|
|
151
|
+
...unavailable ? [(0, import_react.createElement)("p", { style: styles.note }, t("readOnly"))] : state.value ? [
|
|
152
|
+
(0, import_react.createElement)("div", { style: styles.switchRow }, [
|
|
153
|
+
(0, import_react.createElement)("label", { htmlFor: "model-router-enabled", style: styles.label }, t("enabled")),
|
|
154
|
+
(0, import_react.createElement)("input", {
|
|
155
|
+
id: "model-router-enabled",
|
|
156
|
+
type: "checkbox",
|
|
157
|
+
checked: Boolean(state.value.enabled),
|
|
158
|
+
onChange: (event) => {
|
|
159
|
+
void props.setEnabled(event.currentTarget.checked);
|
|
160
|
+
},
|
|
161
|
+
style: styles.switch
|
|
162
|
+
}),
|
|
163
|
+
(0, import_react.createElement)("span", { style: styles.status }, state.value.enabled ? t("on") : t("off"))
|
|
164
|
+
]),
|
|
165
|
+
(0, import_react.createElement)("p", { style: styles.hint }, t("enabledHint")),
|
|
166
|
+
overridden ? (0, import_react.createElement)(
|
|
167
|
+
"button",
|
|
168
|
+
{ type: "button", style: styles.reset, onClick: () => void props.reset() },
|
|
169
|
+
t("reset")
|
|
170
|
+
) : null,
|
|
171
|
+
(0, import_react.createElement)("div", { style: styles.vision }, [
|
|
172
|
+
(0, import_react.createElement)("div", { style: styles.switchRow }, [
|
|
173
|
+
(0, import_react.createElement)("label", { htmlFor: "model-router-vision", style: styles.label }, t("visionSwitch")),
|
|
174
|
+
(0, import_react.createElement)("input", {
|
|
175
|
+
id: "model-router-vision",
|
|
176
|
+
type: "checkbox",
|
|
177
|
+
checked: Boolean(state.value.vision?.enabled),
|
|
178
|
+
onChange: (event) => {
|
|
179
|
+
void props.setVisionEnabled(event.currentTarget.checked);
|
|
180
|
+
},
|
|
181
|
+
style: styles.switch
|
|
182
|
+
}),
|
|
183
|
+
(0, import_react.createElement)("span", { style: styles.status }, state.value.vision?.enabled ? t("on") : t("off"))
|
|
184
|
+
]),
|
|
185
|
+
(0, import_react.createElement)("p", { style: styles.hint }, t("visionDesc")),
|
|
186
|
+
visionOverridden ? (0, import_react.createElement)(
|
|
187
|
+
"button",
|
|
188
|
+
{ type: "button", style: styles.reset, onClick: () => void props.resetVision() },
|
|
189
|
+
t("visionReset")
|
|
190
|
+
) : null,
|
|
191
|
+
(0, import_react.createElement)("dl", { style: styles.routes }, [
|
|
192
|
+
routeRow(
|
|
193
|
+
t("visionRoute"),
|
|
194
|
+
`${state.value.vision?.model ?? "\u2014"} \xB7 ${state.value.vision?.provider ?? "\u2014"}`
|
|
195
|
+
)
|
|
196
|
+
])
|
|
197
|
+
]),
|
|
198
|
+
(0, import_react.createElement)("dl", { style: styles.routes }, [
|
|
199
|
+
routeRow(t("plannerRoute"), state.value.planner?.model ?? "\u2014"),
|
|
200
|
+
routeRow(t("executorRoute"), state.value.executor?.model ?? "\u2014"),
|
|
201
|
+
routeRow(t("mode"), modeLabel(t, state.value.mode))
|
|
202
|
+
])
|
|
203
|
+
] : []
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
var styles = {
|
|
207
|
+
card: {
|
|
208
|
+
display: "flex",
|
|
209
|
+
flexDirection: "column",
|
|
210
|
+
gap: "10px",
|
|
211
|
+
padding: "16px 18px",
|
|
212
|
+
background: "var(--dsw-alias-bg-layer-3)",
|
|
213
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
214
|
+
borderRadius: "12px",
|
|
215
|
+
color: "var(--dsw-alias-label-primary)",
|
|
216
|
+
font: "inherit"
|
|
217
|
+
},
|
|
218
|
+
headRow: {
|
|
219
|
+
display: "flex",
|
|
220
|
+
alignItems: "center",
|
|
221
|
+
gap: "8px"
|
|
222
|
+
},
|
|
223
|
+
title: {
|
|
224
|
+
margin: 0,
|
|
225
|
+
fontSize: "15px",
|
|
226
|
+
fontWeight: 600,
|
|
227
|
+
lineHeight: 1.4
|
|
228
|
+
},
|
|
229
|
+
badge: {
|
|
230
|
+
whiteSpace: "nowrap",
|
|
231
|
+
background: "var(--dsw-alias-bg-module-platform)",
|
|
232
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
233
|
+
borderRadius: "999px",
|
|
234
|
+
padding: "1px 8px",
|
|
235
|
+
fontSize: "11px",
|
|
236
|
+
fontWeight: 500,
|
|
237
|
+
lineHeight: "17px"
|
|
238
|
+
},
|
|
239
|
+
description: {
|
|
240
|
+
margin: 0,
|
|
241
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
242
|
+
fontSize: "13px",
|
|
243
|
+
lineHeight: 1.5
|
|
244
|
+
},
|
|
245
|
+
switchRow: {
|
|
246
|
+
display: "flex",
|
|
247
|
+
alignItems: "center",
|
|
248
|
+
gap: "10px",
|
|
249
|
+
padding: "10px 0"
|
|
250
|
+
},
|
|
251
|
+
label: {
|
|
252
|
+
flex: 1,
|
|
253
|
+
minWidth: 0,
|
|
254
|
+
fontSize: "13px",
|
|
255
|
+
fontWeight: 500,
|
|
256
|
+
lineHeight: 1.5
|
|
257
|
+
},
|
|
258
|
+
switch: {
|
|
259
|
+
width: "16px",
|
|
260
|
+
height: "16px",
|
|
261
|
+
accentColor: "var(--dsw-alias-brand-primary)",
|
|
262
|
+
cursor: "pointer"
|
|
263
|
+
},
|
|
264
|
+
status: {
|
|
265
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
266
|
+
fontSize: "12px",
|
|
267
|
+
lineHeight: 1.5,
|
|
268
|
+
minWidth: "24px"
|
|
269
|
+
},
|
|
270
|
+
hint: {
|
|
271
|
+
margin: 0,
|
|
272
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
273
|
+
fontSize: "12px",
|
|
274
|
+
lineHeight: 1.5
|
|
275
|
+
},
|
|
276
|
+
reset: {
|
|
277
|
+
alignSelf: "flex-start",
|
|
278
|
+
font: "inherit",
|
|
279
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
280
|
+
background: "none",
|
|
281
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
282
|
+
borderRadius: "8px",
|
|
283
|
+
padding: "4px 10px",
|
|
284
|
+
fontSize: "12px",
|
|
285
|
+
lineHeight: 1.5,
|
|
286
|
+
cursor: "pointer"
|
|
287
|
+
},
|
|
288
|
+
routes: {
|
|
289
|
+
margin: "4px 0 0",
|
|
290
|
+
padding: "10px 0 0",
|
|
291
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)",
|
|
292
|
+
display: "flex",
|
|
293
|
+
flexDirection: "column",
|
|
294
|
+
gap: "6px"
|
|
295
|
+
},
|
|
296
|
+
vision: {
|
|
297
|
+
margin: "4px 0 0",
|
|
298
|
+
padding: "12px 0 0",
|
|
299
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)"
|
|
300
|
+
},
|
|
301
|
+
routeRow: {
|
|
302
|
+
display: "flex",
|
|
303
|
+
alignItems: "center",
|
|
304
|
+
gap: "8px"
|
|
305
|
+
},
|
|
306
|
+
routeLabel: {
|
|
307
|
+
minWidth: 0,
|
|
308
|
+
flex: 1,
|
|
309
|
+
margin: 0,
|
|
310
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
311
|
+
fontSize: "12px",
|
|
312
|
+
lineHeight: 1.5
|
|
313
|
+
},
|
|
314
|
+
routeValue: {
|
|
315
|
+
margin: 0,
|
|
316
|
+
color: "var(--dsw-alias-label-primary)",
|
|
317
|
+
fontSize: "12px",
|
|
318
|
+
lineHeight: 1.5,
|
|
319
|
+
fontVariantNumeric: "tabular-nums"
|
|
320
|
+
},
|
|
321
|
+
note: {
|
|
322
|
+
margin: 0,
|
|
323
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
324
|
+
fontSize: "12px",
|
|
325
|
+
lineHeight: 1.5
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
function apply(ctx) {
|
|
329
|
+
const t = ctx.locale.bind(NS);
|
|
330
|
+
ctx.effect(
|
|
331
|
+
() => {
|
|
332
|
+
ctx.locale.register(NS, { zh, en });
|
|
333
|
+
},
|
|
334
|
+
"model-router: card dictionaries"
|
|
335
|
+
);
|
|
336
|
+
const controller = new ModelRouterCardController(ctx.settingsScope.bind({ namespace: NS }));
|
|
337
|
+
ctx.slots.inject("settings.plugin.item", function* () {
|
|
338
|
+
yield ctx.slots.register(
|
|
339
|
+
{
|
|
340
|
+
name: "settings.plugin.item",
|
|
341
|
+
key: NS,
|
|
342
|
+
locale: NS,
|
|
343
|
+
inject: () => controller.inject()
|
|
344
|
+
},
|
|
345
|
+
ModelRouterCard
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
ctx.effect(
|
|
349
|
+
() => () => controller.dispose(),
|
|
350
|
+
"model-router: card controller"
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return module.exports;
|
|
355
|
+
}
|
|
356
|
+
});
|
package/lib/index.d.ts
CHANGED
|
@@ -41,6 +41,23 @@ interface ModelRoute {
|
|
|
41
41
|
}
|
|
42
42
|
/** The two roles the router distinguishes. */
|
|
43
43
|
type AgentRole = "planner" | "executor";
|
|
44
|
+
/**
|
|
45
|
+
* Opt-in vision route: when enabled, any request whose messages carry image
|
|
46
|
+
* content is stamped with the vision model, from every role (root agent and
|
|
47
|
+
* subagents alike). Everything else keeps the pro/flash role routing.
|
|
48
|
+
*/
|
|
49
|
+
interface VisionRoute {
|
|
50
|
+
/** Master switch. Defaults to false — vision routing is opt-in. */
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
/** Provider the vision model is served from. */
|
|
53
|
+
provider: string;
|
|
54
|
+
/** The vision model to stamp onto image-bearing requests. */
|
|
55
|
+
model: string;
|
|
56
|
+
/** Optional reasoning-effort pin; omitted means inherit the session's. */
|
|
57
|
+
reasoningEffort?: ReasoningEffort;
|
|
58
|
+
/** Optional output-token cap; omitted means inherit the session's. */
|
|
59
|
+
maxTokens?: number;
|
|
60
|
+
}
|
|
44
61
|
/** Resolved router configuration: one route per role plus routing mode. */
|
|
45
62
|
interface RouterConfig {
|
|
46
63
|
planner: ModelRoute;
|
|
@@ -55,6 +72,8 @@ interface RouterConfig {
|
|
|
55
72
|
enabled: boolean;
|
|
56
73
|
promptSection: boolean;
|
|
57
74
|
skill: boolean;
|
|
75
|
+
/** Opt-in vision routing for image-bearing requests (v0.6.0+). */
|
|
76
|
+
vision: VisionRoute;
|
|
58
77
|
}
|
|
59
78
|
/**
|
|
60
79
|
* Classify an agent as planner or executor.
|
|
@@ -104,9 +123,37 @@ declare function recentStepsHadError(events: readonly unknown[] | undefined, rec
|
|
|
104
123
|
* @returns the effort to stamp, or `undefined` to inherit.
|
|
105
124
|
*/
|
|
106
125
|
declare function effortFor(route: ModelRoute, events: readonly unknown[] | undefined): ReasoningEffort | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Whether any message in a request payload carries image content.
|
|
128
|
+
*
|
|
129
|
+
* A message carries an image when its `content` is a block array containing
|
|
130
|
+
* an `image` block, directly or nested inside a `tool-result` block (tool
|
|
131
|
+
* results may embed the images they produced). String content never does.
|
|
132
|
+
*
|
|
133
|
+
* @param messages - the request's `messages` array (any runtime shape).
|
|
134
|
+
* @returns true when at least one message contains image content.
|
|
135
|
+
*/
|
|
136
|
+
declare function hasImageContent(messages: unknown): boolean;
|
|
107
137
|
|
|
108
138
|
/** Plugin row id; the bundle patch inserts it under this id. */
|
|
109
139
|
declare const name = "model-router";
|
|
140
|
+
/**
|
|
141
|
+
* Opt-in vision route. Defaults are OFF on `deepseek-v4-flash-vision-exp` from
|
|
142
|
+
* `deepseek-official`; `reasoningEffort` and `maxTokens` are optional pins.
|
|
143
|
+
*/
|
|
144
|
+
declare const VisionRouteSchema: z<Schemastery.ObjectS<{
|
|
145
|
+
enabled: z<boolean, boolean>;
|
|
146
|
+
provider: z<string, string>;
|
|
147
|
+
model: z<string, string>;
|
|
148
|
+
reasoningEffort: z<"off" | "low" | "high" | "max", "off" | "low" | "high" | "max">;
|
|
149
|
+
maxTokens: z<number, number>;
|
|
150
|
+
}>, Schemastery.ObjectT<{
|
|
151
|
+
enabled: z<boolean, boolean>;
|
|
152
|
+
provider: z<string, string>;
|
|
153
|
+
model: z<string, string>;
|
|
154
|
+
reasoningEffort: z<"off" | "low" | "high" | "max", "off" | "low" | "high" | "max">;
|
|
155
|
+
maxTokens: z<number, number>;
|
|
156
|
+
}>>;
|
|
110
157
|
/** The plugin's public config, validated at row load. */
|
|
111
158
|
declare const Config: z<Schemastery.ObjectS<{
|
|
112
159
|
planner: z<Schemastery.ObjectS<{
|
|
@@ -147,6 +194,19 @@ declare const Config: z<Schemastery.ObjectS<{
|
|
|
147
194
|
enabled: z<boolean, boolean>;
|
|
148
195
|
promptSection: z<boolean, boolean>;
|
|
149
196
|
skill: z<boolean, boolean>;
|
|
197
|
+
vision: z<Schemastery.ObjectS<{
|
|
198
|
+
enabled: z<boolean, boolean>;
|
|
199
|
+
provider: z<string, string>;
|
|
200
|
+
model: z<string, string>;
|
|
201
|
+
reasoningEffort: z<"off" | "low" | "high" | "max", "off" | "low" | "high" | "max">;
|
|
202
|
+
maxTokens: z<number, number>;
|
|
203
|
+
}>, Schemastery.ObjectT<{
|
|
204
|
+
enabled: z<boolean, boolean>;
|
|
205
|
+
provider: z<string, string>;
|
|
206
|
+
model: z<string, string>;
|
|
207
|
+
reasoningEffort: z<"off" | "low" | "high" | "max", "off" | "low" | "high" | "max">;
|
|
208
|
+
maxTokens: z<number, number>;
|
|
209
|
+
}>>;
|
|
150
210
|
}>, Schemastery.ObjectT<{
|
|
151
211
|
planner: z<Schemastery.ObjectS<{
|
|
152
212
|
provider: z<string, string>;
|
|
@@ -186,6 +246,19 @@ declare const Config: z<Schemastery.ObjectS<{
|
|
|
186
246
|
enabled: z<boolean, boolean>;
|
|
187
247
|
promptSection: z<boolean, boolean>;
|
|
188
248
|
skill: z<boolean, boolean>;
|
|
249
|
+
vision: z<Schemastery.ObjectS<{
|
|
250
|
+
enabled: z<boolean, boolean>;
|
|
251
|
+
provider: z<string, string>;
|
|
252
|
+
model: z<string, string>;
|
|
253
|
+
reasoningEffort: z<"off" | "low" | "high" | "max", "off" | "low" | "high" | "max">;
|
|
254
|
+
maxTokens: z<number, number>;
|
|
255
|
+
}>, Schemastery.ObjectT<{
|
|
256
|
+
enabled: z<boolean, boolean>;
|
|
257
|
+
provider: z<string, string>;
|
|
258
|
+
model: z<string, string>;
|
|
259
|
+
reasoningEffort: z<"off" | "low" | "high" | "max", "off" | "low" | "high" | "max">;
|
|
260
|
+
maxTokens: z<number, number>;
|
|
261
|
+
}>>;
|
|
189
262
|
}>>;
|
|
190
263
|
/** Settings namespace the live on/off toggle lives under (settings.yaml). */
|
|
191
264
|
declare const SETTINGS_NS: _deepseek_ai_dsh_settings.SettingsNamespace;
|
|
@@ -261,4 +334,4 @@ declare class ModelRouter extends Service {
|
|
|
261
334
|
private render;
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
export { type AgentRole, Config, type ModelRoute, ModelRouter, ROW_ID, type ReasoningEffort, type RouterConfig, type RoutingMode, SETTINGS_NS, SKILL_CONTENT, SKILL_DESCRIPTION, SKILL_NAME, SKILL_WHEN_TO_USE, ModelRouter as default, effortFor, name, recentStepsHadError, roleFor, routeFor };
|
|
337
|
+
export { type AgentRole, Config, type ModelRoute, ModelRouter, ROW_ID, type ReasoningEffort, type RouterConfig, type RoutingMode, SETTINGS_NS, SKILL_CONTENT, SKILL_DESCRIPTION, SKILL_NAME, SKILL_WHEN_TO_USE, type VisionRoute, VisionRouteSchema, ModelRouter as default, effortFor, hasImageContent, name, recentStepsHadError, roleFor, routeFor };
|
package/lib/index.js
CHANGED
|
@@ -44,6 +44,23 @@ function effortFor(route, events) {
|
|
|
44
44
|
}
|
|
45
45
|
return route.reasoningEffort;
|
|
46
46
|
}
|
|
47
|
+
function hasImageContent(messages) {
|
|
48
|
+
if (!Array.isArray(messages)) return false;
|
|
49
|
+
for (const message of messages) {
|
|
50
|
+
const content = message?.content;
|
|
51
|
+
if (typeof content === "string") continue;
|
|
52
|
+
if (Array.isArray(content) && blocksContainImage(content)) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function blocksContainImage(blocks) {
|
|
57
|
+
for (const block of blocks) {
|
|
58
|
+
const candidate = block;
|
|
59
|
+
if (candidate?.type === "image") return true;
|
|
60
|
+
if (candidate?.type === "tool-result" && Array.isArray(candidate.content) && blocksContainImage(candidate.content)) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
47
64
|
|
|
48
65
|
// src/index.ts
|
|
49
66
|
var name = "model-router";
|
|
@@ -56,6 +73,13 @@ var ModelRouteSchema = z.object({
|
|
|
56
73
|
escalateTo: z.union(["off", "low", "high", "max"]),
|
|
57
74
|
recoverySteps: z.number().min(1)
|
|
58
75
|
});
|
|
76
|
+
var VisionRouteSchema = z.object({
|
|
77
|
+
enabled: z.boolean().default(false),
|
|
78
|
+
provider: z.string().min(1).default("deepseek-official"),
|
|
79
|
+
model: z.string().min(1).default("deepseek-v4-flash-vision-exp"),
|
|
80
|
+
reasoningEffort: z.union(["off", "low", "high", "max"]),
|
|
81
|
+
maxTokens: z.number().min(1)
|
|
82
|
+
});
|
|
59
83
|
var Config = z.object({
|
|
60
84
|
planner: ModelRouteSchema.default({
|
|
61
85
|
provider: "deepseek-official",
|
|
@@ -68,7 +92,8 @@ var Config = z.object({
|
|
|
68
92
|
mode: z.union(["strict", "plan"]).default("strict"),
|
|
69
93
|
enabled: z.boolean().default(true),
|
|
70
94
|
promptSection: z.boolean().default(true),
|
|
71
|
-
skill: z.boolean().default(true)
|
|
95
|
+
skill: z.boolean().default(true),
|
|
96
|
+
vision: VisionRouteSchema.default({})
|
|
72
97
|
});
|
|
73
98
|
var SETTINGS_NS = settingsNamespace("model-router");
|
|
74
99
|
function resolveConfig(raw) {
|
|
@@ -79,11 +104,12 @@ function resolveConfig(raw) {
|
|
|
79
104
|
mode: parsed.mode,
|
|
80
105
|
enabled: parsed.enabled,
|
|
81
106
|
promptSection: parsed.promptSection,
|
|
82
|
-
skill: parsed.skill
|
|
107
|
+
skill: parsed.skill,
|
|
108
|
+
vision: parsed.vision
|
|
83
109
|
};
|
|
84
110
|
}
|
|
85
111
|
var SECTION_ORDER = -50;
|
|
86
|
-
var SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL}; implementation runs on {EXECUTOR_MODEL}. You are the root agent: plan, design, review subagent output, and write the final answer here. Delegate implementation \u2014 writing code, running commands, builds, tests \u2014 to subagents with complete, self-contained prompts, preferring background delegation for independent work. Keep plans and replies concise. Do not hand-write large amounts of code or run long executions here; delegate instead
|
|
112
|
+
var SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL}; implementation runs on {EXECUTOR_MODEL}. You are the root agent: plan, design, review subagent output, and write the final answer here. Delegate implementation \u2014 writing code, running commands, builds, tests \u2014 to subagents with complete, self-contained prompts, preferring background delegation for independent work. Keep plans and replies concise. Do not hand-write large amounts of code or run long executions here; delegate instead.{VISION_LINE}`;
|
|
87
113
|
var SKILL_NAME = "pro-flash-routing";
|
|
88
114
|
var SKILL_DESCRIPTION = "Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.";
|
|
89
115
|
var SKILL_WHEN_TO_USE = `Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.`;
|
|
@@ -149,6 +175,17 @@ var ModelRouter = class extends Service {
|
|
|
149
175
|
const resolved = await next();
|
|
150
176
|
const cfg = this.source();
|
|
151
177
|
if (!cfg.enabled) return resolved;
|
|
178
|
+
if (cfg.vision.enabled && hasImageContent(resolved.messages)) {
|
|
179
|
+
const stamped2 = {
|
|
180
|
+
...resolved,
|
|
181
|
+
provider: cfg.vision.provider,
|
|
182
|
+
model: cfg.vision.model
|
|
183
|
+
};
|
|
184
|
+
if (cfg.vision.maxTokens !== void 0) stamped2.maxTokens = cfg.vision.maxTokens;
|
|
185
|
+
const effort2 = effortFor(cfg.vision, agent.session?.events);
|
|
186
|
+
if (effort2 !== void 0) stamped2.reasoningEffort = effort2;
|
|
187
|
+
return stamped2;
|
|
188
|
+
}
|
|
152
189
|
const route = routeFor(agent, cfg, isPlanModeActive(agent));
|
|
153
190
|
if (route === void 0) return resolved;
|
|
154
191
|
const stamped = {
|
|
@@ -201,9 +238,9 @@ var ModelRouter = class extends Service {
|
|
|
201
238
|
this.promptDispose = this.harness.systemPrompt.section({
|
|
202
239
|
name: ROW_ID,
|
|
203
240
|
order: SECTION_ORDER,
|
|
204
|
-
text: SECTION_TEXT.replaceAll("{PLANNER_MODEL}", cfg.planner.model).replaceAll(
|
|
205
|
-
"{
|
|
206
|
-
cfg.
|
|
241
|
+
text: SECTION_TEXT.replaceAll("{PLANNER_MODEL}", cfg.planner.model).replaceAll("{EXECUTOR_MODEL}", cfg.executor.model).replace(
|
|
242
|
+
"{VISION_LINE}",
|
|
243
|
+
cfg.vision.enabled ? ` Image-bearing requests route to ${cfg.vision.model}.` : ""
|
|
207
244
|
)
|
|
208
245
|
});
|
|
209
246
|
}
|
|
@@ -227,8 +264,10 @@ export {
|
|
|
227
264
|
SKILL_DESCRIPTION,
|
|
228
265
|
SKILL_NAME,
|
|
229
266
|
SKILL_WHEN_TO_USE,
|
|
267
|
+
VisionRouteSchema,
|
|
230
268
|
ModelRouter as default,
|
|
231
269
|
effortFor,
|
|
270
|
+
hasImageContent,
|
|
232
271
|
name,
|
|
233
272
|
recentStepsHadError,
|
|
234
273
|
roleFor,
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/policy.ts"],"sourcesContent":["/**\n * dsh-model-router: role-based model routing for the DeepSeek Harness.\n *\n * The planner (the session's root agent) runs on `deepseek-v4-pro`; delegated\n * executor subagents run on `deepseek-v4-flash`. Enforcement is a per-agent\n * `agent/request` rewrite registered when the agent is created, so it applies\n * in every mode (web / headless / tui) and every agent preset, including\n * subagents the delegation tools create.\n *\n * Each role route may also pin `reasoningEffort` and `maxTokens`; when set,\n * they override the session's selection for that role. A `mode` switch lets a\n * deployment reserve the planner route for actual planning.\n *\n * The plugin also publishes:\n * - a system-prompt section stating the planner/executor convention, and\n * - the `pro-flash-routing` skill teaching the agent to plan itself and\n * delegate code execution to flash subagents.\n *\n * @module dsh-model-router\n */\nimport { Context, Service } from \"@deepseek-ai/cordis\";\nimport z from \"@deepseek-ai/schemastery\";\nimport { foldPlanMode } from \"@deepseek-ai/dsh-plan-mode\";\nimport { installSettingsSection, settingsNamespace } from \"@deepseek-ai/dsh-settings\";\nimport { effortFor, recentStepsHadError, roleFor, routeFor, type RouterConfig } from \"./policy.js\";\n\n/** Plugin row id; the bundle patch inserts it under this id. */\nconst name = \"model-router\";\n\n/** One provider/model pair, with defaults and optional effort/token caps. */\nconst ModelRouteSchema = z.object({\n provider: z.string().min(1),\n model: z.string().min(1),\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\n maxTokens: z.number().min(1),\n escalateOnError: z.boolean(),\n escalateTo: z.union([\"off\", \"low\", \"high\", \"max\"]),\n recoverySteps: z.number().min(1),\n});\n\n/** The plugin's public config, validated at row load. */\nconst Config = z.object({\n planner: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-v4-pro\",\n } as never),\n executor: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-v4-flash\",\n } as never),\n mode: z.union([\"strict\", \"plan\"]).default(\"strict\"),\n enabled: z.boolean().default(true),\n promptSection: z.boolean().default(true),\n skill: z.boolean().default(true),\n});\n\n/** Settings namespace the live on/off toggle lives under (settings.yaml). */\nconst SETTINGS_NS = settingsNamespace(\"model-router\");\n\n/**\n * Resolve raw row config into the internal shape, failing loud on garbage.\n * Schemastery schemas are callable: invoking validates and applies defaults.\n * @param raw - the row's config object.\n * @returns the validated RouterConfig.\n */\nfunction resolveConfig(raw: unknown): RouterConfig {\n const parsed = Config(raw ?? {});\n return {\n planner: parsed.planner,\n executor: parsed.executor,\n mode: parsed.mode,\n enabled: parsed.enabled,\n promptSection: parsed.promptSection,\n skill: parsed.skill,\n };\n}\n\n/**\n * Always-on guidance section. Negative order renders before the persona, so\n * the convention is established before the agent's identity line.\n */\nconst SECTION_ORDER = -50;\n\nconst SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL}; implementation runs on {EXECUTOR_MODEL}. You are the root agent: plan, design, review subagent output, and write the final answer here. Delegate implementation — writing code, running commands, builds, tests — to subagents with complete, self-contained prompts, preferring background delegation for independent work. Keep plans and replies concise. Do not hand-write large amounts of code or run long executions here; delegate instead.`;\n\nconst SKILL_NAME = \"pro-flash-routing\";\n\nconst SKILL_DESCRIPTION =\n \"Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.\";\n\nconst SKILL_WHEN_TO_USE = `Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.`;\n\nconst SKILL_CONTENT = `# Pro planner / Flash executor routing\n\nThis session routes models by role:\n\n- **Planner (this agent)** — \\`deepseek-v4-pro\\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.\n- **Executors (every subagent)** — \\`deepseek-v4-flash\\`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.\n\n## Working rhythm\n\n1. **Plan here.** Explore, decide the approach, and (when plan mode is on) submit the plan with \\`exit_plan_mode\\`. The plan stays on this agent.\n2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to \\`deepseek-v4-flash\\`, so keep them execution-focused: give them the decision, not the decision to make.\n3. **Review here.** Read the subagent's result on this agent, verify it yourself (tests, diffs, logs), and iterate with follow-up messages to the same subagent when available.\n4. **Report here.** Summaries, plans, and answers to the user come from this agent.\n\n## Keep this agent's context lean\n\nInput tokens are the expensive part of the planner. Don't re-read large files or full transcripts on this agent — trust the subagent's final report. Prefer targeted reads (offset/limit) over whole files. When the context grows, compact rather than re-sending everything.\n\n## Delegation guidelines\n\n- Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).\n- Prefer \\`subagent\\` for self-contained work and \\`workflow\\` when many independent pieces need fan-out; their workers run on flash as well.\n- Do not delegate design: subagents execute decisions already made.\n- If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.\n\n## Verification\n\n- Executor output was produced by \\`deepseek-v4-flash\\`; planner output by \\`deepseek-v4-pro\\`. If you need to confirm, check the session log's model metadata.\n- If routing ever looks wrong, the \\`model-router\\` plugin row in the profile composition is the single place that owns it.`;\n\n/** The plugin row id the bundle patch must insert. */\nconst ROW_ID = \"model-router\";\n\n/** Minimal structural view of the live agent object the router reads. */\ninterface AgentLike {\n ctx: AgentScopedContext;\n options?: { subagentDepth?: number };\n session?: { header?: { origin?: string }; events?: unknown[] };\n}\n\n/** The agent-scoped context's waterfall surface the router uses. */\ninterface AgentScopedContext {\n on(\n event: \"agent/request\",\n listener: (\n payload: Record<string, unknown>,\n next: () => Promise<Record<string, unknown>>,\n ) => Promise<Record<string, unknown>>,\n options?: { prepend?: boolean },\n ): () => void;\n}\n\n/** Host-plane surface the router consumes (events, prompt registry, skills). */\ninterface HarnessContext {\n on(\n event: \"agent/created\",\n listener: (payload: { agent: AgentLike }) => void,\n ): () => void;\n on(event: \"agent/disposed\", listener: (agent: unknown) => void): () => void;\n systemPrompt: {\n section(section: { name: string; order: number; text: string }): () => void;\n };\n skills: {\n register(skill: {\n name: string;\n description: string;\n whenToUse?: string;\n content: string;\n source: string;\n }): () => void;\n };\n}\n\n/** Fold plan-mode state for an agent without trusting the agent's exact shape. */\nfunction isPlanModeActive(agent: AgentLike): boolean {\n const events = agent.session?.events;\n if (!Array.isArray(events)) return false;\n try {\n return foldPlanMode(events as Parameters<typeof foldPlanMode>[0]);\n } catch {\n return false;\n }\n}\n\n/**\n * Cordis service: per-agent request routing plus the convention surface.\n */\nclass ModelRouter extends Service {\n static inject = [\"skills\", \"systemPrompt\"];\n\n config: RouterConfig;\n\n /** Currently authoritative config; swapped by the settings section when one is mounted. */\n source: () => RouterConfig;\n\n /** Host-plane surface the router consumes (events, prompt registry, skills). */\n harness: HarnessContext;\n\n /** Disposer of the currently registered prompt section, if any. */\n promptDispose?: () => void;\n\n /** Disposer of the currently registered skill, if any. */\n skillDispose?: () => void;\n\n constructor(ctx: Context, rawConfig: unknown = {}) {\n super(ctx, \"modelRouter\");\n this.config = resolveConfig(rawConfig);\n this.source = () => this.config;\n this.harness = ctx as unknown as HarnessContext;\n\n // Every agent that gets created — root sessions, delegation children,\n // workflow workers, ralph rounds — passes through here.\n this.harness.on(\"agent/created\", ({ agent }) => {\n // `prepend` puts this listener OUTERMOST in the `agent/request`\n // waterfall: the harness's model-selection listener runs inside it, so\n // this rewrite is applied LAST and wins over the session's selected\n // model (which dsh-base defaults to deepseek-v4-flash and the user\n // settings or UI can change).\n const dispose = agent.ctx.on(\n \"agent/request\",\n async (payload, next) => {\n const resolved = await next();\n const cfg = this.source();\n if (!cfg.enabled) return resolved;\n const route = routeFor(agent, cfg, isPlanModeActive(agent));\n if (route === undefined) return resolved;\n const stamped: Record<string, unknown> = {\n ...resolved,\n provider: route.provider,\n model: route.model,\n };\n if (route.maxTokens !== undefined) stamped.maxTokens = route.maxTokens;\n const effort = effortFor(route, agent.session?.events);\n if (effort !== undefined) stamped.reasoningEffort = effort;\n return stamped;\n },\n { prepend: true },\n );\n this.harness.on(\"agent/disposed\", (disposed) => {\n if (disposed === agent) dispose();\n });\n });\n\n // Register the convention surface from the composition config, then let\n // the settings section (when mounted) take over as the live source.\n this.render(this.config);\n installSettingsSection(\n ctx,\n SETTINGS_NS,\n Config,\n this.config as unknown as ReturnType<typeof Config>,\n {\n setSource: (current) => {\n this.source = current;\n },\n onChange: () => this.render(this.source()),\n },\n );\n }\n\n /**\n * Register the convention surface (prompt section + skill) for `cfg`,\n * replacing whatever is currently registered. Called on construction and\n * after every committed settings change; while `enabled` is false, nothing\n * stays registered.\n */\n private render(cfg: RouterConfig): void {\n if (this.promptDispose) {\n this.promptDispose();\n this.promptDispose = undefined;\n }\n if (this.skillDispose) {\n this.skillDispose();\n this.skillDispose = undefined;\n }\n if (!cfg.enabled) return;\n if (cfg.promptSection) {\n this.promptDispose = this.harness.systemPrompt.section({\n name: ROW_ID,\n order: SECTION_ORDER,\n text: SECTION_TEXT.replaceAll(\"{PLANNER_MODEL}\", cfg.planner.model).replaceAll(\n \"{EXECUTOR_MODEL}\",\n cfg.executor.model,\n ),\n });\n }\n if (cfg.skill) {\n this.skillDispose = this.harness.skills.register({\n name: SKILL_NAME,\n description: SKILL_DESCRIPTION,\n whenToUse: SKILL_WHEN_TO_USE,\n content: SKILL_CONTENT,\n source: \"runtime\",\n });\n }\n }\n}\n\nexport {\n Config,\n ModelRouter,\n ModelRouter as default,\n SETTINGS_NS,\n name,\n ROW_ID,\n SKILL_CONTENT,\n SKILL_DESCRIPTION,\n SKILL_NAME,\n SKILL_WHEN_TO_USE,\n effortFor,\n recentStepsHadError,\n roleFor,\n routeFor,\n};\nexport type {\n AgentRole,\n ModelRoute,\n ReasoningEffort,\n RoutingMode,\n RouterConfig,\n} from \"./policy.js\";\n","/**\n * Pure routing policy for dsh-model-router: which model each agent role gets.\n * Kept free of Cordis imports so the policy is trivially unit-testable.\n * @module dsh-model-router/policy\n */\n\n/** Reasoning-effort levels a route may pin (mirrors the harness vocabulary). */\nexport type ReasoningEffort = \"off\" | \"low\" | \"high\" | \"max\";\n\n/**\n * How the router treats the root agent.\n * - `strict`: the root agent is always the planner (pro).\n * - `plan`: the root agent is pro only while plan mode is active; otherwise it\n * falls back to the executor route, reserving pro for real planning.\n */\nexport type RoutingMode = \"strict\" | \"plan\";\n\n/** One route: a provider/model pair stamped onto an agent request. */\nexport interface ModelRoute {\n provider: string;\n model: string;\n /**\n * Optional reasoning-effort override. When omitted, the request inherits the\n * session's own selection; when set, the router pins it for that role.\n */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap for the role; omitted means inherit. */\n maxTokens?: number;\n /**\n * Error-driven escalation (v1): when true, a failed execution step bumps the\n * next request's effort to `escalateTo`, wearing off after `recoverySteps`\n * clean steps. Deterministic, stateless — the session log is folded per\n * request, so only *prior* steps are ever considered.\n */\n escalateOnError?: boolean;\n /** Effort used for the request after a failed step. */\n escalateTo?: ReasoningEffort;\n /** Clean steps before escalation wears off. Defaults to 2. */\n recoverySteps?: number;\n}\n\n/** The two roles the router distinguishes. */\nexport type AgentRole = \"planner\" | \"executor\";\n\n/** Resolved router configuration: one route per role plus routing mode. */\nexport interface RouterConfig {\n planner: ModelRoute;\n executor: ModelRoute;\n mode: RoutingMode;\n /**\n * Live off-switch. Defaults to true; settable from Settings → Plugins →\n * model-router (applies immediately) or from the patch row (next boot).\n * When false, the router stops rewriting requests and unregisters the\n * prompt section and the skill.\n */\n enabled: boolean;\n promptSection: boolean;\n skill: boolean;\n}\n\n/** Default recovery window: an error escalates for the next two completed steps. */\nexport const DEFAULT_RECOVERY_STEPS = 2;\n\n/**\n * Classify an agent as planner or executor.\n *\n * The main (root) agent of a session is the planner. Every agent created as a\n * delegation child — `subagent`, `subagent_fork`, workflow workers, ralph\n * rounds — is an executor. The harness stamps two durable facts on children:\n * `options.subagentDepth` (>= 1) and the session header `origin: \"subagent\"`.\n *\n * @param agent - the live agent (any subset of the runtime shape).\n * @returns the role the agent should be routed as.\n */\nexport function roleFor(agent: unknown): AgentRole {\n const options = (agent as { options?: unknown })?.options;\n const depth = (options as { subagentDepth?: unknown })?.subagentDepth;\n if (typeof depth === \"number\" && depth >= 1) return \"executor\";\n const session = (agent as { session?: unknown })?.session;\n const origin = (session as { header?: unknown })?.header\n ? ((session as { header: { origin?: unknown } }).header.origin)\n : undefined;\n if (origin === \"subagent\") return \"executor\";\n return \"planner\";\n}\n\n/**\n * Resolve the route for one agent.\n * @param agent - the live agent.\n * @param config - the resolved router configuration.\n * @param planModeActive - whether plan mode is currently folded active for the\n * agent's session; consulted only in `plan` routing mode.\n * @returns the model route to stamp, or `undefined` to leave the request alone.\n */\nexport function routeFor(\n agent: unknown,\n config: RouterConfig,\n planModeActive = false,\n): ModelRoute | undefined {\n const role = roleFor(agent);\n if (role === \"executor\") return config.executor;\n // Root agent. In `plan` mode, reserve the planner route for actual planning;\n // otherwise the root falls back to the executor route.\n if (config.mode === \"plan\" && !planModeActive) return config.executor;\n return config.planner;\n}\n\n/**\n * Whether any of the last `recoverySteps` completed steps carried a failed\n * tool result. A failure is a `tool/result` event whose data carries an\n * `error` field (the harness records tool failures there).\n *\n * Steps are deduplicated by `turn:step`, and only *completed* steps count —\n * events are scanned from the tail, so the current in-flight request is never\n * considered.\n *\n * @param events - the agent's session event log (or `undefined`).\n * @param recoverySteps - how many completed steps back to scan.\n * @returns true when a failed step is within the window.\n */\nexport function recentStepsHadError(\n events: readonly unknown[] | undefined,\n recoverySteps: number = DEFAULT_RECOVERY_STEPS,\n): boolean {\n if (!Array.isArray(events) || recoverySteps <= 0) return false;\n const seen = new Set<string>();\n let steps = 0;\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i] as\n | { type?: string; data?: { turn?: number; step?: number; error?: unknown } }\n | undefined;\n if (event?.type !== \"tool/result\" || event.data === undefined) continue;\n const key = `${event.data.turn}:${event.data.step}`;\n if (!seen.has(key)) {\n // A new step beyond the recovery window ends the scan; events for steps\n // already inside the window are still checked below.\n if (steps >= recoverySteps) break;\n seen.add(key);\n steps += 1;\n }\n if (event.data.error !== undefined && event.data.error !== null) return true;\n }\n return false;\n}\n\n/**\n * Resolve the reasoning effort to stamp for one request.\n *\n * Baseline is the route's `reasoningEffort`; when `escalateOnError` is enabled\n * and a recent step failed, the effort bumps to `escalateTo` (falling back to\n * the baseline when `escalateTo` is unset). Returns `undefined` to leave the\n * request's effort alone (inherit the session selection).\n *\n * @param route - the resolved route for the agent.\n * @param events - the agent's session event log.\n * @returns the effort to stamp, or `undefined` to inherit.\n */\nexport function effortFor(\n route: ModelRoute,\n events: readonly unknown[] | undefined,\n): ReasoningEffort | undefined {\n if (route.escalateOnError === true && recentStepsHadError(events, route.recoverySteps)) {\n return route.escalateTo ?? route.reasoningEffort;\n }\n return route.reasoningEffort;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/policy.ts"],"sourcesContent":["/**\n * dsh-model-router: role-based model routing for the DeepSeek Harness.\n *\n * The planner (the session's root agent) runs on `deepseek-v4-pro`; delegated\n * executor subagents run on `deepseek-v4-flash`. Enforcement is a per-agent\n * `agent/request` rewrite registered when the agent is created, so it applies\n * in every mode (web / headless / tui) and every agent preset, including\n * subagents the delegation tools create.\n *\n * Each role route may also pin `reasoningEffort` and `maxTokens`; when set,\n * they override the session's selection for that role. A `mode` switch lets a\n * deployment reserve the planner route for actual planning.\n *\n * The plugin also publishes:\n * - a system-prompt section stating the planner/executor convention, and\n * - the `pro-flash-routing` skill teaching the agent to plan itself and\n * delegate code execution to flash subagents.\n *\n * @module dsh-model-router\n */\nimport { Context, Service } from \"@deepseek-ai/cordis\";\nimport z from \"@deepseek-ai/schemastery\";\nimport { foldPlanMode } from \"@deepseek-ai/dsh-plan-mode\";\nimport { installSettingsSection, settingsNamespace } from \"@deepseek-ai/dsh-settings\";\nimport { effortFor, hasImageContent, recentStepsHadError, roleFor, routeFor, type RouterConfig } from \"./policy.js\";\n\n/** Plugin row id; the bundle patch inserts it under this id. */\nconst name = \"model-router\";\n\n/** One provider/model pair, with defaults and optional effort/token caps. */\nconst ModelRouteSchema = z.object({\n provider: z.string().min(1),\n model: z.string().min(1),\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\n maxTokens: z.number().min(1),\n escalateOnError: z.boolean(),\n escalateTo: z.union([\"off\", \"low\", \"high\", \"max\"]),\n recoverySteps: z.number().min(1),\n});\n\n/**\n * Opt-in vision route. Defaults are OFF on `deepseek-v4-flash-vision-exp` from\n * `deepseek-official`; `reasoningEffort` and `maxTokens` are optional pins.\n */\nconst VisionRouteSchema = z.object({\n enabled: z.boolean().default(false),\n provider: z.string().min(1).default(\"deepseek-official\"),\n model: z.string().min(1).default(\"deepseek-v4-flash-vision-exp\"),\n reasoningEffort: z.union([\"off\", \"low\", \"high\", \"max\"]),\n maxTokens: z.number().min(1),\n});\n\n/** The plugin's public config, validated at row load. */\nconst Config = z.object({\n planner: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-v4-pro\",\n } as never),\n executor: ModelRouteSchema.default({\n provider: \"deepseek-official\",\n model: \"deepseek-v4-flash\",\n } as never),\n mode: z.union([\"strict\", \"plan\"]).default(\"strict\"),\n enabled: z.boolean().default(true),\n promptSection: z.boolean().default(true),\n skill: z.boolean().default(true),\n vision: VisionRouteSchema.default({} as never),\n});\n\n/** Settings namespace the live on/off toggle lives under (settings.yaml). */\nconst SETTINGS_NS = settingsNamespace(\"model-router\");\n\n/**\n * Resolve raw row config into the internal shape, failing loud on garbage.\n * Schemastery schemas are callable: invoking validates and applies defaults.\n * @param raw - the row's config object.\n * @returns the validated RouterConfig.\n */\nfunction resolveConfig(raw: unknown): RouterConfig {\n const parsed = Config(raw ?? {});\n return {\n planner: parsed.planner,\n executor: parsed.executor,\n mode: parsed.mode,\n enabled: parsed.enabled,\n promptSection: parsed.promptSection,\n skill: parsed.skill,\n vision: parsed.vision,\n };\n}\n\n/**\n * Always-on guidance section. Negative order renders before the persona, so\n * the convention is established before the agent's identity line.\n */\nconst SECTION_ORDER = -50;\n\nconst SECTION_TEXT = `Model routing is role-based. Planning runs on {PLANNER_MODEL}; implementation runs on {EXECUTOR_MODEL}. You are the root agent: plan, design, review subagent output, and write the final answer here. Delegate implementation — writing code, running commands, builds, tests — to subagents with complete, self-contained prompts, preferring background delegation for independent work. Keep plans and replies concise. Do not hand-write large amounts of code or run long executions here; delegate instead.{VISION_LINE}`;\n\nconst SKILL_NAME = \"pro-flash-routing\";\n\nconst SKILL_DESCRIPTION =\n \"Route planning and code execution across models: plan on the pro planner agent, delegate implementation to flash executor subagents.\";\n\nconst SKILL_WHEN_TO_USE = `Use when a task combines planning and implementation: before writing code, after a plan is approved, when delegating execution work, or when the user asks about the pro/flash routing convention.`;\n\nconst SKILL_CONTENT = `# Pro planner / Flash executor routing\n\nThis session routes models by role:\n\n- **Planner (this agent)** — \\`deepseek-v4-pro\\`. Planning, design decisions, reviewing delegated output, and user-facing synthesis happen here.\n- **Executors (every subagent)** — \\`deepseek-v4-flash\\`. Implementation work happens there: writing code, running commands, builds, and tests. The harness forces the model automatically; you do not select it.\n\n## Working rhythm\n\n1. **Plan here.** Explore, decide the approach, and (when plan mode is on) submit the plan with \\`exit_plan_mode\\`. The plan stays on this agent.\n2. **Delegate the execution.** Once a plan is approved, hand each self-contained chunk of implementation to a subagent with a complete prompt: exact files to touch, the change to make, and how to verify. Subagents are automatically routed to \\`deepseek-v4-flash\\`, so keep them execution-focused: give them the decision, not the decision to make.\n3. **Review here.** Read the subagent's result on this agent, verify it yourself (tests, diffs, logs), and iterate with follow-up messages to the same subagent when available.\n4. **Report here.** Summaries, plans, and answers to the user come from this agent.\n\n## Keep this agent's context lean\n\nInput tokens are the expensive part of the planner. Don't re-read large files or full transcripts on this agent — trust the subagent's final report. Prefer targeted reads (offset/limit) over whole files. When the context grows, compact rather than re-sending everything.\n\n## Delegation guidelines\n\n- Start independent delegations together in one assistant message and continue useful work while they run (background mode by default).\n- Prefer \\`subagent\\` for self-contained work and \\`workflow\\` when many independent pieces need fan-out; their workers run on flash as well.\n- Do not delegate design: subagents execute decisions already made.\n- If a subagent's task grows into design work, pull it back to this agent and re-delegate the narrowed execution.\n\n## Verification\n\n- Executor output was produced by \\`deepseek-v4-flash\\`; planner output by \\`deepseek-v4-pro\\`. If you need to confirm, check the session log's model metadata.\n- If routing ever looks wrong, the \\`model-router\\` plugin row in the profile composition is the single place that owns it.`;\n\n/** The plugin row id the bundle patch must insert. */\nconst ROW_ID = \"model-router\";\n\n/** Minimal structural view of the live agent object the router reads. */\ninterface AgentLike {\n ctx: AgentScopedContext;\n options?: { subagentDepth?: number };\n session?: { header?: { origin?: string }; events?: unknown[] };\n}\n\n/** The agent-scoped context's waterfall surface the router uses. */\ninterface AgentScopedContext {\n on(\n event: \"agent/request\",\n listener: (\n payload: Record<string, unknown>,\n next: () => Promise<Record<string, unknown>>,\n ) => Promise<Record<string, unknown>>,\n options?: { prepend?: boolean },\n ): () => void;\n}\n\n/** Host-plane surface the router consumes (events, prompt registry, skills). */\ninterface HarnessContext {\n on(\n event: \"agent/created\",\n listener: (payload: { agent: AgentLike }) => void,\n ): () => void;\n on(event: \"agent/disposed\", listener: (agent: unknown) => void): () => void;\n systemPrompt: {\n section(section: { name: string; order: number; text: string }): () => void;\n };\n skills: {\n register(skill: {\n name: string;\n description: string;\n whenToUse?: string;\n content: string;\n source: string;\n }): () => void;\n };\n}\n\n/** Fold plan-mode state for an agent without trusting the agent's exact shape. */\nfunction isPlanModeActive(agent: AgentLike): boolean {\n const events = agent.session?.events;\n if (!Array.isArray(events)) return false;\n try {\n return foldPlanMode(events as Parameters<typeof foldPlanMode>[0]);\n } catch {\n return false;\n }\n}\n\n/**\n * Cordis service: per-agent request routing plus the convention surface.\n */\nclass ModelRouter extends Service {\n static inject = [\"skills\", \"systemPrompt\"];\n\n config: RouterConfig;\n\n /** Currently authoritative config; swapped by the settings section when one is mounted. */\n source: () => RouterConfig;\n\n /** Host-plane surface the router consumes (events, prompt registry, skills). */\n harness: HarnessContext;\n\n /** Disposer of the currently registered prompt section, if any. */\n promptDispose?: () => void;\n\n /** Disposer of the currently registered skill, if any. */\n skillDispose?: () => void;\n\n constructor(ctx: Context, rawConfig: unknown = {}) {\n super(ctx, \"modelRouter\");\n this.config = resolveConfig(rawConfig);\n this.source = () => this.config;\n this.harness = ctx as unknown as HarnessContext;\n\n // Every agent that gets created — root sessions, delegation children,\n // workflow workers, ralph rounds — passes through here.\n this.harness.on(\"agent/created\", ({ agent }) => {\n // `prepend` puts this listener OUTERMOST in the `agent/request`\n // waterfall: the harness's model-selection listener runs inside it, so\n // this rewrite is applied LAST and wins over the session's selected\n // model (which dsh-base defaults to deepseek-v4-flash and the user\n // settings or UI can change).\n const dispose = agent.ctx.on(\n \"agent/request\",\n async (payload, next) => {\n const resolved = await next();\n const cfg = this.source();\n if (!cfg.enabled) return resolved;\n // Vision branch FIRST: any request whose messages carry image\n // content is stamped with the vision model, regardless of role.\n // The harness's own model-selection listener has already run\n // (`await next()`), so this rewrite wins over the session model.\n if (\n cfg.vision.enabled &&\n hasImageContent((resolved as { messages?: unknown }).messages)\n ) {\n const stamped: Record<string, unknown> = {\n ...resolved,\n provider: cfg.vision.provider,\n model: cfg.vision.model,\n };\n if (cfg.vision.maxTokens !== undefined) stamped.maxTokens = cfg.vision.maxTokens;\n const effort = effortFor(cfg.vision, agent.session?.events);\n if (effort !== undefined) stamped.reasoningEffort = effort;\n return stamped;\n }\n const route = routeFor(agent, cfg, isPlanModeActive(agent));\n if (route === undefined) return resolved;\n const stamped: Record<string, unknown> = {\n ...resolved,\n provider: route.provider,\n model: route.model,\n };\n if (route.maxTokens !== undefined) stamped.maxTokens = route.maxTokens;\n const effort = effortFor(route, agent.session?.events);\n if (effort !== undefined) stamped.reasoningEffort = effort;\n return stamped;\n },\n { prepend: true },\n );\n this.harness.on(\"agent/disposed\", (disposed) => {\n if (disposed === agent) dispose();\n });\n });\n\n // Register the convention surface from the composition config, then let\n // the settings section (when mounted) take over as the live source.\n this.render(this.config);\n installSettingsSection(\n ctx,\n SETTINGS_NS,\n Config,\n this.config as unknown as ReturnType<typeof Config>,\n {\n setSource: (current) => {\n this.source = current;\n },\n onChange: () => this.render(this.source()),\n },\n );\n }\n\n /**\n * Register the convention surface (prompt section + skill) for `cfg`,\n * replacing whatever is currently registered. Called on construction and\n * after every committed settings change; while `enabled` is false, nothing\n * stays registered.\n */\n private render(cfg: RouterConfig): void {\n if (this.promptDispose) {\n this.promptDispose();\n this.promptDispose = undefined;\n }\n if (this.skillDispose) {\n this.skillDispose();\n this.skillDispose = undefined;\n }\n if (!cfg.enabled) return;\n if (cfg.promptSection) {\n this.promptDispose = this.harness.systemPrompt.section({\n name: ROW_ID,\n order: SECTION_ORDER,\n text: SECTION_TEXT.replaceAll(\"{PLANNER_MODEL}\", cfg.planner.model)\n .replaceAll(\"{EXECUTOR_MODEL}\", cfg.executor.model)\n .replace(\n \"{VISION_LINE}\",\n cfg.vision.enabled\n ? ` Image-bearing requests route to ${cfg.vision.model}.`\n : \"\",\n ),\n });\n }\n if (cfg.skill) {\n this.skillDispose = this.harness.skills.register({\n name: SKILL_NAME,\n description: SKILL_DESCRIPTION,\n whenToUse: SKILL_WHEN_TO_USE,\n content: SKILL_CONTENT,\n source: \"runtime\",\n });\n }\n }\n}\n\nexport {\n Config,\n ModelRouter,\n ModelRouter as default,\n SETTINGS_NS,\n VisionRouteSchema,\n name,\n ROW_ID,\n SKILL_CONTENT,\n SKILL_DESCRIPTION,\n SKILL_NAME,\n SKILL_WHEN_TO_USE,\n effortFor,\n hasImageContent,\n recentStepsHadError,\n roleFor,\n routeFor,\n};\nexport type {\n AgentRole,\n ModelRoute,\n ReasoningEffort,\n RoutingMode,\n RouterConfig,\n VisionRoute,\n} from \"./policy.js\";\n","/**\n * Pure routing policy for dsh-model-router: which model each agent role gets.\n * Kept free of Cordis imports so the policy is trivially unit-testable.\n * @module dsh-model-router/policy\n */\n\n/** Reasoning-effort levels a route may pin (mirrors the harness vocabulary). */\nexport type ReasoningEffort = \"off\" | \"low\" | \"high\" | \"max\";\n\n/**\n * How the router treats the root agent.\n * - `strict`: the root agent is always the planner (pro).\n * - `plan`: the root agent is pro only while plan mode is active; otherwise it\n * falls back to the executor route, reserving pro for real planning.\n */\nexport type RoutingMode = \"strict\" | \"plan\";\n\n/** One route: a provider/model pair stamped onto an agent request. */\nexport interface ModelRoute {\n provider: string;\n model: string;\n /**\n * Optional reasoning-effort override. When omitted, the request inherits the\n * session's own selection; when set, the router pins it for that role.\n */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap for the role; omitted means inherit. */\n maxTokens?: number;\n /**\n * Error-driven escalation (v1): when true, a failed execution step bumps the\n * next request's effort to `escalateTo`, wearing off after `recoverySteps`\n * clean steps. Deterministic, stateless — the session log is folded per\n * request, so only *prior* steps are ever considered.\n */\n escalateOnError?: boolean;\n /** Effort used for the request after a failed step. */\n escalateTo?: ReasoningEffort;\n /** Clean steps before escalation wears off. Defaults to 2. */\n recoverySteps?: number;\n}\n\n/** The two roles the router distinguishes. */\nexport type AgentRole = \"planner\" | \"executor\";\n\n/**\n * Opt-in vision route: when enabled, any request whose messages carry image\n * content is stamped with the vision model, from every role (root agent and\n * subagents alike). Everything else keeps the pro/flash role routing.\n */\nexport interface VisionRoute {\n /** Master switch. Defaults to false — vision routing is opt-in. */\n enabled: boolean;\n /** Provider the vision model is served from. */\n provider: string;\n /** The vision model to stamp onto image-bearing requests. */\n model: string;\n /** Optional reasoning-effort pin; omitted means inherit the session's. */\n reasoningEffort?: ReasoningEffort;\n /** Optional output-token cap; omitted means inherit the session's. */\n maxTokens?: number;\n}\n\n/** Resolved router configuration: one route per role plus routing mode. */\nexport interface RouterConfig {\n planner: ModelRoute;\n executor: ModelRoute;\n mode: RoutingMode;\n /**\n * Live off-switch. Defaults to true; settable from Settings → Plugins →\n * model-router (applies immediately) or from the patch row (next boot).\n * When false, the router stops rewriting requests and unregisters the\n * prompt section and the skill.\n */\n enabled: boolean;\n promptSection: boolean;\n skill: boolean;\n /** Opt-in vision routing for image-bearing requests (v0.6.0+). */\n vision: VisionRoute;\n}\n\n/** Default recovery window: an error escalates for the next two completed steps. */\nexport const DEFAULT_RECOVERY_STEPS = 2;\n\n/**\n * Classify an agent as planner or executor.\n *\n * The main (root) agent of a session is the planner. Every agent created as a\n * delegation child — `subagent`, `subagent_fork`, workflow workers, ralph\n * rounds — is an executor. The harness stamps two durable facts on children:\n * `options.subagentDepth` (>= 1) and the session header `origin: \"subagent\"`.\n *\n * @param agent - the live agent (any subset of the runtime shape).\n * @returns the role the agent should be routed as.\n */\nexport function roleFor(agent: unknown): AgentRole {\n const options = (agent as { options?: unknown })?.options;\n const depth = (options as { subagentDepth?: unknown })?.subagentDepth;\n if (typeof depth === \"number\" && depth >= 1) return \"executor\";\n const session = (agent as { session?: unknown })?.session;\n const origin = (session as { header?: unknown })?.header\n ? ((session as { header: { origin?: unknown } }).header.origin)\n : undefined;\n if (origin === \"subagent\") return \"executor\";\n return \"planner\";\n}\n\n/**\n * Resolve the route for one agent.\n * @param agent - the live agent.\n * @param config - the resolved router configuration.\n * @param planModeActive - whether plan mode is currently folded active for the\n * agent's session; consulted only in `plan` routing mode.\n * @returns the model route to stamp, or `undefined` to leave the request alone.\n */\nexport function routeFor(\n agent: unknown,\n config: RouterConfig,\n planModeActive = false,\n): ModelRoute | undefined {\n const role = roleFor(agent);\n if (role === \"executor\") return config.executor;\n // Root agent. In `plan` mode, reserve the planner route for actual planning;\n // otherwise the root falls back to the executor route.\n if (config.mode === \"plan\" && !planModeActive) return config.executor;\n return config.planner;\n}\n\n/**\n * Whether any of the last `recoverySteps` completed steps carried a failed\n * tool result. A failure is a `tool/result` event whose data carries an\n * `error` field (the harness records tool failures there).\n *\n * Steps are deduplicated by `turn:step`, and only *completed* steps count —\n * events are scanned from the tail, so the current in-flight request is never\n * considered.\n *\n * @param events - the agent's session event log (or `undefined`).\n * @param recoverySteps - how many completed steps back to scan.\n * @returns true when a failed step is within the window.\n */\nexport function recentStepsHadError(\n events: readonly unknown[] | undefined,\n recoverySteps: number = DEFAULT_RECOVERY_STEPS,\n): boolean {\n if (!Array.isArray(events) || recoverySteps <= 0) return false;\n const seen = new Set<string>();\n let steps = 0;\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i] as\n | { type?: string; data?: { turn?: number; step?: number; error?: unknown } }\n | undefined;\n if (event?.type !== \"tool/result\" || event.data === undefined) continue;\n const key = `${event.data.turn}:${event.data.step}`;\n if (!seen.has(key)) {\n // A new step beyond the recovery window ends the scan; events for steps\n // already inside the window are still checked below.\n if (steps >= recoverySteps) break;\n seen.add(key);\n steps += 1;\n }\n if (event.data.error !== undefined && event.data.error !== null) return true;\n }\n return false;\n}\n\n/**\n * Resolve the reasoning effort to stamp for one request.\n *\n * Baseline is the route's `reasoningEffort`; when `escalateOnError` is enabled\n * and a recent step failed, the effort bumps to `escalateTo` (falling back to\n * the baseline when `escalateTo` is unset). Returns `undefined` to leave the\n * request's effort alone (inherit the session selection).\n *\n * @param route - the resolved route for the agent.\n * @param events - the agent's session event log.\n * @returns the effort to stamp, or `undefined` to inherit.\n */\nexport function effortFor(\n route: ModelRoute,\n events: readonly unknown[] | undefined,\n): ReasoningEffort | undefined {\n if (route.escalateOnError === true && recentStepsHadError(events, route.recoverySteps)) {\n return route.escalateTo ?? route.reasoningEffort;\n }\n return route.reasoningEffort;\n}\n\n/**\n * Whether any message in a request payload carries image content.\n *\n * A message carries an image when its `content` is a block array containing\n * an `image` block, directly or nested inside a `tool-result` block (tool\n * results may embed the images they produced). String content never does.\n *\n * @param messages - the request's `messages` array (any runtime shape).\n * @returns true when at least one message contains image content.\n */\nexport function hasImageContent(messages: unknown): boolean {\n if (!Array.isArray(messages)) return false;\n for (const message of messages) {\n const content = (message as { content?: unknown })?.content;\n if (typeof content === \"string\") continue;\n if (Array.isArray(content) && blocksContainImage(content)) return true;\n }\n return false;\n}\n\n/** Whether any block (or nested tool-result content) is an image block. */\nfunction blocksContainImage(blocks: readonly unknown[]): boolean {\n for (const block of blocks) {\n const candidate = block as { type?: unknown; content?: unknown };\n if (candidate?.type === \"image\") return true;\n if (candidate?.type === \"tool-result\" && Array.isArray(candidate.content) && blocksContainImage(candidate.content)) return true;\n }\n return false;\n}\n"],"mappings":"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","names":["stamped","effort"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-router",
|
|
3
3
|
"description": "DeepSeek Harness plugin: role-based model routing — the planner agent runs on deepseek-v4-pro, delegated executor subagents run on deepseek-v4-flash.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"types": "./lib/index.d.ts",
|
|
14
14
|
"default": "./lib/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"default": "./lib/client.js"
|
|
18
|
+
},
|
|
16
19
|
"./package.json": "./package.json"
|
|
17
20
|
},
|
|
18
21
|
"files": [
|
|
@@ -24,10 +27,19 @@
|
|
|
24
27
|
"dsh": {
|
|
25
28
|
"bundle": {
|
|
26
29
|
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"inject": [
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
35
|
+
"@deepseek-ai/dsh-client-locale"
|
|
36
|
+
],
|
|
37
|
+
"platform": "web"
|
|
27
38
|
}
|
|
28
39
|
},
|
|
29
40
|
"scripts": {
|
|
30
|
-
"build": "tsup",
|
|
41
|
+
"build": "tsup && npm run build:client",
|
|
42
|
+
"build:client": "node scripts/build-client.mjs",
|
|
31
43
|
"prepare": "npm run build",
|
|
32
44
|
"typecheck": "tsc --noEmit",
|
|
33
45
|
"test": "vitest run"
|
|
@@ -44,6 +56,8 @@
|
|
|
44
56
|
"@deepseek-ai/dsh-plan-mode": "^0.1.0-rc.8",
|
|
45
57
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
|
|
46
58
|
"@types/node": "^22.10.0",
|
|
59
|
+
"@types/react": "^18.3.12",
|
|
60
|
+
"react": "^18.3.1",
|
|
47
61
|
"tsup": "^8.3.5",
|
|
48
62
|
"typescript": "^5.7.2",
|
|
49
63
|
"vitest": "^2.1.8"
|