dsh-model-switch 0.4.23 → 0.4.24
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 +14 -0
- package/PRODUCT.md +4 -3
- package/README.md +34 -7
- package/README.zh.md +34 -7
- package/cordis.patch.yml +15 -1
- package/docs/screenshots/settings-subagent.png +0 -0
- package/lib/auto-review.js +8 -0
- package/lib/client.js +338 -346
- package/lib/compaction.js +51 -18
- package/lib/index.js +22 -75
- package/lib/provider-group-CsZi7H29.js +94 -0
- package/lib/session-route-h9SAqajc.js +172 -0
- package/lib/session-title.js +217 -0
- package/lib/subagent-runtime.js +20 -17
- package/lib/types/auto-review.d.ts +8 -0
- package/lib/types/client/ModelSwitchSettings.d.ts +8 -3
- package/lib/types/client/derived-config-form.d.ts +14 -0
- package/lib/types/client/locales.d.ts +8 -3
- package/lib/types/client-contract.d.ts +8 -0
- package/lib/types/compaction.d.ts +25 -4
- package/lib/types/host-settings.d.ts +6 -0
- package/lib/types/provider-group.d.ts +20 -36
- 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/compaction.js
CHANGED
|
@@ -81,6 +81,24 @@ function currentCompactionRoute(ctx) {
|
|
|
81
81
|
return compactionRouteOf(settings);
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
+
* Whether the compaction capability is actually served from this context.
|
|
85
|
+
*
|
|
86
|
+
* Awaiting a plugin fiber settles even when the plugin's injections were never
|
|
87
|
+
* satisfied, so the mount is confirmed through the public service instead of by
|
|
88
|
+
* the absence of an error: an unactivated engine must never be recorded as the
|
|
89
|
+
* live engine, or the row would stop retrying while `/compact` had no service.
|
|
90
|
+
* @param ctx - this row's context.
|
|
91
|
+
* @returns true when `ctx.compaction` exposes the engine's public entry points.
|
|
92
|
+
*/
|
|
93
|
+
function engineActive(ctx) {
|
|
94
|
+
try {
|
|
95
|
+
const service = ctx.get("compaction", false);
|
|
96
|
+
return service !== void 0 && typeof service.compactIfNeeded === "function";
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
84
102
|
* Load and mount the official basic compaction engine under `config`.
|
|
85
103
|
* @param ctx - this row's context; the engine mounts unisolated so `/compact` sees it.
|
|
86
104
|
* @param config - composed engine config.
|
|
@@ -100,7 +118,6 @@ async function mountEngine(ctx, config, label) {
|
|
|
100
118
|
try {
|
|
101
119
|
fiber = ctx.plugin(loaded.default, config);
|
|
102
120
|
await fiber;
|
|
103
|
-
return fiber;
|
|
104
121
|
} catch (cause) {
|
|
105
122
|
if (fiber) try {
|
|
106
123
|
await fiber.dispose();
|
|
@@ -109,6 +126,14 @@ async function mountEngine(ctx, config, label) {
|
|
|
109
126
|
ctx.logger.error(cause);
|
|
110
127
|
return;
|
|
111
128
|
}
|
|
129
|
+
if (engineActive(ctx)) return fiber;
|
|
130
|
+
ctx.logger.error(`model-switch: the compaction engine mounted with ${label} but never activated; unloading it so the next settings change retries`);
|
|
131
|
+
try {
|
|
132
|
+
await fiber.dispose();
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
ctx.logger.error("model-switch: failed to unload the unactivated compaction engine");
|
|
135
|
+
ctx.logger.error(cause);
|
|
136
|
+
}
|
|
112
137
|
}
|
|
113
138
|
/**
|
|
114
139
|
* Dispose the live engine, keeping the ledger clean for the next mount.
|
|
@@ -129,8 +154,11 @@ async function disposeEngine(ctx, state) {
|
|
|
129
154
|
/**
|
|
130
155
|
* Reconcile one compaction mount: remount only when the composed config changed.
|
|
131
156
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
157
|
+
* A settings change that leaves the composed config identical — every session
|
|
158
|
+
* naming, subagent, or provider-group save — returns after the signature check,
|
|
159
|
+
* so the engine is never unloaded for settings it does not read. A mount that
|
|
160
|
+
* fails to activate stays unmounted with an explicit error and the next settings
|
|
161
|
+
* change retries with a fresh read.
|
|
134
162
|
* @param ctx - this row's context.
|
|
135
163
|
* @param state - the row's reconcile ledger.
|
|
136
164
|
*/
|
|
@@ -154,22 +182,25 @@ async function runReconcile(ctx, state) {
|
|
|
154
182
|
state.engine = fiber;
|
|
155
183
|
return;
|
|
156
184
|
}
|
|
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
185
|
ctx.logger.error("model-switch: compaction engine is unmounted; /compact and automatic compaction are unavailable until settings change");
|
|
164
186
|
}
|
|
165
187
|
/**
|
|
166
|
-
* Queue
|
|
188
|
+
* Queue one reconcile behind the row's serialized chain, absorbing failures.
|
|
167
189
|
* @param ctx - this row's context.
|
|
168
190
|
* @param state - the row's reconcile ledger.
|
|
191
|
+
* @param coalesce - collapse a burst of triggers into the single pass that reads the settled settings.
|
|
169
192
|
* @returns The chain tail; never rejects.
|
|
170
193
|
*/
|
|
171
|
-
function scheduleReconcile(ctx, state) {
|
|
172
|
-
|
|
194
|
+
function scheduleReconcile(ctx, state, coalesce) {
|
|
195
|
+
if (coalesce) {
|
|
196
|
+
if (state.queued) return state.chain;
|
|
197
|
+
state.queued = true;
|
|
198
|
+
}
|
|
199
|
+
state.chain = state.chain.then(async () => {
|
|
200
|
+
state.queued = false;
|
|
201
|
+
await runReconcile(ctx, state);
|
|
202
|
+
}).catch((cause) => {
|
|
203
|
+
state.queued = false;
|
|
173
204
|
ctx.logger.error("model-switch: compaction engine reconcile failed");
|
|
174
205
|
ctx.logger.error(cause);
|
|
175
206
|
});
|
|
@@ -182,9 +213,10 @@ function scheduleReconcile(ctx, state) {
|
|
|
182
213
|
* with the same package under the configurable summarization route. Host
|
|
183
214
|
* settings and the loader are read structurally so a disabled `model-switch`
|
|
184
215
|
* row leaves official compaction untouched. Reconciles run on
|
|
185
|
-
* `model-switch/settings-update` (emitted by the host
|
|
186
|
-
* ordinary saves)
|
|
187
|
-
*
|
|
216
|
+
* `model-switch/settings-update` (emitted by the host at construction, on
|
|
217
|
+
* volatile commits, and on ordinary saves) and read the settled settings, so one
|
|
218
|
+
* save costs at most one remount; the listener is global so instances of this
|
|
219
|
+
* row inside agent-preset realms still hear the host row's emission. The initial
|
|
188
220
|
* reconcile is awaited so `/compact` resolves the service as soon as this row
|
|
189
221
|
* completes.
|
|
190
222
|
* @param ctx - this row's context.
|
|
@@ -192,14 +224,15 @@ function scheduleReconcile(ctx, state) {
|
|
|
192
224
|
async function apply(ctx) {
|
|
193
225
|
const state = {
|
|
194
226
|
chain: Promise.resolve(),
|
|
227
|
+
queued: false,
|
|
195
228
|
signature: null,
|
|
196
229
|
engine: void 0,
|
|
197
230
|
missingRow: false
|
|
198
231
|
};
|
|
199
232
|
ctx.on("model-switch/settings-update", () => {
|
|
200
|
-
scheduleReconcile(ctx, state);
|
|
233
|
+
scheduleReconcile(ctx, state, true);
|
|
201
234
|
}, { global: true });
|
|
202
|
-
await scheduleReconcile(ctx, state);
|
|
235
|
+
await scheduleReconcile(ctx, state, false);
|
|
203
236
|
}
|
|
204
237
|
//#endregion
|
|
205
|
-
export { apply, compactionRouteOf, composeCompactionConfig, forwardedConfig, inject, name, officialCompactionEntry };
|
|
238
|
+
export { apply, compactionRouteOf, composeCompactionConfig, engineActive, forwardedConfig, inject, name, officialCompactionEntry };
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as normalizeProviderGroups } from "./provider-group-CsZi7H29.js";
|
|
1
2
|
import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -12,77 +13,6 @@ import { dirname, join } from "node:path";
|
|
|
12
13
|
import { renderPrompt } from "@deepseek-ai/dsh-system-prompt";
|
|
13
14
|
import { boundContextSummary, createMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
14
15
|
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
16
|
//#region lib/types/host-settings.js
|
|
87
17
|
/**
|
|
88
18
|
* Annotated rather than inferred: the nested entry schema's inferred type
|
|
@@ -103,6 +33,8 @@ const Config = z.object({
|
|
|
103
33
|
autoReviewModel: z.string().volatile(),
|
|
104
34
|
compactProvider: z.string().volatile(),
|
|
105
35
|
compactModel: z.string().volatile(),
|
|
36
|
+
titleProvider: z.string().volatile(),
|
|
37
|
+
titleModel: z.string().volatile(),
|
|
106
38
|
providerGroups: z.array(z.object({
|
|
107
39
|
provider: z.string().default(""),
|
|
108
40
|
compactProvider: z.string().default(""),
|
|
@@ -111,7 +43,9 @@ const Config = z.object({
|
|
|
111
43
|
autoReviewModel: z.string().default(""),
|
|
112
44
|
subagentProvider: z.string().default(""),
|
|
113
45
|
subagentModel: z.string().default(""),
|
|
114
|
-
subagentReasoningEffort: z.string().default("")
|
|
46
|
+
subagentReasoningEffort: z.string().default(""),
|
|
47
|
+
titleProvider: z.string().default(""),
|
|
48
|
+
titleModel: z.string().default("")
|
|
115
49
|
})).default([]).volatile()
|
|
116
50
|
});
|
|
117
51
|
function readConfig(config) {
|
|
@@ -126,6 +60,8 @@ function readConfig(config) {
|
|
|
126
60
|
const autoReviewModel = config.autoReviewModel.get();
|
|
127
61
|
const compactProvider = config.compactProvider.get();
|
|
128
62
|
const compactModel = config.compactModel.get();
|
|
63
|
+
const titleProvider = config.titleProvider.get();
|
|
64
|
+
const titleModel = config.titleModel.get();
|
|
129
65
|
const providerGroups = normalizeProviderGroups(config.providerGroups.get());
|
|
130
66
|
return {
|
|
131
67
|
subagentMode: config.subagentMode.get(),
|
|
@@ -141,6 +77,8 @@ function readConfig(config) {
|
|
|
141
77
|
...autoReviewModel === void 0 || autoReviewModel === "" ? {} : { autoReviewModel },
|
|
142
78
|
...compactProvider === void 0 || compactProvider === "" ? {} : { compactProvider },
|
|
143
79
|
...compactModel === void 0 || compactModel === "" ? {} : { compactModel },
|
|
80
|
+
...titleProvider === void 0 || titleProvider === "" ? {} : { titleProvider },
|
|
81
|
+
...titleModel === void 0 || titleModel === "" ? {} : { titleModel },
|
|
144
82
|
...Object.keys(providerGroups).length === 0 ? {} : { providerGroups }
|
|
145
83
|
};
|
|
146
84
|
}
|
|
@@ -771,13 +709,22 @@ function installSwitchCompaction(ctx, settings, roleOf = () => void 0) {
|
|
|
771
709
|
const assembled = await next();
|
|
772
710
|
if (context.agent !== void 0) {
|
|
773
711
|
assemblies.set(context.agent, assembled);
|
|
774
|
-
stopGate
|
|
775
|
-
|
|
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?.();
|
|
776
723
|
}
|
|
777
724
|
return assembled;
|
|
778
725
|
});
|
|
779
726
|
function installGate() {
|
|
780
|
-
|
|
727
|
+
return ctx.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
|
|
781
728
|
const decision = await next();
|
|
782
729
|
if (decision.kind !== "enter" || signal.aborted) return decision;
|
|
783
730
|
const enabled = settings().compactOnSwitch !== false;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region lib/types/provider-group.js
|
|
2
|
+
/**
|
|
3
|
+
* Provider configuration groups: per-provider presets for the session-naming
|
|
4
|
+
* 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 to the earlier write-through design.
|
|
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).
|
|
18
|
+
*
|
|
19
|
+
* All functions here are pure and never throw on malformed storage.
|
|
20
|
+
*/
|
|
21
|
+
const ROUTE_FIELDS = Object.freeze([
|
|
22
|
+
"compactProvider",
|
|
23
|
+
"compactModel",
|
|
24
|
+
"autoReviewProvider",
|
|
25
|
+
"autoReviewModel",
|
|
26
|
+
"subagentProvider",
|
|
27
|
+
"subagentModel",
|
|
28
|
+
"subagentReasoningEffort",
|
|
29
|
+
"titleProvider",
|
|
30
|
+
"titleModel"
|
|
31
|
+
]);
|
|
32
|
+
/** Own data property, so a literal '__proto__' provider id can never touch a prototype. */
|
|
33
|
+
function setKey(target, key, value) {
|
|
34
|
+
Object.defineProperty(target, key, {
|
|
35
|
+
value,
|
|
36
|
+
enumerable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
configurable: true
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** A non-blank string. Blank values behave exactly like an absent field. */
|
|
42
|
+
function present(value) {
|
|
43
|
+
return typeof value === "string" && value.trim() !== "";
|
|
44
|
+
}
|
|
45
|
+
/** Normalize one raw entry to its owning provider and route view; undefined for non-object junk. */
|
|
46
|
+
function normalizeEntry(value) {
|
|
47
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
48
|
+
const source = value;
|
|
49
|
+
const group = {};
|
|
50
|
+
for (const field of ROUTE_FIELDS) {
|
|
51
|
+
const item = source[field];
|
|
52
|
+
if (present(item)) group[field] = item;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
provider: present(source.provider) ? source.provider : "",
|
|
56
|
+
group
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Strip a raw stored value to well-formed groups; never throws. Both storage
|
|
61
|
+
* shapes are accepted: the stored entry array (keys live in each entry's
|
|
62
|
+
* `provider` field) and a hand-written provider → routes map (keys are the
|
|
63
|
+
* providers). All-empty entries stay: they pin a provider to every default.
|
|
64
|
+
*/
|
|
65
|
+
function normalizeProviderGroups(raw) {
|
|
66
|
+
const groups = {};
|
|
67
|
+
if (Array.isArray(raw)) {
|
|
68
|
+
for (const entry of raw) {
|
|
69
|
+
const view = normalizeEntry(entry);
|
|
70
|
+
if (view === void 0 || view.provider === "") continue;
|
|
71
|
+
setKey(groups, view.provider, view.group);
|
|
72
|
+
}
|
|
73
|
+
return groups;
|
|
74
|
+
}
|
|
75
|
+
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) {
|
|
76
|
+
if (key.trim() === "") continue;
|
|
77
|
+
const view = normalizeEntry(value);
|
|
78
|
+
if (view === void 0) continue;
|
|
79
|
+
setKey(groups, key, view.group);
|
|
80
|
+
}
|
|
81
|
+
return groups;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The saved group for a provider.
|
|
85
|
+
* @returns the normalized group, or undefined when the provider has no group —
|
|
86
|
+
* which is what leaves the live routes untouched during a switch.
|
|
87
|
+
*/
|
|
88
|
+
function groupOf(groups, provider) {
|
|
89
|
+
if (groups === void 0 || provider.trim() === "") return void 0;
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(groups, provider)) return void 0;
|
|
91
|
+
return groups[provider];
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { normalizeProviderGroups as n, groupOf as t };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { t as groupOf } from "./provider-group-CsZi7H29.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 };
|