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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region lib/types/provider-group.js
|
|
2
|
+
/**
|
|
3
|
+
* Provider configuration groups: per-provider presets for the compaction,
|
|
4
|
+
* auto-review, session-naming, and subagent routes.
|
|
5
|
+
*
|
|
6
|
+
* A group is a DERIVED policy, not a write: `src/session-route.ts` resolves the
|
|
7
|
+
* routes one session should use from its own provider plus this stored table, so
|
|
8
|
+
* switching a session's provider never rewrites deployment-wide settings and two
|
|
9
|
+
* sessions on different providers cannot overwrite each other. The field names
|
|
10
|
+
* below are the flat settings field names a group pins, which keeps the stored
|
|
11
|
+
* shape identical across releases.
|
|
12
|
+
*
|
|
13
|
+
* All functions here are pure and never throw on malformed storage.
|
|
14
|
+
*/
|
|
15
|
+
const ROUTE_FIELDS = Object.freeze([
|
|
16
|
+
"compactProvider",
|
|
17
|
+
"compactModel",
|
|
18
|
+
"autoReviewProvider",
|
|
19
|
+
"autoReviewModel",
|
|
20
|
+
"subagentProvider",
|
|
21
|
+
"subagentModel",
|
|
22
|
+
"subagentReasoningEffort",
|
|
23
|
+
"titleProvider",
|
|
24
|
+
"titleModel"
|
|
25
|
+
]);
|
|
26
|
+
/** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
|
|
27
|
+
function setKey(target, key, value) {
|
|
28
|
+
Object.defineProperty(target, key, {
|
|
29
|
+
value,
|
|
30
|
+
enumerable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
configurable: true
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** A non-blank string. Blank values behave exactly like an absent field. */
|
|
36
|
+
function present(value) {
|
|
37
|
+
return typeof value === "string" && value.trim() !== "";
|
|
38
|
+
}
|
|
39
|
+
/** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
|
|
40
|
+
function normalizeEntry(value) {
|
|
41
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
42
|
+
const source = value;
|
|
43
|
+
const group = {};
|
|
44
|
+
for (const field of ROUTE_FIELDS) {
|
|
45
|
+
const item = source[field];
|
|
46
|
+
if (present(item)) group[field] = item;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
provider: present(source.provider) ? source.provider : "",
|
|
50
|
+
group
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Strip a raw stored value to well-formed groups; never throws. Both storage
|
|
55
|
+
* shapes are accepted: the stored entry array (keys live in each entry's
|
|
56
|
+
* `provider` field) and a hand-written provider → routes map (keys are the
|
|
57
|
+
* providers). All-empty entries stay: they pin a provider to every default.
|
|
58
|
+
*/
|
|
59
|
+
function normalizeProviderGroups(raw) {
|
|
60
|
+
const groups = {};
|
|
61
|
+
if (Array.isArray(raw)) {
|
|
62
|
+
for (const entry of raw) {
|
|
63
|
+
const view = normalizeEntry(entry);
|
|
64
|
+
if (view === void 0 || view.provider === "") continue;
|
|
65
|
+
setKey(groups, view.provider, view.group);
|
|
66
|
+
}
|
|
67
|
+
return groups;
|
|
68
|
+
}
|
|
69
|
+
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
|
|
70
|
+
if (key.trim() === "") continue;
|
|
71
|
+
const view = normalizeEntry(value);
|
|
72
|
+
if (view === void 0) continue;
|
|
73
|
+
setKey(groups, key, view.group);
|
|
74
|
+
}
|
|
75
|
+
return groups;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The saved group for a provider.
|
|
79
|
+
* @returns the normalized group, or undefined when the provider has no group —
|
|
80
|
+
* which is what leaves the live routes untouched during a switch.
|
|
81
|
+
*/
|
|
82
|
+
function groupOf(groups, provider) {
|
|
83
|
+
if (groups === void 0 || provider.trim() === "") return void 0;
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(groups, provider)) return void 0;
|
|
85
|
+
return groups[provider];
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { normalizeProviderGroups as n, groupOf as t };
|
|
@@ -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 { sessionProviderOf as n, sessionSubagentRouteOf as r, resolveCapabilityRoute as t };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-route-BCQFzqRP.js";
|
|
2
|
+
//#region lib/types/session-title.js
|
|
3
|
+
/**
|
|
4
|
+
* Model Switch-owned session-naming row.
|
|
5
|
+
*
|
|
6
|
+
* The official naming rows hardwire the naming route at registration time: the
|
|
7
|
+
* configured provider/model pair wins over the session's own logged route for
|
|
8
|
+
* every session in the profile. This row registers ONE provider of its own and
|
|
9
|
+
* resolves the route PER REQUEST instead, so a session whose provider has a
|
|
10
|
+
* saved group is named through that group's pair while every other session keeps
|
|
11
|
+
* following its own model.
|
|
12
|
+
*
|
|
13
|
+
* The call itself stays inside the official implementation: the exported
|
|
14
|
+
* `generateSessionTitleWithLlm` receives the session-resolved config and records
|
|
15
|
+
* exactly that route in `session/title-llm-request`, so the durable record and
|
|
16
|
+
* the actual request can never disagree. Nothing is remounted when settings
|
|
17
|
+
* change — the route is read through on every naming request — so a settings
|
|
18
|
+
* save can neither interrupt an in-flight naming request nor leave the profile
|
|
19
|
+
* without a registered provider.
|
|
20
|
+
*
|
|
21
|
+
* `cordis.patch.yml` disables the official row (the seam allows one registered
|
|
22
|
+
* provider per context), and this row mirrors the cadence the disabled row
|
|
23
|
+
* declared, so a profile that restated the row as all-prompts keeps its cadence.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-model-switch/session-title
|
|
26
|
+
*/
|
|
27
|
+
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
|
|
28
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
29
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
30
|
+
});
|
|
31
|
+
return path;
|
|
32
|
+
};
|
|
33
|
+
/** Official session-title provider row this module supersedes. */
|
|
34
|
+
const OFFICIAL_TITLE_ROW = "session-title-llm";
|
|
35
|
+
/** The auxiliary-LLM package carrying the shared naming policy and call. */
|
|
36
|
+
const TITLE_LLM_PACKAGE = "@deepseek-ai/dsh-session-title-llm";
|
|
37
|
+
/** Cordis plugin identity; matches the cordis.patch.yml insert row id. */
|
|
38
|
+
const name = "model-switch-session-title";
|
|
39
|
+
/** The loader names the disabled row, and the seam services the call needs are awaited before the first naming request. */
|
|
40
|
+
const inject = [
|
|
41
|
+
"loader",
|
|
42
|
+
"sessionTitle",
|
|
43
|
+
"llm"
|
|
44
|
+
];
|
|
45
|
+
/** Both official cadence packages, in the order a profile may declare them. */
|
|
46
|
+
const TITLE_CADENCES = Object.freeze([Object.freeze({
|
|
47
|
+
pkg: "@deepseek-ai/dsh-session-title-first-prompt-llm",
|
|
48
|
+
id: "session-title-first-prompt-llm",
|
|
49
|
+
automatic: "first-prompt",
|
|
50
|
+
firstPromptOnly: true
|
|
51
|
+
}), Object.freeze({
|
|
52
|
+
pkg: "@deepseek-ai/dsh-session-title-all-prompts-llm",
|
|
53
|
+
id: "session-title-all-prompts-llm",
|
|
54
|
+
automatic: "all-prompts",
|
|
55
|
+
firstPromptOnly: false
|
|
56
|
+
})]);
|
|
57
|
+
/**
|
|
58
|
+
* The cadence one official package name declares.
|
|
59
|
+
* @param pkg - the disabled row's package name.
|
|
60
|
+
* @returns The matching cadence, or undefined for a row this row cannot mirror.
|
|
61
|
+
*/
|
|
62
|
+
function titleCadence(pkg) {
|
|
63
|
+
if (pkg === void 0) return void 0;
|
|
64
|
+
return TITLE_CADENCES.find((cadence) => cadence.pkg === pkg);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Overlay the session-resolved route over the row's own validated policy.
|
|
68
|
+
*
|
|
69
|
+
* A resolved route always names both halves. When nothing resolves, the row's
|
|
70
|
+
* own policy is returned unchanged: an explicit pair the profile configured on
|
|
71
|
+
* the official row stays the deployment default, and a row without one keeps
|
|
72
|
+
* following each session's logged route.
|
|
73
|
+
* @param config - policy validated from the disabled official row.
|
|
74
|
+
* @param route - session-resolved naming route, or undefined for the row's own policy.
|
|
75
|
+
* @returns A detached config for one naming request.
|
|
76
|
+
*/
|
|
77
|
+
function composeTitleRoute(config, route) {
|
|
78
|
+
if (route === void 0) return config;
|
|
79
|
+
return {
|
|
80
|
+
...config,
|
|
81
|
+
provider: route.provider,
|
|
82
|
+
model: route.model
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Frame the exact human messages one naming revision represents.
|
|
87
|
+
* @param messages - every eligible human message of the revision.
|
|
88
|
+
* @param firstPromptOnly - cadence flag: true mirrors the first-prompt provider.
|
|
89
|
+
* @returns The selected messages, in order.
|
|
90
|
+
* @throws when a first-prompt cadence receives an empty revision — the official provider's own contract.
|
|
91
|
+
*/
|
|
92
|
+
function selectTitleMessages(messages, firstPromptOnly) {
|
|
93
|
+
if (!firstPromptOnly) return messages;
|
|
94
|
+
const first = messages[0];
|
|
95
|
+
if (first === void 0) throw new Error("first-prompt title provider requires one human message");
|
|
96
|
+
return [first];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Locate the official session-title provider row in the loader tree.
|
|
100
|
+
* Disabled entries stay in the tree, so a name-matched disable still matches.
|
|
101
|
+
* @param loader - the host loader, when one is reachable.
|
|
102
|
+
* @returns The official entry, or undefined when this profile has no such row.
|
|
103
|
+
*/
|
|
104
|
+
function officialTitleEntry(loader) {
|
|
105
|
+
if (!loader) return void 0;
|
|
106
|
+
for (const entry of loader.entries()) {
|
|
107
|
+
if (entry.id === OFFICIAL_TITLE_ROW) return entry;
|
|
108
|
+
const pkg = entry.options?.name;
|
|
109
|
+
if (typeof pkg === "string" && TITLE_CADENCES.some((cadence) => cadence.pkg === pkg)) return entry;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Copy the official row's stored config, ignoring non-object payloads.
|
|
114
|
+
* @param entry - the official session-title provider entry.
|
|
115
|
+
* @returns A plain object spread; missing or invalid configs become `{}`. The
|
|
116
|
+
* result is untrusted: the profile may be hand-edited, so the official policy
|
|
117
|
+
* validator runs over it before any naming request is served.
|
|
118
|
+
*/
|
|
119
|
+
function forwardedConfig(entry) {
|
|
120
|
+
const raw = entry.options?.config;
|
|
121
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The official package name this row mounts (either cadence).
|
|
126
|
+
* @param entry - the official session-title provider entry.
|
|
127
|
+
* @returns The package name, or undefined when the row declares none.
|
|
128
|
+
*/
|
|
129
|
+
function officialTitlePackage(entry) {
|
|
130
|
+
const pkg = entry.options?.name;
|
|
131
|
+
return typeof pkg === "string" && pkg.trim() !== "" ? pkg : void 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Read live Model Switch settings without requiring the host row.
|
|
135
|
+
*
|
|
136
|
+
* The host is read structurally instead of injected so this row keeps mirroring
|
|
137
|
+
* the official naming behavior when the `model-switch` row is disabled: an
|
|
138
|
+
* unreachable host resolves to the row's own policy rather than to a pinned
|
|
139
|
+
* route.
|
|
140
|
+
* @param ctx - this row's context.
|
|
141
|
+
* @returns The settings, or undefined when the host is unavailable.
|
|
142
|
+
*/
|
|
143
|
+
function currentSettings(ctx) {
|
|
144
|
+
try {
|
|
145
|
+
return ctx.get("modelSwitch", false)?.currentSettings();
|
|
146
|
+
} catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Execute one naming request through the official implementation.
|
|
152
|
+
*
|
|
153
|
+
* The route is resolved from the session being titled — its provider selects the
|
|
154
|
+
* saved group, and the flat settings pair is the deployment default — and is
|
|
155
|
+
* handed to `generateSessionTitleWithLlm`, which frames the request, records the
|
|
156
|
+
* exact route in `session/title-llm-request`, and validates the result.
|
|
157
|
+
* @param ctx - this row's context, exposing `llm`.
|
|
158
|
+
* @param module - the loaded auxiliary-LLM module.
|
|
159
|
+
* @param plan - the profile's fixed naming policy.
|
|
160
|
+
* @param request - service-owned session, message revision, and cancellation.
|
|
161
|
+
* @returns The official provider result.
|
|
162
|
+
*/
|
|
163
|
+
async function generateTitle(ctx, module, plan, request) {
|
|
164
|
+
const provider = sessionProviderOf(ctx, request.session);
|
|
165
|
+
const route = resolveCapabilityRoute(currentSettings(ctx), provider, "title");
|
|
166
|
+
return module.generateSessionTitleWithLlm(ctx, composeTitleRoute(plan.config, route), request, selectTitleMessages(request.messages, plan.cadence.firstPromptOnly), plan.cadence.id);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Mount the Model Switch session-naming row for this profile.
|
|
170
|
+
*
|
|
171
|
+
* A profile with no official naming row, an unrecognized row package, a missing
|
|
172
|
+
* auxiliary-LLM package, or an invalid stored policy keeps the session-title
|
|
173
|
+
* service's deterministic fallback titles and reports the lost capability
|
|
174
|
+
* explicitly instead of guessing a route. The registration is an effect of this
|
|
175
|
+
* row's fiber, so unloading the profile unregisters the provider.
|
|
176
|
+
* @param ctx - this row's context.
|
|
177
|
+
*/
|
|
178
|
+
async function apply(ctx) {
|
|
179
|
+
const entry = officialTitleEntry(ctx.get("loader", false));
|
|
180
|
+
const cadence = titleCadence(entry === void 0 ? void 0 : officialTitlePackage(entry)) ?? TITLE_CADENCES[0];
|
|
181
|
+
if (cadence === void 0) return;
|
|
182
|
+
if (entry === void 0) ctx.logger.info("model-switch: no instantiated official session-title row to mirror; naming uses the first-prompt cadence");
|
|
183
|
+
/** The displaced row's naming policy, or the official defaults when it is absent. */
|
|
184
|
+
const forwarded = entry === void 0 ? {} : forwardedConfig(entry);
|
|
185
|
+
let module;
|
|
186
|
+
try {
|
|
187
|
+
module = await import(__rewriteRelativeImportExtension(TITLE_LLM_PACKAGE));
|
|
188
|
+
} catch (cause) {
|
|
189
|
+
ctx.logger.error(`model-switch: ${TITLE_LLM_PACKAGE} is unavailable; model-backed session naming cannot run at all`);
|
|
190
|
+
ctx.logger.error(cause);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let config;
|
|
194
|
+
try {
|
|
195
|
+
config = module.resolveSessionTitleLlmConfig(forwarded);
|
|
196
|
+
} catch (cause) {
|
|
197
|
+
ctx.logger.error("model-switch: the official session-title row has no valid naming policy; naming keeps the deterministic fallback");
|
|
198
|
+
ctx.logger.error(cause);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const plan = {
|
|
202
|
+
cadence,
|
|
203
|
+
config
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
ctx.sessionTitle.register({
|
|
207
|
+
id: cadence.id,
|
|
208
|
+
automatic: cadence.automatic,
|
|
209
|
+
generate: (request) => generateTitle(ctx, module, plan, request)
|
|
210
|
+
});
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
ctx.logger.error("model-switch: another session-title provider is already registered; the configurable naming route is unavailable");
|
|
213
|
+
ctx.logger.error(cause);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, inject, name, officialTitleEntry, officialTitlePackage, selectTitleMessages, titleCadence };
|
package/lib/subagent-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { r as sessionSubagentRouteOf } from "./session-route-BCQFzqRP.js";
|
|
1
2
|
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
3
|
import OfficialSubagentRuntime from "@deepseek-ai/dsh-subagent";
|
|
3
4
|
//#region lib/types/subagent-runtime.js
|
|
@@ -20,27 +21,28 @@ function present(value) {
|
|
|
20
21
|
function spawnHasAnyRouteField(options) {
|
|
21
22
|
return present(options?.provider) || present(options?.model) || options?.reasoningEffort !== void 0;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Inject one resolved route into a spawn that names none.
|
|
26
|
+
*
|
|
27
|
+
* A spawn that already names a provider, model, or effort keeps it: the caller's
|
|
28
|
+
* explicit selection outranks any profile policy. The route itself is derived
|
|
29
|
+
* from the DELEGATING session (its provider selects a saved group, else the flat
|
|
30
|
+
* Subagent card in `fixed` mode), so two sessions on different providers route
|
|
31
|
+
* their own spawns and neither can rewrite the other's policy.
|
|
32
|
+
* @param request - the caller's spawn request.
|
|
33
|
+
* @param route - the delegating session's resolved route, or undefined for official inherit.
|
|
34
|
+
* @returns The request to hand the official runtime.
|
|
35
|
+
*/
|
|
36
|
+
function routeSubagentRequest(request, route) {
|
|
33
37
|
if (spawnHasAnyRouteField(request.agentOptions)) return request;
|
|
34
|
-
if (
|
|
35
|
-
const selected = fixedRoute(settings);
|
|
36
|
-
if (selected === void 0) return request;
|
|
38
|
+
if (route === void 0) return request;
|
|
37
39
|
return {
|
|
38
40
|
...request,
|
|
39
41
|
agentOptions: {
|
|
40
42
|
...request.agentOptions,
|
|
41
|
-
provider:
|
|
42
|
-
model:
|
|
43
|
-
...
|
|
43
|
+
provider: route.provider,
|
|
44
|
+
model: route.model,
|
|
45
|
+
...route.reasoningEffort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(route.reasoningEffort) }
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
}
|
|
@@ -170,7 +172,8 @@ var ModelSwitchSubagentRuntime = class extends OfficialSubagentRuntime {
|
|
|
170
172
|
routed(name, request) {
|
|
171
173
|
const provider = this.ctx.subagents.getProvider(name);
|
|
172
174
|
if (provider !== void 0 && provider.capabilities.agentOptions !== true) return request;
|
|
173
|
-
|
|
175
|
+
const modelSwitch = routingSurface(this.ctx);
|
|
176
|
+
return routeSubagentRequest(request, sessionSubagentRouteOf(this.ctx, request.parent.session, modelSwitch.currentSettings()));
|
|
174
177
|
}
|
|
175
178
|
start(name, request) {
|
|
176
179
|
return super.start(name, this.routed(name, request));
|
|
@@ -41,7 +41,7 @@ export interface ModelSwitchImageAdapter {
|
|
|
41
41
|
}
|
|
42
42
|
export interface ModelSwitchProviderAdapters {
|
|
43
43
|
readonly provider: string;
|
|
44
|
-
/**
|
|
44
|
+
/** Optional runtime role declared by the provider integration. */
|
|
45
45
|
readonly role?: 'llm' | 'agent';
|
|
46
46
|
readonly search?: ModelSwitchSearchAdapter;
|
|
47
47
|
readonly image?: ModelSwitchImageAdapter;
|
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
* reviewer to the session's own request route (`header.config`). This row runs
|
|
6
6
|
* the official `apply` under a scoped child context whose `llm` service is
|
|
7
7
|
* shadowed by a wrapper that rewrites only `stream()`'s provider/model — and
|
|
8
|
-
* only
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* only when the reviewing session's derived route (its provider group's
|
|
9
|
+
* reviewer pair, else the deployment-wide reviewer card) pins one. With no
|
|
10
|
+
* route pinned the wrapper passes options through by identity, so the reviewer
|
|
11
|
+
* follows the session model exactly.
|
|
12
|
+
*
|
|
13
|
+
* Although the official gate omits `sessionId` and `purpose` from its frozen
|
|
14
|
+
* `GenerateOptions`, its `tools/pre-execute` waterfall listener receives
|
|
15
|
+
* `exec.agent` (`exec.agent.session`). After running `official.apply(scope)`,
|
|
16
|
+
* this module registers a `{ prepend: true }` `tools/pre-execute` listener on
|
|
17
|
+
* the same scope — which Cordis places ahead of the official gate's listener in
|
|
18
|
+
* the waterfall — and binds `exec.agent?.session` in an `AsyncLocalStorage`
|
|
19
|
+
* scope around `next()`. Every `scope.llm.stream()` call from `classifyRisk`
|
|
20
|
+
* therefore resolves the exact session being reviewed, with zero cross-session
|
|
21
|
+
* interference when multiple sessions review tool calls concurrently.
|
|
11
22
|
*
|
|
12
23
|
* The official gate stays an opt-in capability: the row mounts it only when
|
|
13
24
|
* the profile actually started with the experimental-auto-review bundle.
|
|
@@ -16,8 +27,10 @@
|
|
|
16
27
|
*
|
|
17
28
|
* @module dsh-model-switch/auto-review
|
|
18
29
|
*/
|
|
30
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
19
31
|
import type { Context } from '@deepseek-ai/cordis';
|
|
20
32
|
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
33
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
21
34
|
/** Cordis plugin name used by loader diagnostics. */
|
|
22
35
|
export declare const name = "model-switch-auto-review";
|
|
23
36
|
/** The official gate's host services plus Model Switch's settings owner. */
|
|
@@ -52,12 +65,34 @@ export declare function autoReviewRouteOf(settings: {
|
|
|
52
65
|
* @returns A structurally compatible llm face for the scoped context.
|
|
53
66
|
*/
|
|
54
67
|
export declare function reviewLlm(real: ReviewLlm, route: () => AutoReviewRoute | undefined): ReviewLlm;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the reviewer route for one session (or deployment-wide when no session is bound).
|
|
70
|
+
*
|
|
71
|
+
* @param ctx - host context whose modelSwitch settings and sessionProjections derive the route.
|
|
72
|
+
* @param session - the session whose tool call is being reviewed, when known.
|
|
73
|
+
* @returns The derived reviewer route, or undefined to follow the session model.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveSessionAutoReviewRoute(ctx: Context, session?: Session): AutoReviewRoute | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Install the outer `tools/pre-execute` correlation listener ahead of the official gate.
|
|
78
|
+
*
|
|
79
|
+
* Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
|
|
80
|
+
* registering this listener AFTER `official.apply(scope)` places it at index 0
|
|
81
|
+
* — directly ahead of the official gate's own prepended listener — so
|
|
82
|
+
* `activeSession.run(exec.agent?.session, next)` wraps the entire official
|
|
83
|
+
* review execution.
|
|
84
|
+
*
|
|
85
|
+
* @param scope - the review scope context where the gate is mounted.
|
|
86
|
+
* @param activeSession - AsyncLocalStorage holding the session under review.
|
|
87
|
+
*/
|
|
88
|
+
export declare function bindAutoReviewSessionCorrelation(scope: Context, activeSession: AsyncLocalStorage<Session | undefined>): void;
|
|
55
89
|
/**
|
|
56
90
|
* Build the scoped context the official gate runs under.
|
|
57
91
|
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
92
|
+
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
58
93
|
* @returns A child context shadowing only the llm service.
|
|
59
94
|
*/
|
|
60
|
-
export declare function reviewScope(ctx: Context): Context;
|
|
95
|
+
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<Session | undefined>): Context;
|
|
61
96
|
/**
|
|
62
97
|
* Mount the official gate under the reviewer-route scope.
|
|
63
98
|
*
|
|
@@ -7,7 +7,7 @@ import { type ProviderGroupConfig } from '../provider-group.js';
|
|
|
7
7
|
import type { RuntimeCapabilities } from '../runtime-capabilities.js';
|
|
8
8
|
import { type CapabilitiesSnapshot } from './search-capabilities.js';
|
|
9
9
|
import type { ModelSwitchLocaleKey } from './locales.js';
|
|
10
|
-
import type { ConfigViewForm } from './derived-config-form.js';
|
|
10
|
+
import type { ConfigViewForm, ConfigViewOp } from './derived-config-form.js';
|
|
11
11
|
export interface ModelSwitchSettingsFace {
|
|
12
12
|
t: (key: ModelSwitchLocaleKey) => string;
|
|
13
13
|
hooks: {
|
|
@@ -16,12 +16,16 @@ export interface ModelSwitchSettingsFace {
|
|
|
16
16
|
imageSettings: ConfigViewForm<CapabilityRouteView>;
|
|
17
17
|
autoReviewSettings: ConfigViewForm<CapabilityRouteView>;
|
|
18
18
|
compactionSettings: ConfigViewForm<CapabilityRouteView>;
|
|
19
|
+
titleSettings: ConfigViewForm<CapabilityRouteView>;
|
|
19
20
|
switchSettings: Pick<ConfigForm<ModelSwitchSettingsView>, 'getSnapshot' | 'subscribe'>;
|
|
20
21
|
};
|
|
21
22
|
capabilities: RuntimeCapabilities;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Apply one row's changes as a SINGLE revision-fenced write, so a refused
|
|
25
|
+
* write can never leave a half-written provider/model pair behind.
|
|
26
|
+
*/
|
|
27
|
+
saveSubagent: (ops: readonly ConfigViewOp<SubagentSettingsView>[]) => Promise<void>;
|
|
28
|
+
saveCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction' | 'title', ops: readonly ConfigViewOp<CapabilityRouteView>[]) => Promise<void>;
|
|
25
29
|
/** Replace or remove (undefined) one provider's saved capability group. */
|
|
26
30
|
saveProviderGroup: (provider: string, group: ProviderGroupConfig | undefined) => Promise<void>;
|
|
27
31
|
loadCatalog: () => Promise<readonly ModelProviderGroup[]>;
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
2
|
+
/** One field-level change expressed against a view model, before source-path mapping. */
|
|
3
|
+
export interface ConfigViewOp<T> {
|
|
4
|
+
field: keyof T & string;
|
|
5
|
+
value?: unknown;
|
|
6
|
+
}
|
|
2
7
|
export interface ConfigViewForm<T> {
|
|
3
8
|
getSnapshot(): ConfigFormSnapshot<T>;
|
|
4
9
|
subscribe(listener: () => void): () => void;
|
|
5
10
|
set(field: keyof T & string, value: unknown): Promise<void>;
|
|
6
11
|
unset(field: keyof T & string): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Apply several view-field changes as ONE revision-fenced source write, so a row save
|
|
14
|
+
* either lands completely or is rejected as a whole instead of leaving a half-written pair.
|
|
15
|
+
*
|
|
16
|
+
* @param ops view fields to change; `value === undefined` unsets the source field
|
|
17
|
+
* @param expectedRevision revision the draft was seeded from; forwarded to the source fence
|
|
18
|
+
* @returns a promise that rejects with `settings-rejected` when the host refuses the write
|
|
19
|
+
*/
|
|
20
|
+
mutate(ops: readonly ConfigViewOp<T>[], expectedRevision?: number): Promise<void>;
|
|
7
21
|
}
|
|
8
22
|
export declare function deriveConfigForm<Source, View>(source: ConfigForm<Source>, project: (value: Source) => View, fields: {
|
|
9
23
|
[K in keyof View & string]: keyof Source & string;
|