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/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
- * Failure ladder: configured config → official defaults (`{}`) → unmounted with
133
- * explicit errors, so the next settings event retries the whole ladder.
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 a reconcile behind the row's serialized chain, absorbing failures.
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
- state.chain = state.chain.then(() => runReconcile(ctx, state)).catch((cause) => {
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 on volatile commits and
186
- * ordinary saves); the listener is global so instances of this row inside
187
- * agent-preset realms still hear the host row's emission. The initial
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);