@klarkxy/dsh-model-hub 0.1.8 → 0.2.1
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/README.md +9 -12
- package/docs/README.zh-CN.md +9 -12
- package/lib/client.inner.cjs +2093 -1521
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +2093 -1521
- package/lib/index.d.ts +1 -39
- package/lib/index.js +120 -512
- package/lib/index.js.map +1 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +12 -6
package/lib/index.js
CHANGED
|
@@ -1,261 +1,26 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
1
|
import Schema from "@deepseek-ai/schemastery";
|
|
4
2
|
import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
|
|
5
|
-
import {
|
|
6
|
-
import { detectModelFields, modelRouteOverride, normalizeModelRoute } from "@klarkxy/dsh-model-route";
|
|
7
|
-
import { collectModelSettings, notifyModelSettingsChanged } from "@klarkxy/dsh-plugin-kit/model-settings";
|
|
8
|
-
import { isSubagentSession, modelSettingsChangesIdentity } from "@klarkxy/dsh-plugin-kit/contracts";
|
|
9
|
-
import { createModelSettingsChanges } from "@klarkxy/dsh-plugin-kit/model-settings-changes";
|
|
3
|
+
import { notifyModelSettingsChanged } from "@klarkxy/dsh-plugin-kit/model-settings";
|
|
10
4
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function createProbeDiagnostics(run, onError) {
|
|
15
|
-
let dirty = true;
|
|
16
|
-
let stopped = false;
|
|
17
|
-
let timer;
|
|
18
|
-
let running;
|
|
19
|
-
return {
|
|
20
|
-
invalidate() {
|
|
21
|
-
if (!stopped) dirty = true;
|
|
22
|
-
},
|
|
23
|
-
request() {
|
|
24
|
-
if (stopped || !dirty || timer !== void 0 || running) return;
|
|
25
|
-
timer = setTimeout(() => {
|
|
26
|
-
timer = void 0;
|
|
27
|
-
if (stopped || !dirty) return;
|
|
28
|
-
dirty = false;
|
|
29
|
-
running = Promise.resolve().then(run).catch(onError).finally(() => {
|
|
30
|
-
running = void 0;
|
|
31
|
-
});
|
|
32
|
-
}, 0);
|
|
33
|
-
},
|
|
34
|
-
async dispose() {
|
|
35
|
-
stopped = true;
|
|
36
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
37
|
-
timer = void 0;
|
|
38
|
-
await running;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
//#endregion
|
|
43
|
-
//#region src/probe.ts
|
|
44
|
-
/**
|
|
45
|
-
* Pure probe-report builder for the model hub. Takes the host's
|
|
46
|
-
* `settings.describe()` output and reports every detected model-route field,
|
|
47
|
-
* plus whether this plugin's own self-test markers survived projection.
|
|
48
|
-
*/
|
|
49
|
-
const SELF_TEST_EXTRA_KEY_PATH = "selfTestMeta";
|
|
50
|
-
const SELF_TEST_ROLE_PATH = "selfTestRole";
|
|
51
|
-
function channelOf(fields, firstSegment) {
|
|
52
|
-
const hit = fields.find((field) => field.path[0] === firstSegment);
|
|
53
|
-
return hit ? hit.via : "absent";
|
|
54
|
-
}
|
|
55
|
-
function buildProbeReport(descriptors, now = /* @__PURE__ */ new Date()) {
|
|
56
|
-
const entries = listModelFields(descriptors).map(({ ns, fields }) => ({
|
|
57
|
-
ns,
|
|
58
|
-
fields
|
|
59
|
-
}));
|
|
60
|
-
let ownSchema;
|
|
61
|
-
for (const descriptor of descriptors) if (descriptor.ns.includes("model-hub")) ownSchema = descriptor.schema;
|
|
62
|
-
const own = entries.find((entry) => entry.ns.includes("model-hub"));
|
|
63
|
-
return {
|
|
64
|
-
generatedAt: now.toISOString(),
|
|
65
|
-
totalNamespaces: descriptors.length,
|
|
66
|
-
namespacesWithModelFields: entries.length,
|
|
67
|
-
entries,
|
|
68
|
-
selfTest: {
|
|
69
|
-
extraKey: own ? channelOf(own.fields, SELF_TEST_EXTRA_KEY_PATH) : "absent",
|
|
70
|
-
role: own ? channelOf(own.fields, SELF_TEST_ROLE_PATH) : "absent",
|
|
71
|
-
ownSchema
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/** The field matrix the hub page edits: every detected model-route field. */
|
|
76
|
-
function listModelFields(descriptors) {
|
|
77
|
-
const entries = [];
|
|
78
|
-
for (const descriptor of descriptors) {
|
|
79
|
-
const fields = detectModelFields(descriptor.schema, { value: descriptor.value });
|
|
80
|
-
if (!fields.length) continue;
|
|
81
|
-
entries.push({
|
|
82
|
-
ns: descriptor.ns,
|
|
83
|
-
revision: typeof descriptor.revision === "number" ? descriptor.revision : 0,
|
|
84
|
-
fields: fields.map((field) => ({
|
|
85
|
-
path: [...field.path],
|
|
86
|
-
via: field.via,
|
|
87
|
-
...field.label ? { label: field.label } : {},
|
|
88
|
-
...field.description ? { description: field.description } : {},
|
|
89
|
-
...field.supportsReasoning === false ? { supportsReasoning: false } : {},
|
|
90
|
-
...field.rootKeys ? { rootKeys: [...field.rootKeys] } : {},
|
|
91
|
-
...field.writeKeys ? { writeKeys: [...field.writeKeys] } : {},
|
|
92
|
-
...field.preserveSiblings ? { preserveSiblings: true } : {},
|
|
93
|
-
...field.marker ? { marker: field.marker } : {},
|
|
94
|
-
...field.current ? { current: field.current } : {},
|
|
95
|
-
...field.editable === false ? { editable: false } : {},
|
|
96
|
-
...field.arrayItem ? { arrayItem: true } : {}
|
|
97
|
-
}))
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
return entries;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Validate a hub-page write against a fresh detection pass: the target must be
|
|
104
|
-
* a currently detected model-route field, so the page can never write a
|
|
105
|
-
* non-model key. An empty route unsets the field back to its schema default
|
|
106
|
-
* ("follow the host default"); a concrete route is set normalized.
|
|
107
|
-
*/
|
|
108
|
-
function planFieldWrite(entries, payload) {
|
|
109
|
-
const row = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
|
|
110
|
-
const ns = typeof row?.ns === "string" ? row.ns : "";
|
|
111
|
-
const path = Array.isArray(row?.path) && row.path.every((s) => typeof s === "string" && s) ? row.path : void 0;
|
|
112
|
-
if (!ns || !path) throw new Error("Invalid field reference");
|
|
113
|
-
const entry = entries.find((item) => item.ns === ns);
|
|
114
|
-
const field = entry?.fields.find((item) => item.path.length === path.length && item.path.every((segment, index) => segment === path[index]));
|
|
115
|
-
if (!entry || !field) throw new Error("Not a detected model field");
|
|
116
|
-
if (field.editable === false) throw new Error("No collection item to edit");
|
|
117
|
-
const override = modelRouteOverride(normalizeModelRoute(row?.route));
|
|
118
|
-
if (row?.expectedRevision !== void 0 && (!Number.isSafeInteger(row.expectedRevision) || row.expectedRevision < 0)) throw new Error("Invalid revision");
|
|
119
|
-
if (typeof row?.expectedRevision !== "number") throw new Error("A valid expected revision is required; refresh and retry");
|
|
120
|
-
const expectedRevision = row.expectedRevision;
|
|
121
|
-
if (!path.length || field.preserveSiblings) return {
|
|
122
|
-
ns,
|
|
123
|
-
expectedRevision,
|
|
124
|
-
ops: (field.rootKeys ?? field.writeKeys ?? ["provider", "model"]).map((key) => {
|
|
125
|
-
const value = (override ?? (field.arrayItem ? {
|
|
126
|
-
provider: "",
|
|
127
|
-
model: ""
|
|
128
|
-
} : void 0))?.[key];
|
|
129
|
-
return value === void 0 ? {
|
|
130
|
-
op: "unset",
|
|
131
|
-
path: [...path, key]
|
|
132
|
-
} : {
|
|
133
|
-
op: "set",
|
|
134
|
-
path: [...path, key],
|
|
135
|
-
value
|
|
136
|
-
};
|
|
137
|
-
})
|
|
138
|
-
};
|
|
139
|
-
return {
|
|
140
|
-
ns,
|
|
141
|
-
expectedRevision,
|
|
142
|
-
ops: override || field.arrayItem ? [{
|
|
143
|
-
op: "set",
|
|
144
|
-
path,
|
|
145
|
-
value: override ?? {
|
|
146
|
-
provider: "",
|
|
147
|
-
model: ""
|
|
148
|
-
}
|
|
149
|
-
}] : [{
|
|
150
|
-
op: "unset",
|
|
151
|
-
path
|
|
152
|
-
}]
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
//#endregion
|
|
156
|
-
//#region src/owners.ts
|
|
157
|
-
/** Owner contributions supersede the same native namespace, never its storage. */
|
|
158
|
-
function createHubSettings(events, settings, ownNamespace = "@klarkxy/dsh-model-hub") {
|
|
159
|
-
const readOwner = async (owner, signal) => {
|
|
160
|
-
try {
|
|
161
|
-
const snapshot = await owner.read(signal);
|
|
162
|
-
if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0) throw new Error("Invalid owner revision");
|
|
163
|
-
return {
|
|
164
|
-
ns: owner.ns,
|
|
165
|
-
title: owner.title,
|
|
166
|
-
source: "owner",
|
|
167
|
-
ownerId: owner.instanceId,
|
|
168
|
-
revision: snapshot.revision,
|
|
169
|
-
fields: snapshot.fields.map((field) => ({
|
|
170
|
-
...field,
|
|
171
|
-
path: [...field.path],
|
|
172
|
-
via: "contribution"
|
|
173
|
-
})),
|
|
174
|
-
...snapshot.catalog !== void 0 ? { catalog: snapshot.catalog } : {},
|
|
175
|
-
...snapshot.catalogError ? { catalogError: snapshot.catalogError } : {}
|
|
176
|
-
};
|
|
177
|
-
} catch (error) {
|
|
178
|
-
signal.throwIfAborted();
|
|
179
|
-
return {
|
|
180
|
-
ns: owner.ns,
|
|
181
|
-
title: owner.title,
|
|
182
|
-
source: "owner",
|
|
183
|
-
ownerId: owner.instanceId,
|
|
184
|
-
revision: 0,
|
|
185
|
-
fields: [],
|
|
186
|
-
error: error instanceof Error ? error.message : String(error)
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
const list = async (signal) => {
|
|
191
|
-
signal.throwIfAborted();
|
|
192
|
-
const owners = await collectModelSettings(events);
|
|
193
|
-
const namespaces = new Set(owners.map((owner) => owner.ns));
|
|
194
|
-
const native = listModelFields(settings.describe({ redactSecrets: true })).filter((entry) => !namespaces.has(entry.ns)).map((entry) => ({
|
|
195
|
-
...entry,
|
|
196
|
-
fields: entry.fields.filter((field) => !(entry.ns === ownNamespace && ["selfTestMeta", "selfTestRole"].includes(field.path[0] ?? "")))
|
|
197
|
-
})).filter((entry) => entry.fields.length > 0);
|
|
198
|
-
const entries = await Promise.all(owners.map((owner) => readOwner(owner, signal)));
|
|
199
|
-
signal.throwIfAborted();
|
|
200
|
-
return [...native, ...entries].sort((a, b) => a.ns.localeCompare(b.ns));
|
|
201
|
-
};
|
|
202
|
-
const set = async (payload, signal) => {
|
|
203
|
-
signal.throwIfAborted();
|
|
204
|
-
const row = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : void 0;
|
|
205
|
-
if (!row || typeof row.ns !== "string" || !Array.isArray(row.path) || !row.path.every((segment) => typeof segment === "string" && segment)) throw new Error("Invalid field reference");
|
|
206
|
-
if (!Number.isSafeInteger(row.expectedRevision) || row.expectedRevision < 0) throw new Error("A valid expected revision is required; refresh and retry");
|
|
207
|
-
const input = row.route && typeof row.route === "object" && !Array.isArray(row.route) ? row.route : void 0;
|
|
208
|
-
if (!input || [
|
|
209
|
-
"provider",
|
|
210
|
-
"model",
|
|
211
|
-
"reasoningEffort"
|
|
212
|
-
].some((key) => input[key] !== void 0 && typeof input[key] !== "string")) throw new Error("Invalid model route");
|
|
213
|
-
const normalized = normalizeModelRoute(input);
|
|
214
|
-
if ((Boolean(String(input.provider ?? "").trim()) || Boolean(String(input.model ?? "").trim())) && (!normalized.provider || !normalized.model)) throw new Error("Invalid model route");
|
|
215
|
-
const owner = (await collectModelSettings(events)).find((item) => item.ns === row.ns);
|
|
216
|
-
if (owner) {
|
|
217
|
-
if (row.ownerId !== owner.instanceId) throw new Error("Model settings owner changed; refresh and retry");
|
|
218
|
-
const entry = await readOwner(owner, signal);
|
|
219
|
-
if (entry.error) throw new Error(typeof entry.error === "string" ? entry.error : entry.error.en);
|
|
220
|
-
if (entry.revision !== row.expectedRevision) throw new Error("Settings changed; refresh and retry");
|
|
221
|
-
const field = entry.fields.find((field) => JSON.stringify(field.path) === JSON.stringify(row.path));
|
|
222
|
-
if (!field || field.editable === false) throw new Error("Not an editable model field");
|
|
223
|
-
const route = normalized;
|
|
224
|
-
if (field.allowFollow === false && (!route.provider || !route.model)) throw new Error("This model setting requires a concrete model");
|
|
225
|
-
await owner.write(field.path, route, row.expectedRevision, signal);
|
|
226
|
-
} else {
|
|
227
|
-
if (row.ownerId !== void 0) throw new Error("Model settings owner unloaded; refresh and retry");
|
|
228
|
-
if (row.ns === ownNamespace) throw new Error("Self-test fields are diagnostic only");
|
|
229
|
-
const plan = planFieldWrite(listModelFields(settings.describe({ redactSecrets: true })), payload);
|
|
230
|
-
await settings.mutate(plan.ns, plan.ops, plan.expectedRevision);
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
committed: true,
|
|
234
|
-
ns: row.ns
|
|
235
|
-
};
|
|
236
|
-
};
|
|
237
|
-
return {
|
|
238
|
-
list,
|
|
239
|
-
set
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
//#endregion
|
|
243
|
-
//#region src/changes.ts
|
|
244
|
-
/** Model Hub forwards every settings namespace; feature plugins filter their own. */
|
|
245
|
-
function registerHubModelSettingsChanges(ctx) {
|
|
246
|
-
createModelSettingsChanges(ctx, modelSettingsChangesIdentity("@klarkxy/dsh-model-hub"));
|
|
247
|
-
}
|
|
248
|
-
//#endregion
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { modelSettingsChangesIdentity } from "@klarkxy/dsh-plugin-kit/contracts";
|
|
7
|
+
import { createModelSettingsChanges } from "@klarkxy/dsh-plugin-kit/model-settings-changes";
|
|
249
8
|
//#region src/cast.ts
|
|
250
9
|
/**
|
|
251
10
|
* dsh-model-hub — subagent cast (host half).
|
|
252
11
|
*
|
|
253
12
|
* Edits the host-native `subagent-model-selection-settings` namespace
|
|
254
13
|
* (`enabled` + `allowedModels` routes) through the official settings CAS
|
|
255
|
-
* path, and keeps
|
|
256
|
-
* `<profile>/data/model-hub/cast.json
|
|
257
|
-
* the
|
|
258
|
-
*
|
|
14
|
+
* path, and keeps the per-route user preferences in
|
|
15
|
+
* `<profile>/data/model-hub/cast.json`: the free-text "task tendency" note
|
|
16
|
+
* and the reasoning effort.
|
|
17
|
+
*
|
|
18
|
+
* The native AllowedModelRoute schema carries provider and model only — the
|
|
19
|
+
* host strips a reasoning effort from the settings value — so the effort is a
|
|
20
|
+
* plugin-side preference keyed by provider/model and saved with the file's own
|
|
21
|
+
* CAS. Tendencies and efforts are advisory hints for the system-prompt
|
|
22
|
+
* injection in index.ts; the main agent still decides which model a delegated
|
|
23
|
+
* subagent receives.
|
|
259
24
|
*/
|
|
260
25
|
/** Native namespace owning the subagent model-selection preference. */
|
|
261
26
|
const CAST_NAMESPACE = "subagent-model-selection-settings";
|
|
@@ -264,12 +29,13 @@ const CAST_TENDENCIES_NAMESPACE = "@klarkxy/dsh-model-hub/cast-tendencies";
|
|
|
264
29
|
/** Tendency ids are limited like host route ids; notes stay short prompt lines. */
|
|
265
30
|
const CAST_LIMITS = {
|
|
266
31
|
id: 250,
|
|
267
|
-
tendency: 500
|
|
32
|
+
tendency: 500,
|
|
33
|
+
effort: 80
|
|
268
34
|
};
|
|
269
35
|
function isRecord(value) {
|
|
270
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
271
37
|
}
|
|
272
|
-
/**
|
|
38
|
+
/** Store key for one provider/model pair; it keys tendencies and efforts. */
|
|
273
39
|
function tendencyKey(provider, model) {
|
|
274
40
|
return JSON.stringify([provider, model]);
|
|
275
41
|
}
|
|
@@ -279,13 +45,12 @@ function parseAllowedModels(value) {
|
|
|
279
45
|
const routes = [];
|
|
280
46
|
list.forEach((entry, index) => {
|
|
281
47
|
if (!isRecord(entry)) return;
|
|
282
|
-
const { provider, model
|
|
48
|
+
const { provider, model } = entry;
|
|
283
49
|
if (typeof provider !== "string" || !provider || typeof model !== "string" || !model) return;
|
|
284
50
|
routes.push({
|
|
285
51
|
index,
|
|
286
52
|
provider,
|
|
287
|
-
model
|
|
288
|
-
...typeof reasoningEffort === "string" && reasoningEffort ? { reasoningEffort } : {}
|
|
53
|
+
model
|
|
289
54
|
});
|
|
290
55
|
});
|
|
291
56
|
return {
|
|
@@ -293,22 +58,17 @@ function parseAllowedModels(value) {
|
|
|
293
58
|
length: list.length
|
|
294
59
|
};
|
|
295
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Normalize a native route write. The native schema owns provider and model
|
|
63
|
+
* only: a reasoning effort is a plugin-side preference and never travels
|
|
64
|
+
* through the settings CAS.
|
|
65
|
+
*/
|
|
296
66
|
function normalizeRouteInput(input) {
|
|
297
67
|
const row = isRecord(input) ? input : {};
|
|
298
68
|
const provider = typeof row.provider === "string" ? row.provider.trim() : "";
|
|
299
69
|
const model = typeof row.model === "string" ? row.model.trim() : "";
|
|
300
70
|
if (!provider || !model) throw new Error("A route requires non-empty provider and model ids");
|
|
301
71
|
if (provider.length > CAST_LIMITS.id || model.length > CAST_LIMITS.id) throw new Error(`Route provider and model ids are limited to ${CAST_LIMITS.id} characters`);
|
|
302
|
-
if (row.reasoningEffort !== void 0) {
|
|
303
|
-
const effort = typeof row.reasoningEffort === "string" ? row.reasoningEffort.trim() : "";
|
|
304
|
-
if (!effort) throw new Error("A route reasoning effort must be a non-empty string");
|
|
305
|
-
if (effort.length > CAST_LIMITS.id) throw new Error(`Route reasoning effort is limited to ${CAST_LIMITS.id} characters`);
|
|
306
|
-
return {
|
|
307
|
-
provider,
|
|
308
|
-
model,
|
|
309
|
-
reasoningEffort: effort
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
72
|
return {
|
|
313
73
|
provider,
|
|
314
74
|
model
|
|
@@ -324,12 +84,13 @@ function validateIndex(value) {
|
|
|
324
84
|
}
|
|
325
85
|
/**
|
|
326
86
|
* Render the injected guidance. Pure: callers resolve enabled state and the
|
|
327
|
-
*
|
|
87
|
+
* preference-merged routes. Returns '' when the injection should not appear —
|
|
328
88
|
* the host prompt renderer drops empty sections.
|
|
329
89
|
*/
|
|
330
90
|
function buildCastPrompt(enabled, routes) {
|
|
331
91
|
if (!enabled || !routes.length) return "";
|
|
332
92
|
const annotated = routes.filter((route) => typeof route.tendency === "string" && route.tendency.trim());
|
|
93
|
+
const withEffort = routes.filter((route) => typeof route.reasoningEffort === "string" && route.reasoningEffort.trim());
|
|
333
94
|
const guidance = [
|
|
334
95
|
"Subagent model selection:",
|
|
335
96
|
"When delegating an independent task, actively choose a suitable model instead of silently inheriting your own. If list_subagent_models and a delegation tool with provider/model fields are available, inspect the available routes before the first model-selectable delegation in this task, reuse those results for later delegations, and supply provider and model together explicitly.",
|
|
@@ -341,17 +102,27 @@ function buildCastPrompt(enabled, routes) {
|
|
|
341
102
|
provider: route.provider,
|
|
342
103
|
model: route.model
|
|
343
104
|
})}${route.reasoningEffort ? ` (reasoning effort: ${route.reasoningEffort})` : ""}: ${route.tendency}`));
|
|
105
|
+
if (withEffort.length) guidance.push("Configured reasoning effort (advisory reference configuration, applied when a subagent starts; it does not change subagents that are already running): pass reasoning_effort only when the selected route supports that value and omit it otherwise. When a route's provider/model changes, an effort the new route does not support is dropped.", ...withEffort.map((route) => `- ${JSON.stringify({
|
|
106
|
+
provider: route.provider,
|
|
107
|
+
model: route.model
|
|
108
|
+
})}: reasoning effort "${route.reasoningEffort}"`));
|
|
344
109
|
return guidance.join("\n");
|
|
345
110
|
}
|
|
346
111
|
function createCast(deps) {
|
|
347
112
|
const castFile = () => {
|
|
348
|
-
if (!deps.profileDir) throw new Error("No profile directory; cast
|
|
113
|
+
if (!deps.profileDir) throw new Error("No profile directory; cast preferences cannot be stored");
|
|
349
114
|
return join(deps.profileDir, "data", "model-hub", "cast.json");
|
|
350
115
|
};
|
|
351
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Always read the durable revision: another tab or plugin incarnation may
|
|
118
|
+
* have committed since the last snapshot. Failed reads never become a cache.
|
|
119
|
+
* v1 and v2 stores hold tendencies only; v3 adds the effort map.
|
|
120
|
+
*/
|
|
121
|
+
const readPreferences = () => {
|
|
352
122
|
if (!deps.profileDir) return {
|
|
353
123
|
revision: 0,
|
|
354
|
-
tendencies: {}
|
|
124
|
+
tendencies: {},
|
|
125
|
+
efforts: {}
|
|
355
126
|
};
|
|
356
127
|
let raw;
|
|
357
128
|
try {
|
|
@@ -359,42 +130,48 @@ function createCast(deps) {
|
|
|
359
130
|
} catch (error) {
|
|
360
131
|
if (error.code === "ENOENT") return {
|
|
361
132
|
revision: 0,
|
|
362
|
-
tendencies: {}
|
|
133
|
+
tendencies: {},
|
|
134
|
+
efforts: {}
|
|
363
135
|
};
|
|
364
|
-
throw new Error("Cast
|
|
136
|
+
throw new Error("Cast preference store cannot be read; restore access to cast.json and refresh", { cause: error });
|
|
365
137
|
}
|
|
366
138
|
let parsed;
|
|
367
139
|
try {
|
|
368
140
|
parsed = JSON.parse(raw);
|
|
369
141
|
} catch (error) {
|
|
370
|
-
throw new Error("Cast
|
|
142
|
+
throw new Error("Cast preference store is damaged; repair cast.json and refresh", { cause: error });
|
|
371
143
|
}
|
|
372
|
-
|
|
144
|
+
const version = isRecord(parsed) ? parsed.schemaVersion : void 0;
|
|
145
|
+
if (version !== 1 && version !== 2 && version !== 3) throw new Error("Cast preference store version is unsupported; use a compatible plugin before saving");
|
|
373
146
|
try {
|
|
374
|
-
if (!isRecord(parsed) || !isRecord(parsed.tendencies)) throw new Error("Invalid
|
|
375
|
-
if (
|
|
376
|
-
|
|
147
|
+
if (!isRecord(parsed) || !isRecord(parsed.tendencies)) throw new Error("Invalid preference store schema");
|
|
148
|
+
if (version !== 1 && (!Number.isSafeInteger(parsed.revision) || parsed.revision < 0)) throw new Error("Invalid preference store revision");
|
|
149
|
+
const efforts = version === 3 ? parsed.efforts : {};
|
|
150
|
+
if (!isRecord(efforts)) throw new Error("Invalid preference store schema");
|
|
151
|
+
for (const map of [parsed.tendencies, efforts]) for (const [key, value] of Object.entries(map)) {
|
|
377
152
|
const ids = JSON.parse(key);
|
|
378
|
-
if (!Array.isArray(ids) || ids.length !== 2 || !ids.every((id) => typeof id === "string" && id.trim()) || typeof value !== "string") throw new Error("Invalid
|
|
153
|
+
if (!Array.isArray(ids) || ids.length !== 2 || !ids.every((id) => typeof id === "string" && id.trim()) || typeof value !== "string") throw new Error("Invalid preference store entry");
|
|
379
154
|
}
|
|
380
155
|
return {
|
|
381
|
-
revision:
|
|
382
|
-
tendencies: parsed.tendencies
|
|
156
|
+
revision: version === 1 ? 0 : parsed.revision,
|
|
157
|
+
tendencies: parsed.tendencies,
|
|
158
|
+
efforts
|
|
383
159
|
};
|
|
384
160
|
} catch (error) {
|
|
385
|
-
throw new Error("Cast
|
|
161
|
+
throw new Error("Cast preference store schema is invalid; repair cast.json and refresh", { cause: error });
|
|
386
162
|
}
|
|
387
163
|
};
|
|
388
164
|
/** Atomic store write: temp file plus rename, so a crash never halves JSON. */
|
|
389
|
-
const
|
|
165
|
+
const writePreferences = (next, revision) => {
|
|
390
166
|
const file = castFile();
|
|
391
167
|
mkdirSync(dirname(file), { recursive: true });
|
|
392
168
|
const temp = `${file}.${process.pid}.tmp`;
|
|
393
169
|
try {
|
|
394
170
|
writeFileSync(temp, JSON.stringify({
|
|
395
|
-
schemaVersion:
|
|
171
|
+
schemaVersion: 3,
|
|
396
172
|
revision,
|
|
397
|
-
tendencies: next
|
|
173
|
+
tendencies: next.tendencies,
|
|
174
|
+
efforts: next.efforts
|
|
398
175
|
}, null, 2));
|
|
399
176
|
renameSync(temp, file);
|
|
400
177
|
} catch (error) {
|
|
@@ -416,24 +193,28 @@ function createCast(deps) {
|
|
|
416
193
|
length: parsed.length
|
|
417
194
|
};
|
|
418
195
|
};
|
|
419
|
-
|
|
196
|
+
/** Merge the stored preferences into the native routes. */
|
|
197
|
+
const mergePreferences = (routes, stored) => {
|
|
420
198
|
return routes.map((route) => {
|
|
421
|
-
const
|
|
422
|
-
|
|
199
|
+
const key = tendencyKey(route.provider, route.model);
|
|
200
|
+
const tendency = stored.tendencies[key];
|
|
201
|
+
const effort = stored.efforts[key];
|
|
202
|
+
return {
|
|
423
203
|
...route,
|
|
424
|
-
tendency
|
|
425
|
-
|
|
204
|
+
...tendency ? { tendency } : {},
|
|
205
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
206
|
+
};
|
|
426
207
|
});
|
|
427
208
|
};
|
|
428
209
|
const getCast = () => {
|
|
429
210
|
const raw = readRaw();
|
|
430
211
|
try {
|
|
431
|
-
const stored =
|
|
212
|
+
const stored = readPreferences();
|
|
432
213
|
return {
|
|
433
214
|
enabled: raw.enabled,
|
|
434
215
|
revision: raw.revision,
|
|
435
216
|
tendencyRevision: stored.revision,
|
|
436
|
-
routes:
|
|
217
|
+
routes: mergePreferences(raw.routes, stored)
|
|
437
218
|
};
|
|
438
219
|
} catch (error) {
|
|
439
220
|
return {
|
|
@@ -507,24 +288,41 @@ function createCast(deps) {
|
|
|
507
288
|
return ops;
|
|
508
289
|
});
|
|
509
290
|
};
|
|
510
|
-
/**
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (!
|
|
291
|
+
/**
|
|
292
|
+
* Save one route's plugin-side preferences with the file's own CAS. The
|
|
293
|
+
* returned state is the committed one, so the page never waits for a later
|
|
294
|
+
* refresh that could still carry the previous value.
|
|
295
|
+
*/
|
|
296
|
+
const setRoutePreferences = (provider, model, preferences, expectedRevision, signal) => {
|
|
297
|
+
if (typeof provider !== "string" || !provider.trim() || typeof model !== "string" || !model.trim()) throw new Error("A route preference requires non-empty provider and model ids");
|
|
298
|
+
if (!deps.profileDir) throw new Error("No profile directory; cast preferences cannot be stored");
|
|
517
299
|
const revision = validateRevision(expectedRevision);
|
|
518
|
-
const stored =
|
|
519
|
-
if (stored.revision !== revision) throw new Error("Cast
|
|
520
|
-
if (revision === Number.MAX_SAFE_INTEGER) throw new Error("Cast
|
|
521
|
-
const
|
|
522
|
-
const next = {
|
|
300
|
+
const stored = readPreferences();
|
|
301
|
+
if (stored.revision !== revision) throw new Error("Cast preferences changed; refresh and retry");
|
|
302
|
+
if (revision === Number.MAX_SAFE_INTEGER) throw new Error("Cast preference revision limit reached");
|
|
303
|
+
const patch = isRecord(preferences) ? preferences : {};
|
|
304
|
+
const next = {
|
|
305
|
+
tendencies: { ...stored.tendencies },
|
|
306
|
+
efforts: { ...stored.efforts }
|
|
307
|
+
};
|
|
523
308
|
const key = tendencyKey(provider.trim(), model.trim());
|
|
524
|
-
if (
|
|
525
|
-
|
|
309
|
+
if ("tendency" in patch) {
|
|
310
|
+
if (typeof patch.tendency !== "string") throw new Error("A tendency must be a string");
|
|
311
|
+
const text = patch.tendency.trim();
|
|
312
|
+
if (text.length > CAST_LIMITS.tendency) throw new Error(`A tendency is limited to ${CAST_LIMITS.tendency} characters`);
|
|
313
|
+
if (text) next.tendencies[key] = text;
|
|
314
|
+
else Reflect.deleteProperty(next.tendencies, key);
|
|
315
|
+
}
|
|
316
|
+
if ("reasoningEffort" in patch) {
|
|
317
|
+
if (typeof patch.reasoningEffort !== "string") throw new Error("A reasoning effort must be a string");
|
|
318
|
+
const effort = patch.reasoningEffort.trim();
|
|
319
|
+
if (effort.length > CAST_LIMITS.effort) throw new Error(`A reasoning effort is limited to ${CAST_LIMITS.effort} characters`);
|
|
320
|
+
if (effort) next.efforts[key] = effort;
|
|
321
|
+
else Reflect.deleteProperty(next.efforts, key);
|
|
322
|
+
}
|
|
323
|
+
const raw = readRaw();
|
|
526
324
|
signal?.throwIfAborted();
|
|
527
|
-
|
|
325
|
+
writePreferences(next, revision + 1);
|
|
528
326
|
try {
|
|
529
327
|
deps.notifyTendenciesChanged?.(revision + 1);
|
|
530
328
|
} catch {}
|
|
@@ -532,9 +330,14 @@ function createCast(deps) {
|
|
|
532
330
|
enabled: raw.enabled,
|
|
533
331
|
revision: raw.revision,
|
|
534
332
|
tendencyRevision: revision + 1,
|
|
535
|
-
routes:
|
|
333
|
+
routes: mergePreferences(raw.routes, next)
|
|
536
334
|
};
|
|
537
335
|
};
|
|
336
|
+
/** Synchronous read/CAS/rename in one host turn; no native settings write. */
|
|
337
|
+
const setTendency = (provider, model, tendency, expectedRevision, signal) => {
|
|
338
|
+
if (typeof tendency !== "string") throw new Error("A tendency must be a string");
|
|
339
|
+
return setRoutePreferences(provider, model, { tendency }, expectedRevision, signal);
|
|
340
|
+
};
|
|
538
341
|
/**
|
|
539
342
|
* Section text for the system-prompt injection. The native
|
|
540
343
|
* `subagentModelSelection` service is authoritative when present; a missing
|
|
@@ -562,9 +365,9 @@ function createCast(deps) {
|
|
|
562
365
|
return "";
|
|
563
366
|
}
|
|
564
367
|
try {
|
|
565
|
-
return buildCastPrompt(snapshot.enabled,
|
|
368
|
+
return buildCastPrompt(snapshot.enabled, mergePreferences(snapshot.routes, readPreferences()));
|
|
566
369
|
} catch (error) {
|
|
567
|
-
deps.logger?.warn("model-hub: cast
|
|
370
|
+
deps.logger?.warn("model-hub: cast preferences unavailable: %s", error.message);
|
|
568
371
|
return buildCastPrompt(snapshot.enabled, snapshot.routes);
|
|
569
372
|
}
|
|
570
373
|
};
|
|
@@ -575,145 +378,18 @@ function createCast(deps) {
|
|
|
575
378
|
setRoute,
|
|
576
379
|
removeRoute,
|
|
577
380
|
setTendency,
|
|
381
|
+
setRoutePreferences,
|
|
578
382
|
promptText
|
|
579
383
|
};
|
|
580
384
|
}
|
|
581
385
|
//#endregion
|
|
582
|
-
//#region src/
|
|
583
|
-
/**
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
* Read-only live view answering "which model and reasoning effort is each
|
|
587
|
-
* running subagent actually using". `subagent/start` and `subagent/end`
|
|
588
|
-
* maintain the live set; each child's effective route is read from its
|
|
589
|
-
* session epoch header (`session.requestHeader()`) and kept current through
|
|
590
|
-
* the session's own `request/header` events (reason `change` covers mid-run
|
|
591
|
-
* model switches). Children already running when this plugin (re)loads are
|
|
592
|
-
* recovered from the live agent registry via `isSubagentSession`.
|
|
593
|
-
*
|
|
594
|
-
* The effective route is session data recorded by the host; this tracker
|
|
595
|
-
* never writes to sessions or settings. Display-only invalidations reuse the
|
|
596
|
-
* model-settings change channel with RUNNING_NAMESPACE so open pages refetch.
|
|
597
|
-
*/
|
|
598
|
-
/** Invalidation namespace for the running-subagent view; carries no settings. */
|
|
599
|
-
const RUNNING_NAMESPACE = "@klarkxy/dsh-model-hub/running-subagents";
|
|
600
|
-
function text(value) {
|
|
601
|
-
return typeof value === "string" ? value.trim() : "";
|
|
602
|
-
}
|
|
603
|
-
/** Normalize one epoch header into the display route plus the adapter-default flag. */
|
|
604
|
-
function routeFromHeader(header) {
|
|
605
|
-
const config = header?.config;
|
|
606
|
-
const provider = text(config?.provider);
|
|
607
|
-
const model = text(config?.model);
|
|
608
|
-
if (!provider || !model) return {};
|
|
609
|
-
const effort = text(config?.reasoningEffort);
|
|
610
|
-
return {
|
|
611
|
-
route: effort ? {
|
|
612
|
-
provider,
|
|
613
|
-
model,
|
|
614
|
-
reasoningEffort: effort
|
|
615
|
-
} : {
|
|
616
|
-
provider,
|
|
617
|
-
model
|
|
618
|
-
},
|
|
619
|
-
...effort && header?.adapterDefaults?.reasoningEffort === true ? { effortIsAdapterDefault: true } : {}
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
function sameRoute(left, right) {
|
|
623
|
-
return left.route?.provider === right.route?.provider && left.route?.model === right.route?.model && left.route?.reasoningEffort === right.route?.reasoningEffort && left.effortIsAdapterDefault === right.effortIsAdapterDefault;
|
|
624
|
-
}
|
|
625
|
-
function createRunningTracker(deps) {
|
|
626
|
-
const entries = /* @__PURE__ */ new Map();
|
|
627
|
-
let revision = 0;
|
|
628
|
-
const bump = () => deps.notify(++revision);
|
|
629
|
-
const add = (id, info) => {
|
|
630
|
-
const session = (deps.agents?.get(id))?.session;
|
|
631
|
-
const next = routeFromHeader(session?.requestHeader?.());
|
|
632
|
-
const existing = entries.get(id);
|
|
633
|
-
if (existing) {
|
|
634
|
-
const providerName = info?.provider ?? existing.providerName;
|
|
635
|
-
const local = info?.local ?? existing.local;
|
|
636
|
-
const changed = existing.providerName !== providerName || existing.local !== local || !sameRoute(existing, next);
|
|
637
|
-
existing.providerName = providerName;
|
|
638
|
-
existing.local = local;
|
|
639
|
-
existing.route = next.route;
|
|
640
|
-
existing.effortIsAdapterDefault = next.effortIsAdapterDefault;
|
|
641
|
-
if (changed) bump();
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
entries.set(id, {
|
|
645
|
-
id,
|
|
646
|
-
local: info?.local !== false,
|
|
647
|
-
...info?.provider ? { providerName: info.provider } : {},
|
|
648
|
-
...typeof session?.header?.parentSession === "string" && session.header.parentSession ? { parentSessionId: session.header.parentSession } : {},
|
|
649
|
-
...typeof session?.header?.createdAt === "number" ? { startedAt: session.header.createdAt } : {},
|
|
650
|
-
route: next.route,
|
|
651
|
-
effortIsAdapterDefault: next.effortIsAdapterDefault
|
|
652
|
-
});
|
|
653
|
-
bump();
|
|
654
|
-
};
|
|
655
|
-
const remove = (id) => {
|
|
656
|
-
if (entries.delete(id)) bump();
|
|
657
|
-
};
|
|
658
|
-
try {
|
|
659
|
-
for (const agent of deps.agents?.list() ?? []) if (agent?.id && isSubagentSession(agent.session)) add(agent.id);
|
|
660
|
-
} catch {}
|
|
661
|
-
deps.events.on("subagent/start", (info) => {
|
|
662
|
-
if (typeof info?.id === "string" && info.id) add(info.id, info);
|
|
663
|
-
});
|
|
664
|
-
deps.events.on("subagent/end", (info) => {
|
|
665
|
-
if (typeof info?.id === "string") remove(info.id);
|
|
666
|
-
});
|
|
667
|
-
deps.events.on("agent/disposed", (payload) => {
|
|
668
|
-
if (typeof payload?.agent?.id === "string") remove(payload.agent.id);
|
|
669
|
-
});
|
|
670
|
-
deps.events.on("session/event", (session, event) => {
|
|
671
|
-
if (event?.type !== "request/header") return;
|
|
672
|
-
const id = typeof session?.id === "string" ? session.id : void 0;
|
|
673
|
-
const entry = id ? entries.get(id) : void 0;
|
|
674
|
-
if (!entry) return;
|
|
675
|
-
const next = routeFromHeader(event.data?.header);
|
|
676
|
-
if (sameRoute(entry, next)) return;
|
|
677
|
-
entry.route = next.route;
|
|
678
|
-
entry.effortIsAdapterDefault = next.effortIsAdapterDefault;
|
|
679
|
-
bump();
|
|
680
|
-
});
|
|
681
|
-
return { async list(signal) {
|
|
682
|
-
const pendingParents = /* @__PURE__ */ new Set();
|
|
683
|
-
for (const entry of entries.values()) if (entry.label === void 0 && entry.parentSessionId) pendingParents.add(entry.parentSessionId);
|
|
684
|
-
if (deps.subagents && pendingParents.size) await Promise.allSettled([...pendingParents].map(async (parent) => {
|
|
685
|
-
const children = await deps.subagents.listChildren(parent, signal);
|
|
686
|
-
const labels = new Map(children.map((child) => [child.id, child.label]));
|
|
687
|
-
for (const entry of entries.values()) {
|
|
688
|
-
if (entry.parentSessionId !== parent || entry.label !== void 0) continue;
|
|
689
|
-
const label = labels.get(entry.id);
|
|
690
|
-
if (typeof label === "string" && label.trim()) entry.label = label.trim();
|
|
691
|
-
}
|
|
692
|
-
}));
|
|
693
|
-
const snapshot = [...entries.values()].map((entry) => ({
|
|
694
|
-
...entry,
|
|
695
|
-
route: entry.route ? { ...entry.route } : void 0
|
|
696
|
-
})).sort((a, b) => (a.startedAt ?? 0) - (b.startedAt ?? 0) || a.id.localeCompare(b.id));
|
|
697
|
-
return {
|
|
698
|
-
revision,
|
|
699
|
-
entries: snapshot
|
|
700
|
-
};
|
|
701
|
-
} };
|
|
386
|
+
//#region src/changes.ts
|
|
387
|
+
/** Model Hub forwards every settings namespace; feature plugins filter their own. */
|
|
388
|
+
function registerHubModelSettingsChanges(ctx) {
|
|
389
|
+
createModelSettingsChanges(ctx, modelSettingsChangesIdentity("@klarkxy/dsh-model-hub"));
|
|
702
390
|
}
|
|
703
391
|
//#endregion
|
|
704
392
|
//#region src/index.ts
|
|
705
|
-
/**
|
|
706
|
-
* dsh-model-hub — cross-plugin model settings hub.
|
|
707
|
-
*
|
|
708
|
-
* Host half: reads every plugin's settings descriptor through the host
|
|
709
|
-
* `settings` service, runs model-route field detection, and serves the field
|
|
710
|
-
* matrix to the browser half over an authenticated RPC channel. Writes go
|
|
711
|
-
* through `settings.mutate` — the official settings page's own path — with
|
|
712
|
-
* the descriptor revision as CAS. The boot probe report is kept: it is logged
|
|
713
|
-
* and written to `<profile>/data/model-hub/probe-report.json`. The subagent
|
|
714
|
-
* cast (cast.ts) edits the native subagent model-selection settings the same
|
|
715
|
-
* way and injects per-route task tendencies into the system prompt.
|
|
716
|
-
*/
|
|
717
393
|
const name = "@klarkxy/dsh-model-hub";
|
|
718
394
|
const inject = [
|
|
719
395
|
"settings",
|
|
@@ -722,94 +398,26 @@ const inject = [
|
|
|
722
398
|
"systemPrompt"
|
|
723
399
|
];
|
|
724
400
|
const CHANNEL = "/dsh-model-hub";
|
|
725
|
-
const Config = Schema.object({
|
|
726
|
-
selfTestMeta: modelRouteSchemastery({
|
|
727
|
-
purpose: "self-test-meta",
|
|
728
|
-
label: {
|
|
729
|
-
zh: "自检字段(自定义键通道)",
|
|
730
|
-
en: "Self-test field (custom key)"
|
|
731
|
-
}
|
|
732
|
-
}).volatile(),
|
|
733
|
-
selfTestRole: modelRouteRoleSchemastery({
|
|
734
|
-
purpose: "self-test-role",
|
|
735
|
-
label: {
|
|
736
|
-
zh: "自检字段(role 通道)",
|
|
737
|
-
en: "Self-test field (role)"
|
|
738
|
-
}
|
|
739
|
-
}).volatile()
|
|
740
|
-
});
|
|
401
|
+
const Config = Schema.object({});
|
|
741
402
|
function apply(ctx) {
|
|
742
403
|
registerHubModelSettingsChanges(ctx);
|
|
743
404
|
const profile = ctx.get("profileContext");
|
|
744
405
|
const settings = ctx.settings;
|
|
745
|
-
const describe = () => settings.describe({ redactSecrets: true });
|
|
746
|
-
const ownNamespace = ctx.fiber.entry?.options?.id ?? "@klarkxy/dsh-model-hub";
|
|
747
|
-
const hub = createHubSettings(ctx, {
|
|
748
|
-
describe,
|
|
749
|
-
mutate: (ns, ops, revision) => settings.mutate(ns, ops, revision)
|
|
750
|
-
}, ownNamespace);
|
|
751
406
|
const cast = createCast({
|
|
752
|
-
describe,
|
|
753
|
-
mutate: (ns, ops, revision) => settings.mutate(ns, ops, revision),
|
|
407
|
+
describe: () => settings.describe({ redactSecrets: true }),
|
|
408
|
+
mutate: (ns, ops, revision) => settings.mutate(ns, [...ops], revision),
|
|
754
409
|
profileDir: profile?.dir,
|
|
755
410
|
logger: ctx.logger,
|
|
756
411
|
notifyTendenciesChanged: (revision) => notifyModelSettingsChanged(ctx, CAST_TENDENCIES_NAMESPACE, revision)
|
|
757
412
|
});
|
|
758
|
-
const running = createRunningTracker({
|
|
759
|
-
events: ctx,
|
|
760
|
-
agents: ctx.get("agents"),
|
|
761
|
-
subagents: ctx.get("subagents"),
|
|
762
|
-
notify: (revision) => notifyModelSettingsChanged(ctx, RUNNING_NAMESPACE, revision)
|
|
763
|
-
});
|
|
764
413
|
ctx.systemPrompt.section({
|
|
765
414
|
name: "model-hub:subagent-cast",
|
|
766
415
|
order: ctx.systemPrompt.getSectionOrder("TOOL_SUBAGENT") + 1,
|
|
767
416
|
interpolate: false,
|
|
768
417
|
text: () => cast.promptText(() => ctx.get("subagentModelSelection")?.current())
|
|
769
418
|
});
|
|
770
|
-
const run = async () => {
|
|
771
|
-
const reason = "requested";
|
|
772
|
-
let report;
|
|
773
|
-
try {
|
|
774
|
-
report = buildProbeReport(describe());
|
|
775
|
-
} catch (error) {
|
|
776
|
-
ctx.logger.warn("model-hub: probe failed (%s): %s", reason, error instanceof Error ? error.message : String(error));
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
ctx.logger.info("model-hub: probe (%s): %d namespaces, %d with model fields; self-test extra-key=%s role=%s", reason, report.totalNamespaces, report.namespacesWithModelFields, report.selfTest.extraKey, report.selfTest.role);
|
|
780
|
-
if (profile?.dir) try {
|
|
781
|
-
const dir = join(profile.dir, "data", "model-hub");
|
|
782
|
-
await mkdir(dir, { recursive: true });
|
|
783
|
-
await writeFile(join(dir, "probe-report.json"), JSON.stringify(report, null, 2));
|
|
784
|
-
} catch (error) {
|
|
785
|
-
ctx.logger.warn("model-hub: could not write probe report: %s", error instanceof Error ? error.message : String(error));
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
const diagnostics = createProbeDiagnostics(run, (error) => {
|
|
789
|
-
ctx.logger.warn("model-hub: diagnostics failed: %s", String(error));
|
|
790
|
-
});
|
|
791
|
-
ctx.effect(() => {
|
|
792
|
-
const off = ctx.on("settings/document-updated", () => diagnostics.invalidate());
|
|
793
|
-
diagnostics.request();
|
|
794
|
-
return async () => {
|
|
795
|
-
off();
|
|
796
|
-
await diagnostics.dispose();
|
|
797
|
-
};
|
|
798
|
-
});
|
|
799
419
|
ctx.effect(() => registerHostRpc(ctx, CHANNEL, async (endpoint, payload, signal) => {
|
|
800
420
|
try {
|
|
801
|
-
if (endpoint === "fields.list") {
|
|
802
|
-
const value = await hub.list(signal);
|
|
803
|
-
diagnostics.request();
|
|
804
|
-
return {
|
|
805
|
-
ok: true,
|
|
806
|
-
value
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
if (endpoint === "fields.set") return {
|
|
810
|
-
ok: true,
|
|
811
|
-
value: await hub.set(payload, signal)
|
|
812
|
-
};
|
|
813
421
|
if (endpoint.startsWith("cast.")) {
|
|
814
422
|
const row = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
815
423
|
if (endpoint === "cast.get") return {
|
|
@@ -836,9 +444,9 @@ function apply(ctx) {
|
|
|
836
444
|
ok: true,
|
|
837
445
|
value: cast.setTendency(row.provider, row.model, row.tendency, row.expectedRevision, signal)
|
|
838
446
|
};
|
|
839
|
-
if (endpoint === "cast.
|
|
447
|
+
if (endpoint === "cast.setPreferences") return {
|
|
840
448
|
ok: true,
|
|
841
|
-
value:
|
|
449
|
+
value: cast.setRoutePreferences(row.provider, row.model, row.preferences, row.expectedRevision, signal)
|
|
842
450
|
};
|
|
843
451
|
}
|
|
844
452
|
throw new Error(`Unknown endpoint: ${endpoint}`);
|