dsh-model-switch 0.4.23 → 0.4.25
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/CHANGELOG.md +25 -0
- package/PRODUCT.md +6 -3
- package/README.md +33 -12
- package/README.zh.md +33 -12
- package/cordis.patch.yml +15 -1
- package/docs/screenshots/settings-subagent.png +0 -0
- package/lib/auto-review.js +58 -7
- package/lib/client.js +438 -377
- package/lib/compaction.js +147 -19
- package/lib/index.js +10 -363
- package/lib/provider-group-CwadImhH.js +88 -0
- package/lib/session-route-BCQFzqRP.js +172 -0
- package/lib/session-title.js +217 -0
- package/lib/subagent-runtime.js +20 -17
- package/lib/types/adapter-registry.d.ts +1 -1
- package/lib/types/auto-review.d.ts +39 -4
- package/lib/types/client/ModelSwitchSettings.d.ts +8 -4
- package/lib/types/client/derived-config-form.d.ts +14 -0
- package/lib/types/client/locales.d.ts +8 -6
- package/lib/types/client-contract.d.ts +8 -1
- package/lib/types/compaction.d.ts +69 -4
- package/lib/types/host-settings.d.ts +6 -2
- package/lib/types/provider-group.d.ts +13 -35
- package/lib/types/session-route.d.ts +111 -0
- package/lib/types/session-title.d.ts +104 -0
- package/lib/types/subagent-runtime.d.ts +14 -3
- package/package.json +24 -1
- package/lib/types/client/derived-settings-scope.d.ts +0 -4
- package/lib/types/client/picker/main-default-restore.d.ts +0 -56
- package/lib/types/client/picker/provider-group-watch.d.ts +0 -48
- package/lib/types/client/remote-settings-scope.d.ts +0 -45
- package/lib/types/switch-compact.d.ts +0 -72
package/lib/compaction.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-route-BCQFzqRP.js";
|
|
1
2
|
//#region lib/types/compaction.js
|
|
2
3
|
/** Official compaction row this module supersedes. */
|
|
3
4
|
const OFFICIAL_COMPACTION_ROW = "compaction-basic";
|
|
@@ -81,6 +82,118 @@ function currentCompactionRoute(ctx) {
|
|
|
81
82
|
return compactionRouteOf(settings);
|
|
82
83
|
}
|
|
83
84
|
/**
|
|
85
|
+
* Resolve the compaction summarization route for one session id.
|
|
86
|
+
*
|
|
87
|
+
* Looks up the session from `ctx.sessions` when available, derives its current
|
|
88
|
+
* provider (`pending` selection preferred over `lastUsed`, then logged routes),
|
|
89
|
+
* and resolves the provider group's compaction pair before falling back to the
|
|
90
|
+
* deployment-wide compaction pair.
|
|
91
|
+
* @param ctx - this row's context.
|
|
92
|
+
* @param sessionId - the session id carried on `GenerateOptions.sessionId`.
|
|
93
|
+
* @returns The derived compaction route, or undefined for the engine's own route.
|
|
94
|
+
*/
|
|
95
|
+
function resolveSessionCompactionRoute(ctx, sessionId) {
|
|
96
|
+
if (typeof sessionId !== "string" || sessionId.trim() === "") return void 0;
|
|
97
|
+
let settings;
|
|
98
|
+
try {
|
|
99
|
+
settings = ctx.get("modelSwitch", false)?.currentSettings();
|
|
100
|
+
} catch {
|
|
101
|
+
settings = void 0;
|
|
102
|
+
}
|
|
103
|
+
if (!settings) return void 0;
|
|
104
|
+
let session;
|
|
105
|
+
try {
|
|
106
|
+
session = ctx.get("sessions", false)?.get?.(sessionId);
|
|
107
|
+
} catch {
|
|
108
|
+
session = void 0;
|
|
109
|
+
}
|
|
110
|
+
const provider = sessionProviderOf(ctx, session);
|
|
111
|
+
return resolveCapabilityRoute(settings, provider, "compaction");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap the real llm service so `purpose === 'compaction'` calls resolve their
|
|
115
|
+
* summarization route per session while keeping `compaction/summary` truthful.
|
|
116
|
+
*
|
|
117
|
+
* `summarizeWithLlm` builds a mutable `options` literal with `purpose: 'compaction'`
|
|
118
|
+
* and `sessionId: agent.session.id`, calls `ctx.llm.stream(options)`, and then
|
|
119
|
+
* returns `{ provider: options.provider, model: options.model }` to `commitCompactionBody`.
|
|
120
|
+
* Mutating `options.provider` and `options.model` in place before delegating to
|
|
121
|
+
* `target.stream(options)` ensures the durable `compaction/summary` event
|
|
122
|
+
* records the exact provider/model that wrote the summary. All other members
|
|
123
|
+
* (such as `resolveModelInfo`) pass through bound to `real`.
|
|
124
|
+
* @param real - the host's `ctx.llm`.
|
|
125
|
+
* @param resolve - per-session route resolver; undefined keeps `options` untouched.
|
|
126
|
+
* @param onError - optional error sink when the resolver throws.
|
|
127
|
+
* @returns A transparent Proxy over `real`.
|
|
128
|
+
*/
|
|
129
|
+
function compactionLlm(real, resolve, onError) {
|
|
130
|
+
return new Proxy(real, { get(target, property, _receiver) {
|
|
131
|
+
if (property === "stream") return (options) => {
|
|
132
|
+
if (options.purpose === "compaction" && typeof options.sessionId === "string" && options.sessionId.trim() !== "") {
|
|
133
|
+
let route;
|
|
134
|
+
try {
|
|
135
|
+
route = resolve(options.sessionId);
|
|
136
|
+
} catch (cause) {
|
|
137
|
+
onError?.(cause);
|
|
138
|
+
route = void 0;
|
|
139
|
+
}
|
|
140
|
+
if (route !== void 0) {
|
|
141
|
+
if (Object.isFrozen(options)) return target.stream({
|
|
142
|
+
...options,
|
|
143
|
+
provider: route.provider,
|
|
144
|
+
model: route.model
|
|
145
|
+
});
|
|
146
|
+
options.provider = route.provider;
|
|
147
|
+
options.model = route.model;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return target.stream(options);
|
|
151
|
+
};
|
|
152
|
+
const value = Reflect.get(target, property, target);
|
|
153
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
154
|
+
} });
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build the child context the compaction engine mounts under.
|
|
158
|
+
*
|
|
159
|
+
* When `ctx.llm` is present, shadows it with {@link compactionLlm} on an
|
|
160
|
+
* extended context so the child engine fiber resolves the per-session wrapper
|
|
161
|
+
* while registering `ctx.compaction` on the shared context tree.
|
|
162
|
+
* @param ctx - this row's context.
|
|
163
|
+
* @returns A context shadowing `llm` when available, else `ctx`.
|
|
164
|
+
*/
|
|
165
|
+
function compactionScope(ctx) {
|
|
166
|
+
let real;
|
|
167
|
+
try {
|
|
168
|
+
real = ctx.get("llm", false);
|
|
169
|
+
} catch {
|
|
170
|
+
real = void 0;
|
|
171
|
+
}
|
|
172
|
+
if (real === void 0) return ctx;
|
|
173
|
+
return ctx.extend({ llm: compactionLlm(real, (sessionId) => resolveSessionCompactionRoute(ctx, sessionId), (cause) => {
|
|
174
|
+
ctx.logger.error("model-switch: failed to resolve per-session compaction route; keeping the engine route");
|
|
175
|
+
ctx.logger.error(cause);
|
|
176
|
+
}) });
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Whether the compaction capability is actually served from this context.
|
|
180
|
+
*
|
|
181
|
+
* Awaiting a plugin fiber settles even when the plugin's injections were never
|
|
182
|
+
* satisfied, so the mount is confirmed through the public service instead of by
|
|
183
|
+
* the absence of an error: an unactivated engine must never be recorded as the
|
|
184
|
+
* live engine, or the row would stop retrying while `/compact` had no service.
|
|
185
|
+
* @param ctx - this row's context.
|
|
186
|
+
* @returns true when `ctx.compaction` exposes the engine's public entry points.
|
|
187
|
+
*/
|
|
188
|
+
function engineActive(ctx) {
|
|
189
|
+
try {
|
|
190
|
+
const service = ctx.get("compaction", false);
|
|
191
|
+
return service !== void 0 && typeof service.compactIfNeeded === "function";
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
84
197
|
* Load and mount the official basic compaction engine under `config`.
|
|
85
198
|
* @param ctx - this row's context; the engine mounts unisolated so `/compact` sees it.
|
|
86
199
|
* @param config - composed engine config.
|
|
@@ -98,9 +211,8 @@ async function mountEngine(ctx, config, label) {
|
|
|
98
211
|
}
|
|
99
212
|
let fiber;
|
|
100
213
|
try {
|
|
101
|
-
fiber = ctx.plugin(loaded.default, config);
|
|
214
|
+
fiber = compactionScope(ctx).plugin(loaded.default, config);
|
|
102
215
|
await fiber;
|
|
103
|
-
return fiber;
|
|
104
216
|
} catch (cause) {
|
|
105
217
|
if (fiber) try {
|
|
106
218
|
await fiber.dispose();
|
|
@@ -109,6 +221,14 @@ async function mountEngine(ctx, config, label) {
|
|
|
109
221
|
ctx.logger.error(cause);
|
|
110
222
|
return;
|
|
111
223
|
}
|
|
224
|
+
if (engineActive(ctx)) return fiber;
|
|
225
|
+
ctx.logger.error(`model-switch: the compaction engine mounted with ${label} but never activated; unloading it so the next settings change retries`);
|
|
226
|
+
try {
|
|
227
|
+
await fiber.dispose();
|
|
228
|
+
} catch (cause) {
|
|
229
|
+
ctx.logger.error("model-switch: failed to unload the unactivated compaction engine");
|
|
230
|
+
ctx.logger.error(cause);
|
|
231
|
+
}
|
|
112
232
|
}
|
|
113
233
|
/**
|
|
114
234
|
* Dispose the live engine, keeping the ledger clean for the next mount.
|
|
@@ -129,8 +249,11 @@ async function disposeEngine(ctx, state) {
|
|
|
129
249
|
/**
|
|
130
250
|
* Reconcile one compaction mount: remount only when the composed config changed.
|
|
131
251
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
252
|
+
* A settings change that leaves the composed config identical — every session
|
|
253
|
+
* naming, subagent, or provider-group save — returns after the signature check,
|
|
254
|
+
* so the engine is never unloaded for settings it does not read. A mount that
|
|
255
|
+
* fails to activate stays unmounted with an explicit error and the next settings
|
|
256
|
+
* change retries with a fresh read.
|
|
134
257
|
* @param ctx - this row's context.
|
|
135
258
|
* @param state - the row's reconcile ledger.
|
|
136
259
|
*/
|
|
@@ -154,22 +277,25 @@ async function runReconcile(ctx, state) {
|
|
|
154
277
|
state.engine = fiber;
|
|
155
278
|
return;
|
|
156
279
|
}
|
|
157
|
-
ctx.logger.error("model-switch: retrying the compaction engine on the official defaults");
|
|
158
|
-
const fallback = await mountEngine(ctx, {}, "the official defaults");
|
|
159
|
-
if (fallback) {
|
|
160
|
-
state.engine = fallback;
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
280
|
ctx.logger.error("model-switch: compaction engine is unmounted; /compact and automatic compaction are unavailable until settings change");
|
|
164
281
|
}
|
|
165
282
|
/**
|
|
166
|
-
* Queue
|
|
283
|
+
* Queue one reconcile behind the row's serialized chain, absorbing failures.
|
|
167
284
|
* @param ctx - this row's context.
|
|
168
285
|
* @param state - the row's reconcile ledger.
|
|
286
|
+
* @param coalesce - collapse a burst of triggers into the single pass that reads the settled settings.
|
|
169
287
|
* @returns The chain tail; never rejects.
|
|
170
288
|
*/
|
|
171
|
-
function scheduleReconcile(ctx, state) {
|
|
172
|
-
|
|
289
|
+
function scheduleReconcile(ctx, state, coalesce) {
|
|
290
|
+
if (coalesce) {
|
|
291
|
+
if (state.queued) return state.chain;
|
|
292
|
+
state.queued = true;
|
|
293
|
+
}
|
|
294
|
+
state.chain = state.chain.then(async () => {
|
|
295
|
+
state.queued = false;
|
|
296
|
+
await runReconcile(ctx, state);
|
|
297
|
+
}).catch((cause) => {
|
|
298
|
+
state.queued = false;
|
|
173
299
|
ctx.logger.error("model-switch: compaction engine reconcile failed");
|
|
174
300
|
ctx.logger.error(cause);
|
|
175
301
|
});
|
|
@@ -182,9 +308,10 @@ function scheduleReconcile(ctx, state) {
|
|
|
182
308
|
* with the same package under the configurable summarization route. Host
|
|
183
309
|
* settings and the loader are read structurally so a disabled `model-switch`
|
|
184
310
|
* row leaves official compaction untouched. Reconciles run on
|
|
185
|
-
* `model-switch/settings-update` (emitted by the host
|
|
186
|
-
* ordinary saves)
|
|
187
|
-
*
|
|
311
|
+
* `model-switch/settings-update` (emitted by the host at construction, on
|
|
312
|
+
* volatile commits, and on ordinary saves) and read the settled settings, so one
|
|
313
|
+
* save costs at most one remount; the listener is global so instances of this
|
|
314
|
+
* row inside agent-preset realms still hear the host row's emission. The initial
|
|
188
315
|
* reconcile is awaited so `/compact` resolves the service as soon as this row
|
|
189
316
|
* completes.
|
|
190
317
|
* @param ctx - this row's context.
|
|
@@ -192,14 +319,15 @@ function scheduleReconcile(ctx, state) {
|
|
|
192
319
|
async function apply(ctx) {
|
|
193
320
|
const state = {
|
|
194
321
|
chain: Promise.resolve(),
|
|
322
|
+
queued: false,
|
|
195
323
|
signature: null,
|
|
196
324
|
engine: void 0,
|
|
197
325
|
missingRow: false
|
|
198
326
|
};
|
|
199
327
|
ctx.on("model-switch/settings-update", () => {
|
|
200
|
-
scheduleReconcile(ctx, state);
|
|
328
|
+
scheduleReconcile(ctx, state, true);
|
|
201
329
|
}, { global: true });
|
|
202
|
-
await scheduleReconcile(ctx, state);
|
|
330
|
+
await scheduleReconcile(ctx, state, false);
|
|
203
331
|
}
|
|
204
332
|
//#endregion
|
|
205
|
-
export { apply, compactionRouteOf, composeCompactionConfig, forwardedConfig, inject, name, officialCompactionEntry };
|
|
333
|
+
export { apply, compactionLlm, compactionRouteOf, compactionScope, composeCompactionConfig, engineActive, forwardedConfig, inject, name, officialCompactionEntry, resolveSessionCompactionRoute };
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as normalizeProviderGroups } from "./provider-group-CwadImhH.js";
|
|
1
2
|
import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -9,80 +10,6 @@ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
|
9
10
|
import { Config as Config$1, DEEPSEEK_DEFAULT_API_VERSION, DEEPSEEK_DEFAULT_BASE_URL, DEEPSEEK_DEFAULT_MAX_TOKENS, DEEPSEEK_DEFAULT_MAX_USES, DEEPSEEK_DEFAULT_MODEL, DEEPSEEK_PROVIDER_ID, DeepSeekSearchProvider, WEB_SEARCH_DEEPSEEK_SETTINGS_NAMESPACE } from "@deepseek-ai/dsh-web-search-deepseek";
|
|
10
11
|
import { readFileSync } from "node:fs";
|
|
11
12
|
import { dirname, join } from "node:path";
|
|
12
|
-
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
13
|
-
import { boundContextSummary, createMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
14
|
-
import { canonicalHeader } from "@deepseek-ai/dsh-session";
|
|
15
|
-
//#region lib/types/provider-group.js
|
|
16
|
-
/**
|
|
17
|
-
* Provider configuration groups: per-provider presets for the compaction,
|
|
18
|
-
* subagent, and auto-review routes.
|
|
19
|
-
*
|
|
20
|
-
* A group stores the exact flat settings fields it will write, so applying a
|
|
21
|
-
* group is a plain settings-layer write: every capability consumer keeps
|
|
22
|
-
* reading the live values unchanged. All functions here are pure and never
|
|
23
|
-
* throw on malformed storage.
|
|
24
|
-
*/
|
|
25
|
-
const ROUTE_FIELDS = Object.freeze([
|
|
26
|
-
"compactProvider",
|
|
27
|
-
"compactModel",
|
|
28
|
-
"autoReviewProvider",
|
|
29
|
-
"autoReviewModel",
|
|
30
|
-
"subagentProvider",
|
|
31
|
-
"subagentModel",
|
|
32
|
-
"subagentReasoningEffort"
|
|
33
|
-
]);
|
|
34
|
-
/** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
|
|
35
|
-
function setKey(target, key, value) {
|
|
36
|
-
Object.defineProperty(target, key, {
|
|
37
|
-
value,
|
|
38
|
-
enumerable: true,
|
|
39
|
-
writable: true,
|
|
40
|
-
configurable: true
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
/** A non-blank string. Blank values behave exactly like an absent field. */
|
|
44
|
-
function present(value) {
|
|
45
|
-
return typeof value === "string" && value.trim() !== "";
|
|
46
|
-
}
|
|
47
|
-
/** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
|
|
48
|
-
function normalizeEntry(value) {
|
|
49
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
50
|
-
const source = value;
|
|
51
|
-
const group = {};
|
|
52
|
-
for (const field of ROUTE_FIELDS) {
|
|
53
|
-
const item = source[field];
|
|
54
|
-
if (present(item)) group[field] = item;
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
provider: present(source.provider) ? source.provider : "",
|
|
58
|
-
group
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Strip a raw stored value to well-formed groups; never throws. Both storage
|
|
63
|
-
* shapes are accepted: the stored entry array (keys live in each entry's
|
|
64
|
-
* `provider` field) and a hand-written provider → routes map (keys are the
|
|
65
|
-
* providers). All-empty entries stay: they pin a provider to every default.
|
|
66
|
-
*/
|
|
67
|
-
function normalizeProviderGroups(raw) {
|
|
68
|
-
const groups = {};
|
|
69
|
-
if (Array.isArray(raw)) {
|
|
70
|
-
for (const entry of raw) {
|
|
71
|
-
const view = normalizeEntry(entry);
|
|
72
|
-
if (view === void 0 || view.provider === "") continue;
|
|
73
|
-
setKey(groups, view.provider, view.group);
|
|
74
|
-
}
|
|
75
|
-
return groups;
|
|
76
|
-
}
|
|
77
|
-
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
|
|
78
|
-
if (key.trim() === "") continue;
|
|
79
|
-
const view = normalizeEntry(value);
|
|
80
|
-
if (view === void 0) continue;
|
|
81
|
-
setKey(groups, key, view.group);
|
|
82
|
-
}
|
|
83
|
-
return groups;
|
|
84
|
-
}
|
|
85
|
-
//#endregion
|
|
86
13
|
//#region lib/types/host-settings.js
|
|
87
14
|
/**
|
|
88
15
|
* Annotated rather than inferred: the nested entry schema's inferred type
|
|
@@ -98,11 +25,12 @@ const Config = z.object({
|
|
|
98
25
|
searchModel: z.string().volatile(),
|
|
99
26
|
imageProvider: z.string().volatile(),
|
|
100
27
|
imageModel: z.string().volatile(),
|
|
101
|
-
compactOnSwitch: z.boolean().default(true).volatile(),
|
|
102
28
|
autoReviewProvider: z.string().volatile(),
|
|
103
29
|
autoReviewModel: z.string().volatile(),
|
|
104
30
|
compactProvider: z.string().volatile(),
|
|
105
31
|
compactModel: z.string().volatile(),
|
|
32
|
+
titleProvider: z.string().volatile(),
|
|
33
|
+
titleModel: z.string().volatile(),
|
|
106
34
|
providerGroups: z.array(z.object({
|
|
107
35
|
provider: z.string().default(""),
|
|
108
36
|
compactProvider: z.string().default(""),
|
|
@@ -111,7 +39,9 @@ const Config = z.object({
|
|
|
111
39
|
autoReviewModel: z.string().default(""),
|
|
112
40
|
subagentProvider: z.string().default(""),
|
|
113
41
|
subagentModel: z.string().default(""),
|
|
114
|
-
subagentReasoningEffort: z.string().default("")
|
|
42
|
+
subagentReasoningEffort: z.string().default(""),
|
|
43
|
+
titleProvider: z.string().default(""),
|
|
44
|
+
titleModel: z.string().default("")
|
|
115
45
|
})).default([]).volatile()
|
|
116
46
|
});
|
|
117
47
|
function readConfig(config) {
|
|
@@ -126,6 +56,8 @@ function readConfig(config) {
|
|
|
126
56
|
const autoReviewModel = config.autoReviewModel.get();
|
|
127
57
|
const compactProvider = config.compactProvider.get();
|
|
128
58
|
const compactModel = config.compactModel.get();
|
|
59
|
+
const titleProvider = config.titleProvider.get();
|
|
60
|
+
const titleModel = config.titleModel.get();
|
|
129
61
|
const providerGroups = normalizeProviderGroups(config.providerGroups.get());
|
|
130
62
|
return {
|
|
131
63
|
subagentMode: config.subagentMode.get(),
|
|
@@ -136,11 +68,12 @@ function readConfig(config) {
|
|
|
136
68
|
...searchModel === void 0 ? {} : { searchModel },
|
|
137
69
|
...imageProvider === void 0 ? {} : { imageProvider },
|
|
138
70
|
...imageModel === void 0 ? {} : { imageModel },
|
|
139
|
-
compactOnSwitch: config.compactOnSwitch.get(),
|
|
140
71
|
...autoReviewProvider === void 0 || autoReviewProvider === "" ? {} : { autoReviewProvider },
|
|
141
72
|
...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
|
|
142
73
|
...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
|
|
143
74
|
...compactModel === void 0 || compactModel === "" ? {} : { compactModel },
|
|
75
|
+
...titleProvider === void 0 || titleProvider === "" ? {} : { titleProvider },
|
|
76
|
+
...titleModel === void 0 || titleModel === "" ? {} : { titleModel },
|
|
144
77
|
...Object.keys(providerGroups).length === 0 ? {} : { providerGroups }
|
|
145
78
|
};
|
|
146
79
|
}
|
|
@@ -679,291 +612,6 @@ function allowDshRuntime(logger, pluginName, candidates) {
|
|
|
679
612
|
return shouldMountDshRuntime(logger, pluginName, version, new Set(Object.entries(compatibility?.dshReleases ?? {}).filter(([, status]) => status === "compatible" || status === "verified").map(([release]) => release)), compatibility?.blocklist);
|
|
680
613
|
}
|
|
681
614
|
//#endregion
|
|
682
|
-
//#region lib/types/switch-compact.js
|
|
683
|
-
/** Send-time context check and optional old-model compaction before a switched request. */
|
|
684
|
-
/** Fraction of the target window reserved for output plus estimator error. */
|
|
685
|
-
const OUTPUT_RESERVE = .2;
|
|
686
|
-
/** Extra tokens on top of the ratio; the meter heuristic is not exact. */
|
|
687
|
-
const ESTIMATE_MARGIN = 64;
|
|
688
|
-
/**
|
|
689
|
-
* Compare two fully specified model routes.
|
|
690
|
-
* @param left - first route, if one was resolved.
|
|
691
|
-
* @param right - second route, if one was resolved.
|
|
692
|
-
* @returns Whether both routes identify the same provider and model.
|
|
693
|
-
*/
|
|
694
|
-
function sameSwitchRoute(left, right) {
|
|
695
|
-
return left !== void 0 && right !== void 0 && left.provider === right.provider && left.model === right.model;
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Decide whether a changed, complete route needs send-time context protection.
|
|
699
|
-
* @param enabled - whether the user enabled send-time protection.
|
|
700
|
-
* @param previous - route used by the preceding request.
|
|
701
|
-
* @param selected - route assembled for the pending request.
|
|
702
|
-
* @returns Whether the pending request changes its provider or model.
|
|
703
|
-
*/
|
|
704
|
-
function shouldProtectSwitch(enabled, previous, selected) {
|
|
705
|
-
if (!enabled) return false;
|
|
706
|
-
if (previous === void 0 || selected === void 0) return false;
|
|
707
|
-
return !sameSwitchRoute(previous, selected);
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Input budget for the target window. Reserves output (maxTokens or 20%) plus a
|
|
711
|
-
* fixed estimator margin. `undefined` means the window cannot be used.
|
|
712
|
-
* @param contextWindow - adapter-owned combined request/response capacity.
|
|
713
|
-
* @param maxTokens - optional generation cap from the live agent options.
|
|
714
|
-
* @returns Tokens available to the request input, or undefined for an invalid window.
|
|
715
|
-
*/
|
|
716
|
-
function switchInputBudget(contextWindow, maxTokens) {
|
|
717
|
-
if (!(contextWindow > 0) || !Number.isFinite(contextWindow)) return void 0;
|
|
718
|
-
const reserve = Math.max(Math.ceil(contextWindow * OUTPUT_RESERVE), maxTokens ?? 0) + ESTIMATE_MARGIN;
|
|
719
|
-
return Math.max(0, Math.floor(contextWindow - reserve));
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Determine whether an estimated input exceeds its target route budget.
|
|
723
|
-
* @param tokens - estimated input tokens.
|
|
724
|
-
* @param budget - input budget for the target window.
|
|
725
|
-
* @returns Whether the budget is unusable or the estimate is too large.
|
|
726
|
-
*/
|
|
727
|
-
function overSwitchBudget(tokens, budget) {
|
|
728
|
-
return !(budget > 0) || tokens > budget;
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Build one transcript-visible Model Switch progress notice.
|
|
732
|
-
* @param text - localized notice text shown in the transcript.
|
|
733
|
-
* @param summary - bounded event summary for durable session metadata.
|
|
734
|
-
* @returns A plugin-authored user message for the session log.
|
|
735
|
-
*/
|
|
736
|
-
function switchNotice(text, summary) {
|
|
737
|
-
return createUserMessage({
|
|
738
|
-
content: [{
|
|
739
|
-
type: "text",
|
|
740
|
-
text
|
|
741
|
-
}],
|
|
742
|
-
source: {
|
|
743
|
-
kind: "model-switch",
|
|
744
|
-
form: "notice",
|
|
745
|
-
summary: boundContextSummary(summary)
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Extract a complete route from partial request configuration.
|
|
751
|
-
* @param value - candidate provider and model values.
|
|
752
|
-
* @returns A complete route, or undefined when either identifier is absent.
|
|
753
|
-
*/
|
|
754
|
-
function routeOf(value) {
|
|
755
|
-
if (value === void 0 || value.provider === void 0 || value.provider.length === 0 || value.model === void 0 || value.model.length === 0) return void 0;
|
|
756
|
-
return {
|
|
757
|
-
provider: value.provider,
|
|
758
|
-
model: value.model
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Install send-time switch protection on the host context.
|
|
763
|
-
* @param ctx - host context that owns agent-loop, llm, and tokenMeter.
|
|
764
|
-
* @param settings - live Model Switch settings; `compactOnSwitch !== false` is on.
|
|
765
|
-
* @param roleOf - provider-role lookup used to exclude native Agent runtimes.
|
|
766
|
-
*/
|
|
767
|
-
function installSwitchCompaction(ctx, settings, roleOf = () => void 0) {
|
|
768
|
-
const assemblies = /* @__PURE__ */ new WeakMap();
|
|
769
|
-
let stopGate;
|
|
770
|
-
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
771
|
-
const assembled = await next();
|
|
772
|
-
if (context.agent !== void 0) {
|
|
773
|
-
assemblies.set(context.agent, assembled);
|
|
774
|
-
stopGate?.();
|
|
775
|
-
installGate();
|
|
776
|
-
}
|
|
777
|
-
return assembled;
|
|
778
|
-
});
|
|
779
|
-
function installGate() {
|
|
780
|
-
stopGate = ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
781
|
-
const decision = await next();
|
|
782
|
-
if (decision.kind !== "enter" || signal.aborted) return decision;
|
|
783
|
-
const enabled = settings().compactOnSwitch !== false;
|
|
784
|
-
const previous = routeOf(agent.session.requestHeader()?.config);
|
|
785
|
-
const assembly = assemblies.get(agent);
|
|
786
|
-
const selected = routeOf(assembly === void 0 ? void 0 : {
|
|
787
|
-
...assembly.variables.provider === void 0 ? {} : { provider: assembly.variables.provider },
|
|
788
|
-
...assembly.variables.model === void 0 ? {} : { model: assembly.variables.model }
|
|
789
|
-
});
|
|
790
|
-
if (!shouldProtectSwitch(enabled, previous, selected) || previous === void 0 || selected === void 0) return decision;
|
|
791
|
-
if (roleOf(previous.provider) === "agent" || roleOf(selected.provider) === "agent") return decision;
|
|
792
|
-
return protectSwitch(ctx, agent, previous, selected, assembly, decision, messages, signal);
|
|
793
|
-
}, { prepend: true });
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
async function protectSwitch(ctx, agent, previous, selected, assembly, decision, claimed, signal) {
|
|
797
|
-
const session = agent.session;
|
|
798
|
-
const notice = (text, summary) => {
|
|
799
|
-
session.append("user/message", switchNotice(text, summary), { surfaceOp: "append" });
|
|
800
|
-
};
|
|
801
|
-
const reject = (reason, summary, compacted) => {
|
|
802
|
-
notice(compacted ? `无法继续本次模型切换:压缩已发生,但请求未继续(${reason})。` : `无法继续本次模型切换:${reason}`, summary);
|
|
803
|
-
retainUserClaims(session, claimed);
|
|
804
|
-
return { kind: "reject" };
|
|
805
|
-
};
|
|
806
|
-
notice("正在检查上下文…", "checking context");
|
|
807
|
-
if (assembly === void 0) return reject("无法确定本次请求组装的目标模型。", "switch blocked: no assembled target", false);
|
|
808
|
-
const llm = ctx.get("llm", false);
|
|
809
|
-
if (llm?.resolveModelInfo === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
|
|
810
|
-
let window;
|
|
811
|
-
try {
|
|
812
|
-
const value = (await llm.resolveModelInfo(selected.provider, selected.model, signal)).context?.contextWindow;
|
|
813
|
-
window = typeof value === "number" && value > 0 ? value : void 0;
|
|
814
|
-
} catch {
|
|
815
|
-
window = void 0;
|
|
816
|
-
}
|
|
817
|
-
if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", false);
|
|
818
|
-
if (window === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
|
|
819
|
-
const meter = ctx.get("tokenMeter", false);
|
|
820
|
-
if (meter === void 0 || typeof meter.measure !== "function" || typeof meter.estimateMessage !== "function") return reject("无法估算待发上下文(缺少 token 计量)。", "switch blocked: no token meter", false);
|
|
821
|
-
const header = targetHeader(selected, assembly);
|
|
822
|
-
const previousHeader = session.requestHeader();
|
|
823
|
-
let pressure;
|
|
824
|
-
try {
|
|
825
|
-
pressure = estimateSwitchPressure(meter, session, header, assembly, decision.messages, previousHeader);
|
|
826
|
-
} catch (error) {
|
|
827
|
-
return reject(`无法估算待发上下文(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: estimate failed", false);
|
|
828
|
-
}
|
|
829
|
-
const budget = agent.options.maxTokens === void 0 ? switchInputBudget(window) : switchInputBudget(window, agent.options.maxTokens);
|
|
830
|
-
if (budget === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
|
|
831
|
-
const passed = switchNotice("上下文容量检查通过,无需压缩。", "context check passed");
|
|
832
|
-
const completed = switchNotice("压缩完成,复查通过。", "compacted");
|
|
833
|
-
if (!overSwitchBudget(pressure + meter.estimateMessage(passed), budget)) {
|
|
834
|
-
session.append("user/message", passed, { surfaceOp: "append" });
|
|
835
|
-
return decision;
|
|
836
|
-
}
|
|
837
|
-
let fixedPressure;
|
|
838
|
-
try {
|
|
839
|
-
fixedPressure = estimateFixedInput(meter, session, header, assembly, decision.messages);
|
|
840
|
-
} catch (error) {
|
|
841
|
-
return reject(`无法估算待发上下文(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: estimate failed", false);
|
|
842
|
-
}
|
|
843
|
-
if (overSwitchBudget(fixedPressure + meter.estimateMessage(completed), budget)) return reject("新消息或固定请求内容已超过目标模型可用容量。", "switch blocked: fixed input oversized", false);
|
|
844
|
-
notice("当前上下文超过目标模型可用容量,正在使用旧模型压缩…", "compacting with previous model");
|
|
845
|
-
const compactionGeneration = session.surface.replaceGeneration;
|
|
846
|
-
let compacted = false;
|
|
847
|
-
try {
|
|
848
|
-
const result = await compactWithPrevious(ctx, agent, previous, signal);
|
|
849
|
-
compacted = result !== "none" || session.surface.replaceGeneration > compactionGeneration;
|
|
850
|
-
if (result === "none") return reject("没有可压缩的历史。", "switch blocked: nothing to compact", compacted);
|
|
851
|
-
} catch (error) {
|
|
852
|
-
compacted = compacted || session.surface.replaceGeneration > compactionGeneration;
|
|
853
|
-
if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", compacted);
|
|
854
|
-
return reject(`压缩失败(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: compact failed", compacted);
|
|
855
|
-
}
|
|
856
|
-
if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", compacted);
|
|
857
|
-
let again;
|
|
858
|
-
try {
|
|
859
|
-
again = estimateSwitchPressure(meter, session, header, assembly, decision.messages);
|
|
860
|
-
} catch (error) {
|
|
861
|
-
return reject(`压缩后无法复查(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: remeasure failed", compacted);
|
|
862
|
-
}
|
|
863
|
-
if (overSwitchBudget(again + meter.estimateMessage(completed), budget)) return reject("压缩后仍超过目标模型可用容量。", "switch blocked: still over budget", compacted);
|
|
864
|
-
session.append("user/message", completed, { surfaceOp: "append" });
|
|
865
|
-
return decision;
|
|
866
|
-
}
|
|
867
|
-
function targetHeader(selected, assembly) {
|
|
868
|
-
return canonicalHeader({
|
|
869
|
-
config: {
|
|
870
|
-
provider: selected.provider,
|
|
871
|
-
model: selected.model
|
|
872
|
-
},
|
|
873
|
-
...assembly.tools.length > 0 ? { tools: [...assembly.tools] } : {}
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Next-request input under the target route. Uses the committed surface priced
|
|
878
|
-
* for the target (including image tokens), then adds still-pending enter
|
|
879
|
-
* messages and the prospective system prompt while dropping already-priced
|
|
880
|
-
* system nodes so they are not counted twice.
|
|
881
|
-
*
|
|
882
|
-
* The target envelope has no provider usage of its own, so the meter can only
|
|
883
|
-
* price it with the chars-per-token heuristic. That heuristic is unconservative
|
|
884
|
-
* for dense text (base64 payloads, minified sources, CJK). The previous
|
|
885
|
-
* envelope holds the provider's reported usage for the same surface, so the
|
|
886
|
-
* switch takes the larger of the two prices: the anchor keeps the check
|
|
887
|
-
* truthful exactly where a switch makes the heuristic weakest.
|
|
888
|
-
*
|
|
889
|
-
* @param meter - replay token meter.
|
|
890
|
-
* @param session - session being measured.
|
|
891
|
-
* @param header - target request envelope.
|
|
892
|
-
* @param assembly - prospective prompt assembly.
|
|
893
|
-
* @param pending - messages the pre-step is about to enter.
|
|
894
|
-
* @param previousHeader - envelope of the last request, when the session has one.
|
|
895
|
-
* @returns Estimated next-request input tokens.
|
|
896
|
-
*/
|
|
897
|
-
function estimateSwitchPressure(meter, session, header, assembly, pending, previousHeader) {
|
|
898
|
-
const estimate = routePressure(meter, session, header, assembly, pending);
|
|
899
|
-
if (previousHeader === void 0) return estimate;
|
|
900
|
-
return Math.max(estimate, routePressure(meter, session, previousHeader, assembly, pending));
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Price one request envelope against the committed surface.
|
|
904
|
-
* @param meter - replay token meter.
|
|
905
|
-
* @param session - session being measured.
|
|
906
|
-
* @param header - request envelope to price.
|
|
907
|
-
* @param assembly - prospective prompt assembly.
|
|
908
|
-
* @param pending - messages the pre-step is about to enter.
|
|
909
|
-
* @returns Estimated input tokens for that envelope.
|
|
910
|
-
*/
|
|
911
|
-
function routePressure(meter, session, header, assembly, pending) {
|
|
912
|
-
const measured = meter.measure(session, header);
|
|
913
|
-
let systemTokens = 0;
|
|
914
|
-
for (const node of measured.nodes) if (session.eventAt(node.seq)?.type === "system/message") systemTokens += node.tokens;
|
|
915
|
-
return measured.totalTokens - systemTokens + systemPromptTokens(meter, assembly) + pendingTokens(meter, pending);
|
|
916
|
-
}
|
|
917
|
-
function estimateFixedInput(meter, session, header, assembly, pending) {
|
|
918
|
-
const measured = meter.measure(session, header);
|
|
919
|
-
return measured.totalTokens - measured.surfaceTokens + systemPromptTokens(meter, assembly) + pendingTokens(meter, pending);
|
|
920
|
-
}
|
|
921
|
-
function pendingTokens(meter, messages) {
|
|
922
|
-
let total = 0;
|
|
923
|
-
for (const message of messages) {
|
|
924
|
-
const tokens = meter.estimateMessage(message);
|
|
925
|
-
if (!(tokens >= 0) || !Number.isFinite(tokens)) throw new Error("pending message token estimate is not a number");
|
|
926
|
-
total += tokens;
|
|
927
|
-
}
|
|
928
|
-
return total;
|
|
929
|
-
}
|
|
930
|
-
function systemPromptTokens(meter, assembly) {
|
|
931
|
-
const text = renderPrompt(assembly);
|
|
932
|
-
if (text.length === 0) return 0;
|
|
933
|
-
return meter.estimateMessage(createMessage({
|
|
934
|
-
role: "system",
|
|
935
|
-
content: [{
|
|
936
|
-
type: "text",
|
|
937
|
-
text
|
|
938
|
-
}],
|
|
939
|
-
source: { kind: "system-prompt" }
|
|
940
|
-
}));
|
|
941
|
-
}
|
|
942
|
-
function retainUserClaims(session, claimed) {
|
|
943
|
-
for (const message of claimed) {
|
|
944
|
-
if (message.source.kind !== "user") continue;
|
|
945
|
-
session.append("user/message", message, { surfaceOp: "append" });
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
async function compactWithPrevious(ctx, agent, previous, signal) {
|
|
949
|
-
const isolated = ctx.isolate("compaction");
|
|
950
|
-
const Engine = (await import("@deepseek-ai/dsh-compaction-basic")).default;
|
|
951
|
-
const fiber = isolated.plugin(Engine, {
|
|
952
|
-
auto: false,
|
|
953
|
-
summarizationProvider: previous.provider,
|
|
954
|
-
summarizationModel: previous.model,
|
|
955
|
-
compactionRetries: 0
|
|
956
|
-
});
|
|
957
|
-
try {
|
|
958
|
-
await fiber;
|
|
959
|
-
const engine = isolated.get("compaction", false);
|
|
960
|
-
if (engine === void 0) throw new Error("private compaction engine is unavailable");
|
|
961
|
-
return await engine.compactIfNeeded(agent, "context-overflow", signal) === null ? "none" : "compacted";
|
|
962
|
-
} finally {
|
|
963
|
-
await fiber.dispose();
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
//#endregion
|
|
967
615
|
//#region lib/types/host-runtime.js
|
|
968
616
|
/** Host owner for Model Switch settings. */
|
|
969
617
|
var ModelSwitchRuntime = class extends Service {
|
|
@@ -986,7 +634,6 @@ var ModelSwitchRuntime = class extends Service {
|
|
|
986
634
|
this.source = () => readConfig(entry);
|
|
987
635
|
ctx.inject(["settings"], (scope) => scope.effect(() => scope.settings.configure({ auto: false }, ctx.fiber), "Model Switch: disable automatic settings form"));
|
|
988
636
|
if (!allowDshRuntime(ctx.logger, "dsh-model-switch", ["@deepseek-ai/dsh-agent"])) return;
|
|
989
|
-
installSwitchCompaction(ctx, () => this.source(), (provider) => this.adapters.get(provider)?.role);
|
|
990
637
|
installDeepSeekSearchAdapter(ctx);
|
|
991
638
|
installModelSwitchSearchProvider(ctx, this);
|
|
992
639
|
installCapabilitiesRpc(ctx, this.adapters, () => this.capabilities);
|