dsh-model-switch 0.4.24 → 0.4.26
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 -4
- package/README.md +7 -13
- package/README.zh.md +6 -12
- package/cordis.patch.yml +9 -0
- package/lib/auto-review.js +57 -14
- package/lib/client.js +208 -139
- package/lib/compaction.js +97 -2
- package/lib/index.js +1 -301
- package/lib/{provider-group-CsZi7H29.js → provider-group-CwadImhH.js} +3 -9
- package/lib/{session-route-h9SAqajc.js → session-route-BCQFzqRP.js} +1 -1
- package/lib/session-route-BjNPlliu.js +172 -0
- package/lib/session-title.js +387 -34
- package/lib/subagent-runtime.js +26 -3
- package/lib/types/adapter-registry.d.ts +1 -1
- package/lib/types/auto-review.d.ts +38 -11
- package/lib/types/client/ModelSwitchSettings.d.ts +0 -1
- package/lib/types/client/locales.d.ts +2 -5
- package/lib/types/client-contract.d.ts +0 -1
- package/lib/types/compaction.d.ts +50 -6
- package/lib/types/host-settings.d.ts +0 -2
- package/lib/types/provider-group.d.ts +4 -10
- package/lib/types/session-title.d.ts +194 -14
- package/lib/types/subagent-runtime.d.ts +10 -0
- package/package.json +3 -2
- 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 { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-BjNPlliu.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,100 @@ 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
|
+
/**
|
|
84
179
|
* Whether the compaction capability is actually served from this context.
|
|
85
180
|
*
|
|
86
181
|
* Awaiting a plugin fiber settles even when the plugin's injections were never
|
|
@@ -116,7 +211,7 @@ async function mountEngine(ctx, config, label) {
|
|
|
116
211
|
}
|
|
117
212
|
let fiber;
|
|
118
213
|
try {
|
|
119
|
-
fiber = ctx.plugin(loaded.default, config);
|
|
214
|
+
fiber = compactionScope(ctx).plugin(loaded.default, config);
|
|
120
215
|
await fiber;
|
|
121
216
|
} catch (cause) {
|
|
122
217
|
if (fiber) try {
|
|
@@ -235,4 +330,4 @@ async function apply(ctx) {
|
|
|
235
330
|
await scheduleReconcile(ctx, state, false);
|
|
236
331
|
}
|
|
237
332
|
//#endregion
|
|
238
|
-
export { apply, compactionRouteOf, composeCompactionConfig, engineActive, forwardedConfig, inject, name, officialCompactionEntry };
|
|
333
|
+
export { apply, compactionLlm, compactionRouteOf, compactionScope, composeCompactionConfig, engineActive, forwardedConfig, inject, name, officialCompactionEntry, resolveSessionCompactionRoute };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as normalizeProviderGroups } from "./provider-group-
|
|
1
|
+
import { n as normalizeProviderGroups } from "./provider-group-CwadImhH.js";
|
|
2
2
|
import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -10,9 +10,6 @@ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
|
10
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";
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
14
|
-
import { boundContextSummary, createMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
15
|
-
import { canonicalHeader } from "@deepseek-ai/dsh-session";
|
|
16
13
|
//#region lib/types/host-settings.js
|
|
17
14
|
/**
|
|
18
15
|
* Annotated rather than inferred: the nested entry schema's inferred type
|
|
@@ -28,7 +25,6 @@ const Config = z.object({
|
|
|
28
25
|
searchModel: z.string().volatile(),
|
|
29
26
|
imageProvider: z.string().volatile(),
|
|
30
27
|
imageModel: z.string().volatile(),
|
|
31
|
-
compactOnSwitch: z.boolean().default(true).volatile(),
|
|
32
28
|
autoReviewProvider: z.string().volatile(),
|
|
33
29
|
autoReviewModel: z.string().volatile(),
|
|
34
30
|
compactProvider: z.string().volatile(),
|
|
@@ -72,7 +68,6 @@ function readConfig(config) {
|
|
|
72
68
|
...searchModel === void 0 ? {} : { searchModel },
|
|
73
69
|
...imageProvider === void 0 ? {} : { imageProvider },
|
|
74
70
|
...imageModel === void 0 ? {} : { imageModel },
|
|
75
|
-
compactOnSwitch: config.compactOnSwitch.get(),
|
|
76
71
|
...autoReviewProvider === void 0 || autoReviewProvider === "" ? {} : { autoReviewProvider },
|
|
77
72
|
...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
|
|
78
73
|
...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
|
|
@@ -617,300 +612,6 @@ function allowDshRuntime(logger, pluginName, candidates) {
|
|
|
617
612
|
return shouldMountDshRuntime(logger, pluginName, version, new Set(Object.entries(compatibility?.dshReleases ?? {}).filter(([, status]) => status === "compatible" || status === "verified").map(([release]) => release)), compatibility?.blocklist);
|
|
618
613
|
}
|
|
619
614
|
//#endregion
|
|
620
|
-
//#region lib/types/switch-compact.js
|
|
621
|
-
/** Send-time context check and optional old-model compaction before a switched request. */
|
|
622
|
-
/** Fraction of the target window reserved for output plus estimator error. */
|
|
623
|
-
const OUTPUT_RESERVE = .2;
|
|
624
|
-
/** Extra tokens on top of the ratio; the meter heuristic is not exact. */
|
|
625
|
-
const ESTIMATE_MARGIN = 64;
|
|
626
|
-
/**
|
|
627
|
-
* Compare two fully specified model routes.
|
|
628
|
-
* @param left - first route, if one was resolved.
|
|
629
|
-
* @param right - second route, if one was resolved.
|
|
630
|
-
* @returns Whether both routes identify the same provider and model.
|
|
631
|
-
*/
|
|
632
|
-
function sameSwitchRoute(left, right) {
|
|
633
|
-
return left !== void 0 && right !== void 0 && left.provider === right.provider && left.model === right.model;
|
|
634
|
-
}
|
|
635
|
-
/**
|
|
636
|
-
* Decide whether a changed, complete route needs send-time context protection.
|
|
637
|
-
* @param enabled - whether the user enabled send-time protection.
|
|
638
|
-
* @param previous - route used by the preceding request.
|
|
639
|
-
* @param selected - route assembled for the pending request.
|
|
640
|
-
* @returns Whether the pending request changes its provider or model.
|
|
641
|
-
*/
|
|
642
|
-
function shouldProtectSwitch(enabled, previous, selected) {
|
|
643
|
-
if (!enabled) return false;
|
|
644
|
-
if (previous === void 0 || selected === void 0) return false;
|
|
645
|
-
return !sameSwitchRoute(previous, selected);
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Input budget for the target window. Reserves output (maxTokens or 20%) plus a
|
|
649
|
-
* fixed estimator margin. `undefined` means the window cannot be used.
|
|
650
|
-
* @param contextWindow - adapter-owned combined request/response capacity.
|
|
651
|
-
* @param maxTokens - optional generation cap from the live agent options.
|
|
652
|
-
* @returns Tokens available to the request input, or undefined for an invalid window.
|
|
653
|
-
*/
|
|
654
|
-
function switchInputBudget(contextWindow, maxTokens) {
|
|
655
|
-
if (!(contextWindow > 0) || !Number.isFinite(contextWindow)) return void 0;
|
|
656
|
-
const reserve = Math.max(Math.ceil(contextWindow * OUTPUT_RESERVE), maxTokens ?? 0) + ESTIMATE_MARGIN;
|
|
657
|
-
return Math.max(0, Math.floor(contextWindow - reserve));
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Determine whether an estimated input exceeds its target route budget.
|
|
661
|
-
* @param tokens - estimated input tokens.
|
|
662
|
-
* @param budget - input budget for the target window.
|
|
663
|
-
* @returns Whether the budget is unusable or the estimate is too large.
|
|
664
|
-
*/
|
|
665
|
-
function overSwitchBudget(tokens, budget) {
|
|
666
|
-
return !(budget > 0) || tokens > budget;
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Build one transcript-visible Model Switch progress notice.
|
|
670
|
-
* @param text - localized notice text shown in the transcript.
|
|
671
|
-
* @param summary - bounded event summary for durable session metadata.
|
|
672
|
-
* @returns A plugin-authored user message for the session log.
|
|
673
|
-
*/
|
|
674
|
-
function switchNotice(text, summary) {
|
|
675
|
-
return createUserMessage({
|
|
676
|
-
content: [{
|
|
677
|
-
type: "text",
|
|
678
|
-
text
|
|
679
|
-
}],
|
|
680
|
-
source: {
|
|
681
|
-
kind: "model-switch",
|
|
682
|
-
form: "notice",
|
|
683
|
-
summary: boundContextSummary(summary)
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* Extract a complete route from partial request configuration.
|
|
689
|
-
* @param value - candidate provider and model values.
|
|
690
|
-
* @returns A complete route, or undefined when either identifier is absent.
|
|
691
|
-
*/
|
|
692
|
-
function routeOf(value) {
|
|
693
|
-
if (value === void 0 || value.provider === void 0 || value.provider.length === 0 || value.model === void 0 || value.model.length === 0) return void 0;
|
|
694
|
-
return {
|
|
695
|
-
provider: value.provider,
|
|
696
|
-
model: value.model
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Install send-time switch protection on the host context.
|
|
701
|
-
* @param ctx - host context that owns agent-loop, llm, and tokenMeter.
|
|
702
|
-
* @param settings - live Model Switch settings; `compactOnSwitch !== false` is on.
|
|
703
|
-
* @param roleOf - provider-role lookup used to exclude native Agent runtimes.
|
|
704
|
-
*/
|
|
705
|
-
function installSwitchCompaction(ctx, settings, roleOf = () => void 0) {
|
|
706
|
-
const assemblies = /* @__PURE__ */ new WeakMap();
|
|
707
|
-
let stopGate;
|
|
708
|
-
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
709
|
-
const assembled = await next();
|
|
710
|
-
if (context.agent !== void 0) {
|
|
711
|
-
assemblies.set(context.agent, assembled);
|
|
712
|
-
const previous = stopGate;
|
|
713
|
-
let replacement;
|
|
714
|
-
try {
|
|
715
|
-
replacement = installGate();
|
|
716
|
-
} catch (cause) {
|
|
717
|
-
ctx.logger.warn("model-switch: could not refresh the switch-time compaction guard; the existing guard stays in place");
|
|
718
|
-
ctx.logger.warn(cause);
|
|
719
|
-
return assembled;
|
|
720
|
-
}
|
|
721
|
-
stopGate = replacement;
|
|
722
|
-
previous?.();
|
|
723
|
-
}
|
|
724
|
-
return assembled;
|
|
725
|
-
});
|
|
726
|
-
function installGate() {
|
|
727
|
-
return ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
728
|
-
const decision = await next();
|
|
729
|
-
if (decision.kind !== "enter" || signal.aborted) return decision;
|
|
730
|
-
const enabled = settings().compactOnSwitch !== false;
|
|
731
|
-
const previous = routeOf(agent.session.requestHeader()?.config);
|
|
732
|
-
const assembly = assemblies.get(agent);
|
|
733
|
-
const selected = routeOf(assembly === void 0 ? void 0 : {
|
|
734
|
-
...assembly.variables.provider === void 0 ? {} : { provider: assembly.variables.provider },
|
|
735
|
-
...assembly.variables.model === void 0 ? {} : { model: assembly.variables.model }
|
|
736
|
-
});
|
|
737
|
-
if (!shouldProtectSwitch(enabled, previous, selected) || previous === void 0 || selected === void 0) return decision;
|
|
738
|
-
if (roleOf(previous.provider) === "agent" || roleOf(selected.provider) === "agent") return decision;
|
|
739
|
-
return protectSwitch(ctx, agent, previous, selected, assembly, decision, messages, signal);
|
|
740
|
-
}, { prepend: true });
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
async function protectSwitch(ctx, agent, previous, selected, assembly, decision, claimed, signal) {
|
|
744
|
-
const session = agent.session;
|
|
745
|
-
const notice = (text, summary) => {
|
|
746
|
-
session.append("user/message", switchNotice(text, summary), { surfaceOp: "append" });
|
|
747
|
-
};
|
|
748
|
-
const reject = (reason, summary, compacted) => {
|
|
749
|
-
notice(compacted ? `无法继续本次模型切换:压缩已发生,但请求未继续(${reason})。` : `无法继续本次模型切换:${reason}`, summary);
|
|
750
|
-
retainUserClaims(session, claimed);
|
|
751
|
-
return { kind: "reject" };
|
|
752
|
-
};
|
|
753
|
-
notice("正在检查上下文…", "checking context");
|
|
754
|
-
if (assembly === void 0) return reject("无法确定本次请求组装的目标模型。", "switch blocked: no assembled target", false);
|
|
755
|
-
const llm = ctx.get("llm", false);
|
|
756
|
-
if (llm?.resolveModelInfo === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
|
|
757
|
-
let window;
|
|
758
|
-
try {
|
|
759
|
-
const value = (await llm.resolveModelInfo(selected.provider, selected.model, signal)).context?.contextWindow;
|
|
760
|
-
window = typeof value === "number" && value > 0 ? value : void 0;
|
|
761
|
-
} catch {
|
|
762
|
-
window = void 0;
|
|
763
|
-
}
|
|
764
|
-
if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", false);
|
|
765
|
-
if (window === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
|
|
766
|
-
const meter = ctx.get("tokenMeter", false);
|
|
767
|
-
if (meter === void 0 || typeof meter.measure !== "function" || typeof meter.estimateMessage !== "function") return reject("无法估算待发上下文(缺少 token 计量)。", "switch blocked: no token meter", false);
|
|
768
|
-
const header = targetHeader(selected, assembly);
|
|
769
|
-
const previousHeader = session.requestHeader();
|
|
770
|
-
let pressure;
|
|
771
|
-
try {
|
|
772
|
-
pressure = estimateSwitchPressure(meter, session, header, assembly, decision.messages, previousHeader);
|
|
773
|
-
} catch (error) {
|
|
774
|
-
return reject(`无法估算待发上下文(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: estimate failed", false);
|
|
775
|
-
}
|
|
776
|
-
const budget = agent.options.maxTokens === void 0 ? switchInputBudget(window) : switchInputBudget(window, agent.options.maxTokens);
|
|
777
|
-
if (budget === void 0) return reject("无法确定目标模型的上下文窗口。", "switch blocked: unknown window", false);
|
|
778
|
-
const passed = switchNotice("上下文容量检查通过,无需压缩。", "context check passed");
|
|
779
|
-
const completed = switchNotice("压缩完成,复查通过。", "compacted");
|
|
780
|
-
if (!overSwitchBudget(pressure + meter.estimateMessage(passed), budget)) {
|
|
781
|
-
session.append("user/message", passed, { surfaceOp: "append" });
|
|
782
|
-
return decision;
|
|
783
|
-
}
|
|
784
|
-
let fixedPressure;
|
|
785
|
-
try {
|
|
786
|
-
fixedPressure = estimateFixedInput(meter, session, header, assembly, decision.messages);
|
|
787
|
-
} catch (error) {
|
|
788
|
-
return reject(`无法估算待发上下文(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: estimate failed", false);
|
|
789
|
-
}
|
|
790
|
-
if (overSwitchBudget(fixedPressure + meter.estimateMessage(completed), budget)) return reject("新消息或固定请求内容已超过目标模型可用容量。", "switch blocked: fixed input oversized", false);
|
|
791
|
-
notice("当前上下文超过目标模型可用容量,正在使用旧模型压缩…", "compacting with previous model");
|
|
792
|
-
const compactionGeneration = session.surface.replaceGeneration;
|
|
793
|
-
let compacted = false;
|
|
794
|
-
try {
|
|
795
|
-
const result = await compactWithPrevious(ctx, agent, previous, signal);
|
|
796
|
-
compacted = result !== "none" || session.surface.replaceGeneration > compactionGeneration;
|
|
797
|
-
if (result === "none") return reject("没有可压缩的历史。", "switch blocked: nothing to compact", compacted);
|
|
798
|
-
} catch (error) {
|
|
799
|
-
compacted = compacted || session.surface.replaceGeneration > compactionGeneration;
|
|
800
|
-
if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", compacted);
|
|
801
|
-
return reject(`压缩失败(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: compact failed", compacted);
|
|
802
|
-
}
|
|
803
|
-
if (signal.aborted) return reject("已取消。", "switch blocked: cancelled", compacted);
|
|
804
|
-
let again;
|
|
805
|
-
try {
|
|
806
|
-
again = estimateSwitchPressure(meter, session, header, assembly, decision.messages);
|
|
807
|
-
} catch (error) {
|
|
808
|
-
return reject(`压缩后无法复查(${error instanceof Error ? error.message : String(error)})。`, "switch blocked: remeasure failed", compacted);
|
|
809
|
-
}
|
|
810
|
-
if (overSwitchBudget(again + meter.estimateMessage(completed), budget)) return reject("压缩后仍超过目标模型可用容量。", "switch blocked: still over budget", compacted);
|
|
811
|
-
session.append("user/message", completed, { surfaceOp: "append" });
|
|
812
|
-
return decision;
|
|
813
|
-
}
|
|
814
|
-
function targetHeader(selected, assembly) {
|
|
815
|
-
return canonicalHeader({
|
|
816
|
-
config: {
|
|
817
|
-
provider: selected.provider,
|
|
818
|
-
model: selected.model
|
|
819
|
-
},
|
|
820
|
-
...assembly.tools.length > 0 ? { tools: [...assembly.tools] } : {}
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Next-request input under the target route. Uses the committed surface priced
|
|
825
|
-
* for the target (including image tokens), then adds still-pending enter
|
|
826
|
-
* messages and the prospective system prompt while dropping already-priced
|
|
827
|
-
* system nodes so they are not counted twice.
|
|
828
|
-
*
|
|
829
|
-
* The target envelope has no provider usage of its own, so the meter can only
|
|
830
|
-
* price it with the chars-per-token heuristic. That heuristic is unconservative
|
|
831
|
-
* for dense text (base64 payloads, minified sources, CJK). The previous
|
|
832
|
-
* envelope holds the provider's reported usage for the same surface, so the
|
|
833
|
-
* switch takes the larger of the two prices: the anchor keeps the check
|
|
834
|
-
* truthful exactly where a switch makes the heuristic weakest.
|
|
835
|
-
*
|
|
836
|
-
* @param meter - replay token meter.
|
|
837
|
-
* @param session - session being measured.
|
|
838
|
-
* @param header - target request envelope.
|
|
839
|
-
* @param assembly - prospective prompt assembly.
|
|
840
|
-
* @param pending - messages the pre-step is about to enter.
|
|
841
|
-
* @param previousHeader - envelope of the last request, when the session has one.
|
|
842
|
-
* @returns Estimated next-request input tokens.
|
|
843
|
-
*/
|
|
844
|
-
function estimateSwitchPressure(meter, session, header, assembly, pending, previousHeader) {
|
|
845
|
-
const estimate = routePressure(meter, session, header, assembly, pending);
|
|
846
|
-
if (previousHeader === void 0) return estimate;
|
|
847
|
-
return Math.max(estimate, routePressure(meter, session, previousHeader, assembly, pending));
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* Price one request envelope against the committed surface.
|
|
851
|
-
* @param meter - replay token meter.
|
|
852
|
-
* @param session - session being measured.
|
|
853
|
-
* @param header - request envelope to price.
|
|
854
|
-
* @param assembly - prospective prompt assembly.
|
|
855
|
-
* @param pending - messages the pre-step is about to enter.
|
|
856
|
-
* @returns Estimated input tokens for that envelope.
|
|
857
|
-
*/
|
|
858
|
-
function routePressure(meter, session, header, assembly, pending) {
|
|
859
|
-
const measured = meter.measure(session, header);
|
|
860
|
-
let systemTokens = 0;
|
|
861
|
-
for (const node of measured.nodes) if (session.eventAt(node.seq)?.type === "system/message") systemTokens += node.tokens;
|
|
862
|
-
return measured.totalTokens - systemTokens + systemPromptTokens(meter, assembly) + pendingTokens(meter, pending);
|
|
863
|
-
}
|
|
864
|
-
function estimateFixedInput(meter, session, header, assembly, pending) {
|
|
865
|
-
const measured = meter.measure(session, header);
|
|
866
|
-
return measured.totalTokens - measured.surfaceTokens + systemPromptTokens(meter, assembly) + pendingTokens(meter, pending);
|
|
867
|
-
}
|
|
868
|
-
function pendingTokens(meter, messages) {
|
|
869
|
-
let total = 0;
|
|
870
|
-
for (const message of messages) {
|
|
871
|
-
const tokens = meter.estimateMessage(message);
|
|
872
|
-
if (!(tokens >= 0) || !Number.isFinite(tokens)) throw new Error("pending message token estimate is not a number");
|
|
873
|
-
total += tokens;
|
|
874
|
-
}
|
|
875
|
-
return total;
|
|
876
|
-
}
|
|
877
|
-
function systemPromptTokens(meter, assembly) {
|
|
878
|
-
const text = renderPrompt(assembly);
|
|
879
|
-
if (text.length === 0) return 0;
|
|
880
|
-
return meter.estimateMessage(createMessage({
|
|
881
|
-
role: "system",
|
|
882
|
-
content: [{
|
|
883
|
-
type: "text",
|
|
884
|
-
text
|
|
885
|
-
}],
|
|
886
|
-
source: { kind: "system-prompt" }
|
|
887
|
-
}));
|
|
888
|
-
}
|
|
889
|
-
function retainUserClaims(session, claimed) {
|
|
890
|
-
for (const message of claimed) {
|
|
891
|
-
if (message.source.kind !== "user") continue;
|
|
892
|
-
session.append("user/message", message, { surfaceOp: "append" });
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
async function compactWithPrevious(ctx, agent, previous, signal) {
|
|
896
|
-
const isolated = ctx.isolate("compaction");
|
|
897
|
-
const Engine = (await import("@deepseek-ai/dsh-compaction-basic")).default;
|
|
898
|
-
const fiber = isolated.plugin(Engine, {
|
|
899
|
-
auto: false,
|
|
900
|
-
summarizationProvider: previous.provider,
|
|
901
|
-
summarizationModel: previous.model,
|
|
902
|
-
compactionRetries: 0
|
|
903
|
-
});
|
|
904
|
-
try {
|
|
905
|
-
await fiber;
|
|
906
|
-
const engine = isolated.get("compaction", false);
|
|
907
|
-
if (engine === void 0) throw new Error("private compaction engine is unavailable");
|
|
908
|
-
return await engine.compactIfNeeded(agent, "context-overflow", signal) === null ? "none" : "compacted";
|
|
909
|
-
} finally {
|
|
910
|
-
await fiber.dispose();
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
//#endregion
|
|
914
615
|
//#region lib/types/host-runtime.js
|
|
915
616
|
/** Host owner for Model Switch settings. */
|
|
916
617
|
var ModelSwitchRuntime = class extends Service {
|
|
@@ -933,7 +634,6 @@ var ModelSwitchRuntime = class extends Service {
|
|
|
933
634
|
this.source = () => readConfig(entry);
|
|
934
635
|
ctx.inject(["settings"], (scope) => scope.effect(() => scope.settings.configure({ auto: false }, ctx.fiber), "Model Switch: disable automatic settings form"));
|
|
935
636
|
if (!allowDshRuntime(ctx.logger, "dsh-model-switch", ["@deepseek-ai/dsh-agent"])) return;
|
|
936
|
-
installSwitchCompaction(ctx, () => this.source(), (provider) => this.adapters.get(provider)?.role);
|
|
937
637
|
installDeepSeekSearchAdapter(ctx);
|
|
938
638
|
installModelSwitchSearchProvider(ctx, this);
|
|
939
639
|
installCapabilitiesRpc(ctx, this.adapters, () => this.capabilities);
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
//#region lib/types/provider-group.js
|
|
2
2
|
/**
|
|
3
|
-
* Provider configuration groups: per-provider presets for the
|
|
4
|
-
* and subagent routes.
|
|
3
|
+
* Provider configuration groups: per-provider presets for the compaction,
|
|
4
|
+
* auto-review, session-naming, and subagent routes.
|
|
5
5
|
*
|
|
6
6
|
* A group is a DERIVED policy, not a write: `src/session-route.ts` resolves the
|
|
7
7
|
* routes one session should use from its own provider plus this stored table, so
|
|
8
8
|
* switching a session's provider never rewrites deployment-wide settings and two
|
|
9
9
|
* sessions on different providers cannot overwrite each other. The field names
|
|
10
10
|
* below are the flat settings field names a group pins, which keeps the stored
|
|
11
|
-
* shape identical
|
|
12
|
-
*
|
|
13
|
-
* Legacy entries may still carry `compactProvider`/`compactModel`/
|
|
14
|
-
* `autoReviewProvider`/`autoReviewModel`: normalization preserves them so an
|
|
15
|
-
* older stored document is never silently rewritten into a different meaning,
|
|
16
|
-
* while the consumption side ignores them (a group save from the current UI
|
|
17
|
-
* writes '' for every unsupported field, which normalization drops).
|
|
11
|
+
* shape identical across releases.
|
|
18
12
|
*
|
|
19
13
|
* All functions here are pure and never throw on malformed storage.
|
|
20
14
|
*/
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { t as groupOf } from "./provider-group-CwadImhH.js";
|
|
2
|
+
//#region lib/types/session-route.js
|
|
3
|
+
/**
|
|
4
|
+
* Per-session capability route derivation.
|
|
5
|
+
*
|
|
6
|
+
* Model Switch stores one deployment-wide route per capability (the flat
|
|
7
|
+
* settings fields) plus, per provider, a saved provider group. Nothing is
|
|
8
|
+
* written when a session changes provider: this module DERIVES the route a
|
|
9
|
+
* capability should use for one session from (a) the session's own provider and
|
|
10
|
+
* (b) the stored group table. Two sessions on different providers therefore
|
|
11
|
+
* resolve independently, and no session's actions rewrite another session's
|
|
12
|
+
* route.
|
|
13
|
+
*
|
|
14
|
+
* The session's provider is read from durable, official state in this order:
|
|
15
|
+
* the `modelSelection` session projection (`pending` before `lastUsed`, so a
|
|
16
|
+
* selection the user just made counts), then the latest logged request-context
|
|
17
|
+
* route, then the latest `request/header` route. Every read is optional: a
|
|
18
|
+
* session with no observable provider, an unregistered projection, or a
|
|
19
|
+
* profile without the projection package all resolve to the deployment default,
|
|
20
|
+
* which is exactly the pre-derivation behavior.
|
|
21
|
+
*
|
|
22
|
+
* All functions are pure except the readers, which never throw and never write.
|
|
23
|
+
*/
|
|
24
|
+
/** Flat deployment-default fields per capability, in group-storage field names. */
|
|
25
|
+
const CAPABILITY_FIELDS = Object.freeze({
|
|
26
|
+
compaction: {
|
|
27
|
+
provider: "compactProvider",
|
|
28
|
+
model: "compactModel"
|
|
29
|
+
},
|
|
30
|
+
autoReview: {
|
|
31
|
+
provider: "autoReviewProvider",
|
|
32
|
+
model: "autoReviewModel"
|
|
33
|
+
},
|
|
34
|
+
title: {
|
|
35
|
+
provider: "titleProvider",
|
|
36
|
+
model: "titleModel"
|
|
37
|
+
},
|
|
38
|
+
subagent: {
|
|
39
|
+
provider: "subagentProvider",
|
|
40
|
+
model: "subagentModel"
|
|
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
|
+
/** Read one capability's pair from a flat route record; undefined unless both halves are present. */
|
|
48
|
+
function pairOf(source, fields) {
|
|
49
|
+
if (source === void 0) return void 0;
|
|
50
|
+
const record = source;
|
|
51
|
+
const provider = record[fields.provider];
|
|
52
|
+
const model = record[fields.model];
|
|
53
|
+
if (!present(provider) || !present(model)) return void 0;
|
|
54
|
+
return {
|
|
55
|
+
provider,
|
|
56
|
+
model
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The deployment-wide route for one capability: the flat settings pair.
|
|
61
|
+
* @param settings - live Model Switch settings; undefined means none are reachable.
|
|
62
|
+
* @param capability - capability whose flat pair is read.
|
|
63
|
+
* @returns The complete pair, or undefined when the capability stays at its official/derived default.
|
|
64
|
+
*/
|
|
65
|
+
function deploymentRoute(settings, capability) {
|
|
66
|
+
return pairOf(settings, CAPABILITY_FIELDS[capability]);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The route a subagent spawn should use for one parent-session provider.
|
|
70
|
+
*
|
|
71
|
+
* A saved group for that provider pins the spawn; otherwise the flat Subagent
|
|
72
|
+
* card pins it, but only in `fixed` mode — `follow-main` IS the official inherit
|
|
73
|
+
* path, expressed by leaving the request untouched rather than by naming it.
|
|
74
|
+
* @param settings - live Model Switch settings.
|
|
75
|
+
* @param provider - the delegating session's provider, when one is observable.
|
|
76
|
+
* @returns The complete route, or undefined for official inherit.
|
|
77
|
+
*/
|
|
78
|
+
function subagentRouteOf(settings, provider) {
|
|
79
|
+
const group = groupRoute(settings, provider, "subagent");
|
|
80
|
+
if (group !== void 0) {
|
|
81
|
+
const effort = groupOf(settings?.providerGroups, provider ?? "")?.subagentReasoningEffort;
|
|
82
|
+
return present(effort) ? {
|
|
83
|
+
...group,
|
|
84
|
+
reasoningEffort: effort
|
|
85
|
+
} : group;
|
|
86
|
+
}
|
|
87
|
+
if (settings?.subagentMode !== "fixed") return void 0;
|
|
88
|
+
const fixed = deploymentRoute(settings, "subagent");
|
|
89
|
+
if (fixed === void 0) return void 0;
|
|
90
|
+
return present(settings.subagentReasoningEffort) ? {
|
|
91
|
+
...fixed,
|
|
92
|
+
reasoningEffort: settings.subagentReasoningEffort
|
|
93
|
+
} : fixed;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One provider's saved route for one capability.
|
|
97
|
+
* @param settings - live Model Switch settings carrying the group table.
|
|
98
|
+
* @param provider - the session's provider; blank or unknown resolves to undefined.
|
|
99
|
+
* @param capability - capability whose group pin is read.
|
|
100
|
+
* @returns The complete group pair, or undefined when the group pins nothing here.
|
|
101
|
+
*/
|
|
102
|
+
function groupRoute(settings, provider, capability) {
|
|
103
|
+
if (!present(provider)) return void 0;
|
|
104
|
+
return pairOf(groupOf(settings?.providerGroups, provider), CAPABILITY_FIELDS[capability]);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The route one capability should use for one session.
|
|
108
|
+
*
|
|
109
|
+
* A saved provider group for the session's provider wins; otherwise the flat
|
|
110
|
+
* deployment default applies. An unknown session provider skips every group, so
|
|
111
|
+
* a session Model Switch cannot attribute keeps the deployment default.
|
|
112
|
+
* @param settings - live Model Switch settings.
|
|
113
|
+
* @param provider - the session's provider, when one is observable.
|
|
114
|
+
* @param capability - capability being resolved.
|
|
115
|
+
* @returns The complete pair, or undefined for the capability's official default.
|
|
116
|
+
*/
|
|
117
|
+
function resolveCapabilityRoute(settings, provider, capability) {
|
|
118
|
+
return groupRoute(settings, provider, capability) ?? deploymentRoute(settings, capability);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read the provider from the durable model-selection projection.
|
|
122
|
+
* @param ctx - context that may expose `sessionProjections`.
|
|
123
|
+
* @param session - the session whose selection is read.
|
|
124
|
+
* @returns The pending provider, else the last-used provider, else undefined.
|
|
125
|
+
*/
|
|
126
|
+
function projectionProvider(ctx, session) {
|
|
127
|
+
let state;
|
|
128
|
+
try {
|
|
129
|
+
state = ctx.get("sessionProjections", false)?.stateOf(session, "modelSelection");
|
|
130
|
+
} catch {
|
|
131
|
+
state = void 0;
|
|
132
|
+
}
|
|
133
|
+
if (state === void 0) return void 0;
|
|
134
|
+
if (present(state.pending?.provider)) return state.pending.provider;
|
|
135
|
+
if (present(state.lastUsed?.provider)) return state.lastUsed.provider;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The provider one session is using, from official durable state.
|
|
139
|
+
*
|
|
140
|
+
* Preferred source is the session's own model selection (it covers a switch the
|
|
141
|
+
* client has sent but no request has consumed yet); the logged request-context
|
|
142
|
+
* and request-header routes cover profiles without the projection package and
|
|
143
|
+
* replayed sessions that never emitted a selection.
|
|
144
|
+
* @param ctx - context used for the optional projection read.
|
|
145
|
+
* @param session - the session to resolve; undefined resolves to undefined.
|
|
146
|
+
* @returns The provider, or undefined when nothing durable names one.
|
|
147
|
+
*/
|
|
148
|
+
function sessionProviderOf(ctx, session) {
|
|
149
|
+
if (session === void 0) return void 0;
|
|
150
|
+
const selected = projectionProvider(ctx, session);
|
|
151
|
+
if (selected !== void 0) return selected;
|
|
152
|
+
try {
|
|
153
|
+
const context = session.requestContext()?.provider;
|
|
154
|
+
if (present(context)) return context;
|
|
155
|
+
const header = session.requestHeader()?.config.provider;
|
|
156
|
+
if (present(header)) return header;
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The subagent route one exact session's spawns should use.
|
|
163
|
+
* @param ctx - context used for the optional session reads.
|
|
164
|
+
* @param session - the delegating session; undefined keeps official inherit.
|
|
165
|
+
* @param settings - live Model Switch settings.
|
|
166
|
+
* @returns The complete route, or undefined for official inherit.
|
|
167
|
+
*/
|
|
168
|
+
function sessionSubagentRouteOf(ctx, session, settings) {
|
|
169
|
+
return subagentRouteOf(settings, sessionProviderOf(ctx, session));
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { sessionSubagentRouteOf as a, sessionProviderOf as i, groupRoute as n, resolveCapabilityRoute as r, deploymentRoute as t };
|