@klarkxy/dsh-current-title 0.3.1 → 0.3.3
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 +7 -4
- package/docs/README.zh-CN.md +8 -5
- package/lib/client.inner.cjs +965 -280
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +965 -280
- package/lib/contracts.d.ts +53 -3
- package/lib/contracts.js +2 -55
- package/lib/contracts2.d.ts +2 -0
- package/lib/contracts2.js +105 -0
- package/lib/contracts2.js.map +1 -0
- package/lib/index.d.ts +5 -3
- package/lib/index.js +904 -11
- package/lib/index.js.map +1 -1
- package/package.json +8 -5
- package/lib/contracts.js.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,11 +1,888 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createModelSettingsChanges } from "@klarkxy/dsh-plugin-kit/model-settings-changes";
|
|
5
|
-
import { notifyModelSettingsChanged, registerModelSettings } from "@klarkxy/dsh-plugin-kit/model-settings";
|
|
6
|
-
import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
|
|
1
|
+
import { C as normalizeCadence, D as normalizeModelRoute, E as modelRouteOverride, S as isTitleLocaleMode, T as defaultModelRoute, _ as SETTINGS_KEY, b as defaultGenerateConfig, c as DEFAULT_TIMEOUT_MS, d as PLUGIN_NAME, h as RPC_CHANNEL, p as PROVIDER_ID, v as TITLE_CALL_FEATURE, w as normalizePrompt } from "./contracts2.js";
|
|
2
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
|
+
import { BlockAssembler, ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
7
4
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
8
5
|
import { z } from "zod";
|
|
6
|
+
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
7
|
+
function producerMessageSource(plugin) {
|
|
8
|
+
return {
|
|
9
|
+
kind: `plugin:${plugin}`,
|
|
10
|
+
plugin
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* True when `session` is a delegated child thread rather than a human-driven top-level
|
|
15
|
+
* conversation.
|
|
16
|
+
*
|
|
17
|
+
* A message's `source.kind === 'user'` cannot answer this: a lead agent's dispatch prompt
|
|
18
|
+
* is delivered as the child's own user message, so delegated work is indistinguishable
|
|
19
|
+
* from a person typing once you only look at the message. Plugins use this to keep
|
|
20
|
+
* automatic writes and derived model calls on top-level sessions.
|
|
21
|
+
*/
|
|
22
|
+
function isSubagentSession(session) {
|
|
23
|
+
const header = session?.header;
|
|
24
|
+
if (!header || typeof header !== "object") return false;
|
|
25
|
+
if (header.origin === "subagent" || Number(header.delegationDepth ?? 0) > 0) return true;
|
|
26
|
+
const humanFork = header.isSeeded === true && (header.delegationDepth === void 0 || header.delegationDepth === 0) && header.origin === void 0 && (header.classification === void 0 || header.classification === "ordinary" || header.classification === "free");
|
|
27
|
+
return Boolean(header.parentSession) && !humanFork;
|
|
28
|
+
}
|
|
29
|
+
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
30
|
+
function modelSettingsChangesIdentity(packageName) {
|
|
31
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
32
|
+
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
33
|
+
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
34
|
+
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
35
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
36
|
+
return {
|
|
37
|
+
service: segment,
|
|
38
|
+
namespace: segment
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Root context service key holding the shared auxiliary-model state. */
|
|
42
|
+
const AUXILIARY_MODEL_SERVICE = "dshAuxiliaryModel";
|
|
43
|
+
/** Authenticated host-RPC channel for get/set of the shared default. */
|
|
44
|
+
const AUXILIARY_MODEL_CHANNEL = "/dsh-auxiliary-model";
|
|
45
|
+
/** Model-settings namespace under which the shared default is published. */
|
|
46
|
+
const AUXILIARY_MODEL_NS = "@klarkxy/dsh-plugin-kit/auxiliary-model";
|
|
47
|
+
/**
|
|
48
|
+
* Typert identity of the shared change stream. Mounted on the root context
|
|
49
|
+
* with ownerNs filtering, so frames only carry {@link AUXILIARY_MODEL_NS}.
|
|
50
|
+
*/
|
|
51
|
+
const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
|
|
52
|
+
function sharedAuxiliaryModel(host) {
|
|
53
|
+
const root = host?.root;
|
|
54
|
+
try {
|
|
55
|
+
const shared = root?.get?.(AUXILIARY_MODEL_SERVICE, false);
|
|
56
|
+
return shared && typeof shared.snapshot === "function" ? shared : void 0;
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Synchronously read the shared auxiliary default; `undefined` only when no
|
|
63
|
+
* participant retains the singleton or the default is legitimately unset.
|
|
64
|
+
* An initialization or snapshot failure propagates: silently falling back to
|
|
65
|
+
* a different model after a storage error would spend the wrong budget.
|
|
66
|
+
*/
|
|
67
|
+
function getAuxiliaryModel(host) {
|
|
68
|
+
const shared = sharedAuxiliaryModel(host);
|
|
69
|
+
if (!shared) return void 0;
|
|
70
|
+
return modelRouteOverride(shared.snapshot().model ?? void 0);
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../dsh-plugin-kit/src/host-rpc.ts
|
|
74
|
+
/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
|
|
75
|
+
function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
76
|
+
const bodyLimit = options.maxBodyBytes ?? 65536;
|
|
77
|
+
if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
|
|
78
|
+
const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
|
|
79
|
+
return ctx.webServer.register({
|
|
80
|
+
kind: "prefix",
|
|
81
|
+
path: channel,
|
|
82
|
+
handler: async (req, res) => {
|
|
83
|
+
const reject = (status, error, override) => {
|
|
84
|
+
if (res.destroyed) return;
|
|
85
|
+
if (status === 413) res.writeHead(status, { connection: "close" });
|
|
86
|
+
else res.writeHead(status);
|
|
87
|
+
res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
if (typeof ctx.connection.requestRejection !== "function") {
|
|
91
|
+
reject(503);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const rejection = ctx.connection.requestRejection(req);
|
|
95
|
+
if (rejection !== void 0) {
|
|
96
|
+
reject(rejection);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
100
|
+
const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
|
|
101
|
+
if (req.method !== "POST" || !isEndpoint(endpoint)) {
|
|
102
|
+
reject(404);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
|
|
106
|
+
if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
|
|
107
|
+
reject(415);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const guarded = options.beforeBody?.(req, endpoint);
|
|
111
|
+
if (guarded) {
|
|
112
|
+
reject(guarded.status, void 0, guarded.body);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
res.on("close", () => {
|
|
117
|
+
if (!res.writableEnded) controller.abort();
|
|
118
|
+
});
|
|
119
|
+
const chunks = [];
|
|
120
|
+
let size = 0;
|
|
121
|
+
for await (const chunk of req) {
|
|
122
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
123
|
+
size += buffer.length;
|
|
124
|
+
if (size > bodyLimit) {
|
|
125
|
+
reject(413);
|
|
126
|
+
req.destroy();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
chunks.push(buffer);
|
|
130
|
+
}
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
|
|
134
|
+
} catch {
|
|
135
|
+
reject(400);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await dispatch(endpoint, body, controller.signal, res);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
reject(500, error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Write the native Connection response envelope after a plugin has handled a request. */
|
|
146
|
+
function sendHostRpcResponse(response, rpcId, result, noStore = true) {
|
|
147
|
+
if (response.destroyed) return;
|
|
148
|
+
response.writeHead(200, {
|
|
149
|
+
"content-type": "application/json",
|
|
150
|
+
...noStore ? { "cache-control": "no-store" } : {}
|
|
151
|
+
});
|
|
152
|
+
response.end(JSON.stringify({
|
|
153
|
+
type: "server-response",
|
|
154
|
+
rpcId,
|
|
155
|
+
result
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/** Safe host RPC registration. Missing requestRejection fails closed. */
|
|
159
|
+
function registerHostRpc(ctx, channel, call, options = {}) {
|
|
160
|
+
return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
|
|
161
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
162
|
+
res.writeHead(400);
|
|
163
|
+
res.end();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const request = body;
|
|
167
|
+
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
|
|
168
|
+
res.writeHead(400);
|
|
169
|
+
res.end();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const raw = await call(endpoint, request.payload, signal);
|
|
173
|
+
const result = raw.ok ? raw : {
|
|
174
|
+
...raw,
|
|
175
|
+
error: {
|
|
176
|
+
details: {},
|
|
177
|
+
...raw.error
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
sendHostRpcResponse(res, request.rpcId, result);
|
|
181
|
+
}, options);
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region ../dsh-plugin-kit/src/model-settings.ts
|
|
185
|
+
const MODEL_SETTINGS_EVENT = "model-settings/collect";
|
|
186
|
+
const MODEL_SETTINGS_CHANGED_EVENT = "model-settings/changed";
|
|
187
|
+
/** Publish only after the owner's save commits. A failed observer must never
|
|
188
|
+
* turn an already committed write into an apparent save failure. */
|
|
189
|
+
function notifyModelSettingsChanged(ctx, ns, revision) {
|
|
190
|
+
try {
|
|
191
|
+
ctx.parallel(MODEL_SETTINGS_CHANGED_EVENT, {
|
|
192
|
+
ns,
|
|
193
|
+
revision
|
|
194
|
+
}).catch(() => {});
|
|
195
|
+
} catch {}
|
|
196
|
+
}
|
|
197
|
+
/** Cordis disposes the listener with its fiber. In-flight references also fail closed. */
|
|
198
|
+
function registerModelSettings(ctx, provider) {
|
|
199
|
+
let active = true;
|
|
200
|
+
const requireActive = (signal) => {
|
|
201
|
+
signal.throwIfAborted();
|
|
202
|
+
if (!active) throw new Error("Model settings owner unloaded; refresh and retry");
|
|
203
|
+
};
|
|
204
|
+
const guarded = {
|
|
205
|
+
ns: provider.ns,
|
|
206
|
+
title: provider.title,
|
|
207
|
+
instanceId: globalThis.crypto.randomUUID(),
|
|
208
|
+
async read(signal) {
|
|
209
|
+
requireActive(signal);
|
|
210
|
+
const value = await provider.read(signal);
|
|
211
|
+
requireActive(signal);
|
|
212
|
+
return value;
|
|
213
|
+
},
|
|
214
|
+
async write(path, route, revision, signal) {
|
|
215
|
+
requireActive(signal);
|
|
216
|
+
return provider.write(path, route, revision, signal);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const off = ctx.on(MODEL_SETTINGS_EVENT, (collect) => {
|
|
220
|
+
if (active) collect(guarded);
|
|
221
|
+
});
|
|
222
|
+
return () => {
|
|
223
|
+
active = false;
|
|
224
|
+
off();
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region ../dsh-plugin-kit/src/model-settings-client.ts
|
|
229
|
+
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
230
|
+
function parseModelSettingsChangeFrame(value) {
|
|
231
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
|
|
232
|
+
const frame = value;
|
|
233
|
+
if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
|
|
234
|
+
if (frame.kind === "changed" && Object.keys(frame).length === 3 && typeof frame.ns === "string" && frame.ns.length > 0 && typeof frame.revision === "number" && Number.isSafeInteger(frame.revision) && frame.revision >= 0) return {
|
|
235
|
+
kind: "changed",
|
|
236
|
+
ns: frame.ns,
|
|
237
|
+
revision: frame.revision
|
|
238
|
+
};
|
|
239
|
+
throw new Error("Invalid model settings change frame");
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region ../dsh-plugin-kit/src/model-settings-changes.ts
|
|
243
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
244
|
+
var useValue = arguments.length > 2;
|
|
245
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
246
|
+
return useValue ? value : void 0;
|
|
247
|
+
};
|
|
248
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
249
|
+
function accept(f) {
|
|
250
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
251
|
+
return f;
|
|
252
|
+
}
|
|
253
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
254
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
255
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
256
|
+
var _, done = false;
|
|
257
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
258
|
+
var context = {};
|
|
259
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
260
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
261
|
+
context.addInitializer = function(f) {
|
|
262
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
263
|
+
extraInitializers.push(accept(f || null));
|
|
264
|
+
};
|
|
265
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
266
|
+
get: descriptor.get,
|
|
267
|
+
set: descriptor.set
|
|
268
|
+
} : descriptor[key], context);
|
|
269
|
+
if (kind === "accessor") {
|
|
270
|
+
if (result === void 0) continue;
|
|
271
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
272
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
273
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
274
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
275
|
+
} else if (_ = accept(result)) {
|
|
276
|
+
if (kind === "field") initializers.unshift(_);
|
|
277
|
+
else descriptor[key] = _;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
281
|
+
done = true;
|
|
282
|
+
};
|
|
283
|
+
/** Attach first, then yield ready: commits during a consumer's baseline read
|
|
284
|
+
* remain queued. Coalescing revisions never invents a settings snapshot.
|
|
285
|
+
* `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */
|
|
286
|
+
async function* watchModelSettingsChanges(events, signal, ownerNs) {
|
|
287
|
+
signal.throwIfAborted();
|
|
288
|
+
const pending = /* @__PURE__ */ new Map();
|
|
289
|
+
let full = false;
|
|
290
|
+
let wake;
|
|
291
|
+
const changed = (ns, revision) => {
|
|
292
|
+
if (ownerNs !== void 0 && ns !== ownerNs) return;
|
|
293
|
+
let frame;
|
|
294
|
+
try {
|
|
295
|
+
frame = parseModelSettingsChangeFrame({
|
|
296
|
+
kind: "changed",
|
|
297
|
+
ns,
|
|
298
|
+
revision
|
|
299
|
+
});
|
|
300
|
+
} catch {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!full) {
|
|
304
|
+
if (!pending.has(ns) && pending.size >= 256) {
|
|
305
|
+
pending.clear();
|
|
306
|
+
full = true;
|
|
307
|
+
} else pending.set(ns, frame);
|
|
308
|
+
}
|
|
309
|
+
wake?.();
|
|
310
|
+
};
|
|
311
|
+
const disposers = [];
|
|
312
|
+
const cleanup = () => {
|
|
313
|
+
for (const off of disposers.splice(0).reverse()) off();
|
|
314
|
+
pending.clear();
|
|
315
|
+
};
|
|
316
|
+
const aborted = () => {
|
|
317
|
+
cleanup();
|
|
318
|
+
wake?.();
|
|
319
|
+
};
|
|
320
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
321
|
+
try {
|
|
322
|
+
disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)));
|
|
323
|
+
disposers.push(events.on("settings/document-updated", changed));
|
|
324
|
+
signal.throwIfAborted();
|
|
325
|
+
yield { kind: "ready" };
|
|
326
|
+
while (true) {
|
|
327
|
+
signal.throwIfAborted();
|
|
328
|
+
if (full) {
|
|
329
|
+
full = false;
|
|
330
|
+
yield { kind: "ready" };
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (pending.size) {
|
|
334
|
+
const [ns, frame] = pending.entries().next().value;
|
|
335
|
+
pending.delete(ns);
|
|
336
|
+
yield frame;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
await new Promise((resolve) => {
|
|
340
|
+
wake = resolve;
|
|
341
|
+
if (signal.aborted || full || pending.size) resolve();
|
|
342
|
+
});
|
|
343
|
+
wake = void 0;
|
|
344
|
+
}
|
|
345
|
+
} finally {
|
|
346
|
+
signal.removeEventListener("abort", aborted);
|
|
347
|
+
cleanup();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** One owner's invalidation stream, registered on the calling plugin fiber.
|
|
351
|
+
* Nothing here is stored in a module-level singleton. */
|
|
352
|
+
let ModelSettingsChangesController = (() => {
|
|
353
|
+
let _classSuper = TypertRemoteService;
|
|
354
|
+
let _instanceExtraInitializers = [];
|
|
355
|
+
let _watch_decorators;
|
|
356
|
+
return class ModelSettingsChangesController extends _classSuper {
|
|
357
|
+
static {
|
|
358
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
359
|
+
_watch_decorators = [Remote({ mode: "stream" })];
|
|
360
|
+
__esDecorate(this, null, _watch_decorators, {
|
|
361
|
+
kind: "method",
|
|
362
|
+
name: "watch",
|
|
363
|
+
static: false,
|
|
364
|
+
private: false,
|
|
365
|
+
access: {
|
|
366
|
+
has: (obj) => "watch" in obj,
|
|
367
|
+
get: (obj) => obj.watch
|
|
368
|
+
},
|
|
369
|
+
metadata: _metadata
|
|
370
|
+
}, null, _instanceExtraInitializers);
|
|
371
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
372
|
+
enumerable: true,
|
|
373
|
+
configurable: true,
|
|
374
|
+
writable: true,
|
|
375
|
+
value: _metadata
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
lifetime = (__runInitializers(this, _instanceExtraInitializers), new AbortController());
|
|
379
|
+
ownerNs;
|
|
380
|
+
constructor(ctx, identity, ownerNs) {
|
|
381
|
+
super(ctx, identity.service, { namespace: identity.namespace });
|
|
382
|
+
this.ownerNs = ownerNs;
|
|
383
|
+
ctx.effect(() => () => {
|
|
384
|
+
this.lifetime.abort(/* @__PURE__ */ new Error("Model settings sync owner unloaded"));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
watch(signal) {
|
|
388
|
+
return watchModelSettingsChanges(this.ctx, AbortSignal.any([this.lifetime.signal, signal]), this.ownerNs);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
})();
|
|
392
|
+
/** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */
|
|
393
|
+
function createModelSettingsChanges(ctx, identity, ownerNs) {
|
|
394
|
+
return new ModelSettingsChangesController(ctx, identity, ownerNs);
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region ../dsh-plugin-kit/src/auxiliary-model.ts
|
|
398
|
+
const DOMAIN_NAME = "dsh_auxiliary";
|
|
399
|
+
const DOMAIN_VERSION = 1;
|
|
400
|
+
const TABLE = "settings";
|
|
401
|
+
const KEY = "defaults";
|
|
402
|
+
const INJECT = [
|
|
403
|
+
"storageDomain",
|
|
404
|
+
"webServer",
|
|
405
|
+
"connection"
|
|
406
|
+
];
|
|
407
|
+
/** Marker on the benign "released while the domain was still opening" error. */
|
|
408
|
+
const RECOVERABLE = Symbol.for("klarkxy.dsh.auxiliary-model.recoverable");
|
|
409
|
+
function recoverable(error) {
|
|
410
|
+
error[RECOVERABLE] = true;
|
|
411
|
+
return error;
|
|
412
|
+
}
|
|
413
|
+
async function openSharedDomain(scope) {
|
|
414
|
+
const [{ defineDomain, domainTable }, { z }] = await Promise.all([import("@deepseek-ai/dsh-storage-domain"), import("zod")]);
|
|
415
|
+
const identifier = z.string().min(1).max(250).refine((value) => value === value.trim() && !/[\x00-\x1f\x7f]/.test(value), "model route identifiers must be non-blank, trimmed and control-free");
|
|
416
|
+
const model = z.union([z.object({
|
|
417
|
+
provider: identifier,
|
|
418
|
+
model: identifier,
|
|
419
|
+
reasoningEffort: identifier.max(80).optional()
|
|
420
|
+
}).strict(), z.null()]);
|
|
421
|
+
return scope.storageDomain.open(defineDomain({
|
|
422
|
+
name: DOMAIN_NAME,
|
|
423
|
+
version: DOMAIN_VERSION,
|
|
424
|
+
tables: { [TABLE]: domainTable(z.object({
|
|
425
|
+
revision: z.number().int().nonnegative(),
|
|
426
|
+
model
|
|
427
|
+
}).strict()) }
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
/** Marks an optimistic-lock conflict so the RPC envelope can carry 'conflict'. */
|
|
431
|
+
function conflictError(message) {
|
|
432
|
+
return Object.assign(new Error(message), { conflict: true });
|
|
433
|
+
}
|
|
434
|
+
/** RPC-visible error classification; storage failures stay sanitized. */
|
|
435
|
+
function rpcFailure(error) {
|
|
436
|
+
if (error && typeof error === "object" && "conflict" in error) return {
|
|
437
|
+
ok: false,
|
|
438
|
+
error: {
|
|
439
|
+
code: "auxiliary-model/conflict",
|
|
440
|
+
message: error instanceof Error ? error.message : "Revision conflict"
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
if (error instanceof Error && /Invalid model route|valid expected revision|Unknown auxiliary model setting/.test(error.message)) return {
|
|
444
|
+
ok: false,
|
|
445
|
+
error: {
|
|
446
|
+
code: "auxiliary-model/rejected",
|
|
447
|
+
message: error.message
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
error: {
|
|
453
|
+
code: "auxiliary-model/store",
|
|
454
|
+
message: "Auxiliary model store request failed"
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Serialized optimistic-lock write; resolves after the durable put commits.
|
|
460
|
+
* Revision verification happens inside the queue: two writers carrying the
|
|
461
|
+
* same expectedRevision race for one slot and only the first commits.
|
|
462
|
+
* Notification fires only after commit, so a failed observer can never turn
|
|
463
|
+
* an already committed write into an apparent save failure.
|
|
464
|
+
*/
|
|
465
|
+
function setSharedModel(state, ctx, input, expectedRevision) {
|
|
466
|
+
if (!Number.isSafeInteger(expectedRevision)) return Promise.reject(conflictError("A valid expected revision is required; reload and retry"));
|
|
467
|
+
const row = input && typeof input === "object" && !Array.isArray(input) ? input : void 0;
|
|
468
|
+
const validField = (value) => typeof value === "string" && value === value.trim() && !/[\x00-\x1f\x7f]/.test(value);
|
|
469
|
+
if (!row || [
|
|
470
|
+
"provider",
|
|
471
|
+
"model",
|
|
472
|
+
"reasoningEffort"
|
|
473
|
+
].some((key) => row[key] !== void 0 && !validField(row[key]))) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
|
|
474
|
+
const normalized = normalizeModelRoute(row);
|
|
475
|
+
if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
|
|
476
|
+
const commit = state.tail.then(async () => {
|
|
477
|
+
if (state.closed || !state.domain) throw new Error("Auxiliary model settings are unloading; retry in a moment");
|
|
478
|
+
if (expectedRevision !== state.data.revision) throw conflictError("Auxiliary model settings changed; reload and retry");
|
|
479
|
+
const next = {
|
|
480
|
+
revision: state.data.revision + 1,
|
|
481
|
+
model: modelRouteOverride(normalized) ?? null
|
|
482
|
+
};
|
|
483
|
+
await state.domain.table(TABLE).put(KEY, {
|
|
484
|
+
revision: next.revision,
|
|
485
|
+
model: next.model
|
|
486
|
+
});
|
|
487
|
+
state.data = next;
|
|
488
|
+
notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision);
|
|
489
|
+
return next;
|
|
490
|
+
});
|
|
491
|
+
state.tail = commit.then(() => {}, () => {});
|
|
492
|
+
return commit;
|
|
493
|
+
}
|
|
494
|
+
function initScope(state, ctx, scope) {
|
|
495
|
+
const generation = ++state.generation;
|
|
496
|
+
let disposed = false;
|
|
497
|
+
scope.effect(() => () => {
|
|
498
|
+
disposed = true;
|
|
499
|
+
});
|
|
500
|
+
const stale = () => state.closed || disposed || state.generation !== generation;
|
|
501
|
+
const run = (async () => {
|
|
502
|
+
state.available = false;
|
|
503
|
+
const domain = await openSharedDomain(scope);
|
|
504
|
+
if (stale()) {
|
|
505
|
+
await domain.close();
|
|
506
|
+
throw recoverable(/* @__PURE__ */ new Error("Auxiliary model store released during open"));
|
|
507
|
+
}
|
|
508
|
+
state.domain = domain;
|
|
509
|
+
try {
|
|
510
|
+
scope.effect(() => () => domain.close());
|
|
511
|
+
} catch (error) {
|
|
512
|
+
await domain.close();
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
const stored = domain.table(TABLE).get(KEY);
|
|
516
|
+
if (stale()) {
|
|
517
|
+
await domain.close();
|
|
518
|
+
throw recoverable(/* @__PURE__ */ new Error("Auxiliary model store superseded during open"));
|
|
519
|
+
}
|
|
520
|
+
state.data = {
|
|
521
|
+
revision: stored?.revision ?? 0,
|
|
522
|
+
model: stored?.model ?? null
|
|
523
|
+
};
|
|
524
|
+
state.error = void 0;
|
|
525
|
+
state.available = true;
|
|
526
|
+
scope.effect(() => registerModelSettings(scope, {
|
|
527
|
+
ns: AUXILIARY_MODEL_NS,
|
|
528
|
+
title: {
|
|
529
|
+
zh: "辅助模型",
|
|
530
|
+
en: "Auxiliary model"
|
|
531
|
+
},
|
|
532
|
+
read(signal) {
|
|
533
|
+
signal.throwIfAborted();
|
|
534
|
+
const { revision, model } = state.data;
|
|
535
|
+
return {
|
|
536
|
+
revision,
|
|
537
|
+
fields: [{
|
|
538
|
+
path: ["model"],
|
|
539
|
+
label: {
|
|
540
|
+
zh: "全局辅助模型",
|
|
541
|
+
en: "Global auxiliary model"
|
|
542
|
+
},
|
|
543
|
+
current: model ?? void 0,
|
|
544
|
+
allowFollow: true,
|
|
545
|
+
followLabel: {
|
|
546
|
+
zh: "当前会话模型",
|
|
547
|
+
en: "Current conversation model"
|
|
548
|
+
}
|
|
549
|
+
}]
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
async write(path, route, expectedRevision, signal) {
|
|
553
|
+
signal.throwIfAborted();
|
|
554
|
+
if (path.length !== 1 || path[0] !== "model") throw new Error("Unknown auxiliary model setting");
|
|
555
|
+
return setSharedModel(state, ctx, route, expectedRevision);
|
|
556
|
+
}
|
|
557
|
+
}));
|
|
558
|
+
scope.effect(() => registerHostRpc(scope, AUXILIARY_MODEL_CHANNEL, async (endpoint, payload, signal) => {
|
|
559
|
+
signal.throwIfAborted();
|
|
560
|
+
if (state.closed) return {
|
|
561
|
+
ok: false,
|
|
562
|
+
error: {
|
|
563
|
+
code: "auxiliary-model/unloading",
|
|
564
|
+
message: "Auxiliary model settings are unloading; retry in a moment"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
if (!state.available) return state.error ? {
|
|
568
|
+
ok: false,
|
|
569
|
+
error: {
|
|
570
|
+
code: "auxiliary-model/store",
|
|
571
|
+
message: "Auxiliary model store request failed"
|
|
572
|
+
}
|
|
573
|
+
} : {
|
|
574
|
+
ok: false,
|
|
575
|
+
error: {
|
|
576
|
+
code: "auxiliary-model/unavailable",
|
|
577
|
+
message: "Auxiliary model settings are unavailable; retry in a moment"
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
if (endpoint === "defaults.get") {
|
|
581
|
+
const { revision, model } = state.data;
|
|
582
|
+
return {
|
|
583
|
+
ok: true,
|
|
584
|
+
value: {
|
|
585
|
+
revision,
|
|
586
|
+
model
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (endpoint === "defaults.set") try {
|
|
591
|
+
const body = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
592
|
+
const value = await setSharedModel(state, ctx, body.model, body.expectedRevision);
|
|
593
|
+
signal.throwIfAborted();
|
|
594
|
+
return {
|
|
595
|
+
ok: true,
|
|
596
|
+
value
|
|
597
|
+
};
|
|
598
|
+
} catch (error) {
|
|
599
|
+
if (signal.aborted) throw error;
|
|
600
|
+
return rpcFailure(error);
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
ok: false,
|
|
604
|
+
error: {
|
|
605
|
+
code: "auxiliary-model/unknown-endpoint",
|
|
606
|
+
message: `Unknown endpoint: ${endpoint}`
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}, { isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint) }));
|
|
610
|
+
createModelSettingsChanges(scope, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS);
|
|
611
|
+
scope.effect(() => () => {
|
|
612
|
+
if (state.generation === generation) {
|
|
613
|
+
state.available = false;
|
|
614
|
+
state.domain = void 0;
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
})().catch((error) => {
|
|
618
|
+
if (error && typeof error === "object") error.auxGeneration = generation;
|
|
619
|
+
if (state.generation === generation) {
|
|
620
|
+
state.available = false;
|
|
621
|
+
state.error = error;
|
|
622
|
+
}
|
|
623
|
+
throw error;
|
|
624
|
+
});
|
|
625
|
+
run.catch(() => {});
|
|
626
|
+
const armed = run.then(() => {});
|
|
627
|
+
armed.catch(() => {});
|
|
628
|
+
state.ready = armed;
|
|
629
|
+
return run;
|
|
630
|
+
}
|
|
631
|
+
function teardown(state, ctx) {
|
|
632
|
+
if (!state.closing) {
|
|
633
|
+
state.closed = true;
|
|
634
|
+
const root = ctx.root;
|
|
635
|
+
state.closing = (async () => {
|
|
636
|
+
await state.scopeInit?.catch(() => {});
|
|
637
|
+
await state.tail.catch(() => {});
|
|
638
|
+
try {
|
|
639
|
+
await state.fiber?.dispose();
|
|
640
|
+
} catch (error) {
|
|
641
|
+
try {
|
|
642
|
+
ctx.logger?.warn("auxiliary-model: fiber dispose failed", error);
|
|
643
|
+
} catch {}
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
if (root.get("dshAuxiliaryModel", false) === state) state.stopUnprovide?.();
|
|
647
|
+
} catch {}
|
|
648
|
+
state.stopUnprovide = void 0;
|
|
649
|
+
})();
|
|
650
|
+
}
|
|
651
|
+
return state.closing;
|
|
652
|
+
}
|
|
653
|
+
function createSharedState(ctx) {
|
|
654
|
+
const root = ctx.root;
|
|
655
|
+
const state = {
|
|
656
|
+
users: 0,
|
|
657
|
+
closed: false,
|
|
658
|
+
available: false,
|
|
659
|
+
generation: 0,
|
|
660
|
+
tail: Promise.resolve(),
|
|
661
|
+
data: {
|
|
662
|
+
revision: 0,
|
|
663
|
+
model: null
|
|
664
|
+
},
|
|
665
|
+
snapshot() {
|
|
666
|
+
if (this.error) throw this.error;
|
|
667
|
+
if (!this.available) throw new Error("Auxiliary model store is unavailable; retry in a moment");
|
|
668
|
+
return this.data;
|
|
669
|
+
},
|
|
670
|
+
ready: Promise.resolve(),
|
|
671
|
+
teardown() {
|
|
672
|
+
return teardown(this, ctx);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
const inactive = INJECT.filter((service) => {
|
|
676
|
+
try {
|
|
677
|
+
return root.get(service, true) === void 0;
|
|
678
|
+
} catch {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
if (inactive.length) throw new Error(`retainAuxiliaryModel requires active ${inactive.join(", ")}; declare them in inject`);
|
|
683
|
+
state.fiber = root.inject(INJECT, (scope) => {
|
|
684
|
+
state.scopeInit = initScope(state, ctx, scope);
|
|
685
|
+
});
|
|
686
|
+
state.ready = Promise.resolve(state.fiber).then(() => {
|
|
687
|
+
if (!state.scopeInit) throw recoverable(/* @__PURE__ */ new Error("Auxiliary model owner fiber settled without initializing its scope"));
|
|
688
|
+
return state.scopeInit;
|
|
689
|
+
});
|
|
690
|
+
state.ready.catch((error) => {
|
|
691
|
+
const tag = error.auxGeneration;
|
|
692
|
+
if (tag !== void 0 && tag !== state.generation) return;
|
|
693
|
+
state.error = state.error ?? error;
|
|
694
|
+
state.closed = true;
|
|
695
|
+
state.teardown();
|
|
696
|
+
});
|
|
697
|
+
state.stopUnprovide = root.provide(AUXILIARY_MODEL_SERVICE, state);
|
|
698
|
+
return state;
|
|
699
|
+
}
|
|
700
|
+
async function obtainShared(ctx) {
|
|
701
|
+
const root = ctx.root;
|
|
702
|
+
for (;;) {
|
|
703
|
+
const existing = root.get(AUXILIARY_MODEL_SERVICE, false);
|
|
704
|
+
if (existing) {
|
|
705
|
+
if (!existing.closed) return existing;
|
|
706
|
+
await existing.closing;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
return createSharedState(ctx);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Retain the shared auxiliary-model store for the calling plugin.
|
|
714
|
+
*
|
|
715
|
+
* Resolves once storage is open and the read/write surfaces are live, with a
|
|
716
|
+
* release function; register it as `ctx.effect(() => release)` so the store
|
|
717
|
+
* unloads with the plugin. The first participant opens the shared owner
|
|
718
|
+
* fiber; `await release()` drains pending writes and fully tears the owner
|
|
719
|
+
* down when the last participant leaves. Requires `storageDomain`,
|
|
720
|
+
* `webServer` and `connection` to exist on the host.
|
|
721
|
+
*
|
|
722
|
+
* Concurrent retainers share one owner; a retain arriving during teardown
|
|
723
|
+
* waits for the closing owner and then reinstalls — it never inherits a
|
|
724
|
+
* half-torn-down state.
|
|
725
|
+
*/
|
|
726
|
+
async function retainAuxiliaryModel(ctx) {
|
|
727
|
+
for (;;) {
|
|
728
|
+
const state = await obtainShared(ctx);
|
|
729
|
+
await state.ready;
|
|
730
|
+
if (state.closed) continue;
|
|
731
|
+
state.users += 1;
|
|
732
|
+
let released = false;
|
|
733
|
+
return async () => {
|
|
734
|
+
if (released) return;
|
|
735
|
+
released = true;
|
|
736
|
+
state.users -= 1;
|
|
737
|
+
if (state.users <= 0) await state.teardown();
|
|
738
|
+
else await state.tail.catch(() => {});
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
new TextEncoder();
|
|
743
|
+
function normalizeSegments(context) {
|
|
744
|
+
if (context === void 0) return [];
|
|
745
|
+
return (typeof context === "string" ? [context] : context).filter((segment) => typeof segment === "string" && segment.length > 0);
|
|
746
|
+
}
|
|
747
|
+
/** Only word characters, dots, colons and dashes are kept inside the feature attribute. */
|
|
748
|
+
function featureAttribute(feature) {
|
|
749
|
+
return feature.replaceAll(/[^\w.:-]/g, "_");
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Assemble the single user message for an auxiliary model call: stable
|
|
753
|
+
* material in one labelled block first, the live request last. With no
|
|
754
|
+
* context the result is byte-identical to `text`, so callers without
|
|
755
|
+
* material keep exactly the previous wire shape.
|
|
756
|
+
*/
|
|
757
|
+
function buildAuxiliaryUserText(request) {
|
|
758
|
+
const context = normalizeSegments(request.context);
|
|
759
|
+
if (!context.length) return request.text;
|
|
760
|
+
return `<auxiliary-context${request.feature ? ` feature="${featureAttribute(request.feature)}"` : ""}>\n${context.join("\n")}\n</auxiliary-context>\n<request>\n${request.text}\n</request>`;
|
|
761
|
+
}
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region ../dsh-plugin-kit/src/llm-call.ts
|
|
764
|
+
function routeOf(value) {
|
|
765
|
+
if (typeof value?.provider !== "string" || !value.provider.trim()) return void 0;
|
|
766
|
+
if (typeof value.model !== "string" || !value.model.trim()) return void 0;
|
|
767
|
+
const effort = typeof value.reasoningEffort === "string" ? value.reasoningEffort.trim() : "";
|
|
768
|
+
return effort ? {
|
|
769
|
+
provider: value.provider.trim(),
|
|
770
|
+
model: value.model.trim(),
|
|
771
|
+
reasoningEffort: effort
|
|
772
|
+
} : {
|
|
773
|
+
provider: value.provider.trim(),
|
|
774
|
+
model: value.model.trim()
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Plugin selection first; then the shared auxiliary default (when any
|
|
779
|
+
* participant retains it); otherwise the live session model, then the host
|
|
780
|
+
* default. Cordis callers must declare agents, sessionProjections and
|
|
781
|
+
* agentDefaultModel in their inject list so the fallback reads use the
|
|
782
|
+
* caller's live services.
|
|
783
|
+
*/
|
|
784
|
+
function resolveFeatureModel(host, saved, sessionId) {
|
|
785
|
+
const explicit = routeOf(saved);
|
|
786
|
+
if (explicit) return explicit;
|
|
787
|
+
const auxiliary = getAuxiliaryModel(host);
|
|
788
|
+
if (auxiliary) return auxiliary;
|
|
789
|
+
const face = host;
|
|
790
|
+
if (sessionId && face?.agents && face.sessionProjections) {
|
|
791
|
+
const session = face.agents.get(sessionId)?.session;
|
|
792
|
+
if (session) {
|
|
793
|
+
try {
|
|
794
|
+
const pending = face.sessionProjections.stateOf(session, "modelSelection")?.pending;
|
|
795
|
+
const selected = routeOf(pending ?? void 0);
|
|
796
|
+
if (selected) return selected;
|
|
797
|
+
} catch {}
|
|
798
|
+
const header = session.requestHeader?.();
|
|
799
|
+
const recorded = header?.config;
|
|
800
|
+
if (recorded) {
|
|
801
|
+
const route = routeOf(header?.adapterDefaults?.reasoningEffort === true ? {
|
|
802
|
+
provider: recorded.provider,
|
|
803
|
+
model: recorded.model
|
|
804
|
+
} : recorded);
|
|
805
|
+
if (route) return route;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return routeOf(face?.agentDefaultModel?.currentSelection());
|
|
810
|
+
}
|
|
811
|
+
function joinText(assembler) {
|
|
812
|
+
return assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
813
|
+
}
|
|
814
|
+
async function readStream(stream, signal, live) {
|
|
815
|
+
const assembler = new BlockAssembler();
|
|
816
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
817
|
+
try {
|
|
818
|
+
while (live()) {
|
|
819
|
+
if (signal.aborted) break;
|
|
820
|
+
const item = await iterator.next();
|
|
821
|
+
if (item.done) break;
|
|
822
|
+
assembler.push(item.value);
|
|
823
|
+
}
|
|
824
|
+
} finally {
|
|
825
|
+
await iterator.return?.();
|
|
826
|
+
}
|
|
827
|
+
return assembler;
|
|
828
|
+
}
|
|
829
|
+
/** One native model call. No retry, timeout policy, or provider queue. */
|
|
830
|
+
async function callLlmText(llm, request) {
|
|
831
|
+
request.signal.throwIfAborted();
|
|
832
|
+
const live = () => request.isCurrent?.() !== false && !request.signal.aborted;
|
|
833
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
834
|
+
const effort = request.route.reasoningEffort?.trim();
|
|
835
|
+
const prepared = await llm.prepareCall({
|
|
836
|
+
provider: request.route.provider,
|
|
837
|
+
model: request.route.model,
|
|
838
|
+
...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
|
|
839
|
+
maxTokens: request.maxTokens
|
|
840
|
+
}, request.signal, request.configure);
|
|
841
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
842
|
+
const options = {
|
|
843
|
+
provider: prepared.config.provider,
|
|
844
|
+
model: prepared.config.model,
|
|
845
|
+
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
846
|
+
...prepared.config.maxTokens !== void 0 ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens },
|
|
847
|
+
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
848
|
+
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
849
|
+
system: request.system,
|
|
850
|
+
messages: [createUserMessage({
|
|
851
|
+
source: producerMessageSource(request.plugin),
|
|
852
|
+
content: [{
|
|
853
|
+
type: "text",
|
|
854
|
+
text: buildAuxiliaryUserText(request)
|
|
855
|
+
}]
|
|
856
|
+
})],
|
|
857
|
+
signal: request.signal,
|
|
858
|
+
...request.purpose ? { purpose: request.purpose } : {},
|
|
859
|
+
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
860
|
+
};
|
|
861
|
+
const assembler = await readStream(prepared.stream(options), request.signal, live);
|
|
862
|
+
if (request.signal.aborted || assembler.finish.kind === "aborted") throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
863
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
864
|
+
if (assembler.finish.kind === "error") {
|
|
865
|
+
const code = assembler.finish.failure.code;
|
|
866
|
+
throw Object.assign(new Error(assembler.finish.failure.message || "模型调用失败。"), { code });
|
|
867
|
+
}
|
|
868
|
+
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
869
|
+
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
870
|
+
const usage = assembler.usage;
|
|
871
|
+
return {
|
|
872
|
+
text: joinText(assembler),
|
|
873
|
+
provider: prepared.config.provider,
|
|
874
|
+
model: prepared.config.model,
|
|
875
|
+
...usage ? {
|
|
876
|
+
inputTokens: usage.inputTokens,
|
|
877
|
+
outputTokens: usage.outputTokens,
|
|
878
|
+
...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
879
|
+
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
880
|
+
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
881
|
+
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
|
|
882
|
+
} : {}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
//#endregion
|
|
9
886
|
//#region src/input.ts
|
|
10
887
|
const FRAME_PREFIX = "Choose the current task from these recent human messages, ordered oldest to newest:\n";
|
|
11
888
|
function frameMessages(messages) {
|
|
@@ -205,6 +1082,18 @@ function sourceVersionOf(sessionId, messages) {
|
|
|
205
1082
|
}
|
|
206
1083
|
//#endregion
|
|
207
1084
|
//#region src/generate.ts
|
|
1085
|
+
/**
|
|
1086
|
+
* Fixed bytes the auxiliary-context envelope adds around the selected
|
|
1087
|
+
* messages, so maxInputBytes keeps bounding the wire text. The stable block
|
|
1088
|
+
* carries the framing rule and the request block the JSON message list —
|
|
1089
|
+
* selectRecentMessages measures that same pair through frameMessages, so
|
|
1090
|
+
* only the constant envelope overhead is new.
|
|
1091
|
+
*/
|
|
1092
|
+
const ENVELOPE_OVERHEAD_BYTES = utf8Bytes(buildAuxiliaryUserText({
|
|
1093
|
+
feature: TITLE_CALL_FEATURE,
|
|
1094
|
+
context: FRAME_PREFIX,
|
|
1095
|
+
text: "[]"
|
|
1096
|
+
})) - utf8Bytes(frameMessages([]));
|
|
208
1097
|
function systemPrompt(config, customPrompt = "") {
|
|
209
1098
|
return [
|
|
210
1099
|
customPrompt.trim() || "Name the current task in an AI coding-assistant session.",
|
|
@@ -236,15 +1125,15 @@ function titleModelRoute(route) {
|
|
|
236
1125
|
async function generateCurrentTitle(options) {
|
|
237
1126
|
options.signal.throwIfAborted();
|
|
238
1127
|
if (!options.isCurrent()) throw Object.assign(/* @__PURE__ */ new Error("dsh-current-title: generation is no longer current"), { code: "superseded" });
|
|
239
|
-
const selected = selectRecentMessages(options.messages, options.config.maxRecentMessages, options.config.maxInputBytes);
|
|
1128
|
+
const selected = selectRecentMessages(options.messages, options.config.maxRecentMessages, options.config.maxInputBytes - ENVELOPE_OVERHEAD_BYTES);
|
|
240
1129
|
if (selected.length === 0) throw new Error("dsh-current-title: at least one source message is required");
|
|
241
1130
|
const route = resolveFeatureModel(options.host, titleModelRoute(options.model), options.sessionId);
|
|
242
|
-
if (!route) throw new Error("dsh-current-title:
|
|
1131
|
+
if (!route) throw new Error("dsh-current-title: 请在设置页选择模型,或设置默认辅助模型 / 宿主默认对话模型。");
|
|
243
1132
|
const callRoute = {
|
|
244
1133
|
provider: route.provider,
|
|
245
1134
|
model: route.model
|
|
246
1135
|
};
|
|
247
|
-
const framedInput =
|
|
1136
|
+
const framedInput = JSON.stringify(selected);
|
|
248
1137
|
const locale = resolveTitleLocale(options.localeMode ?? options.config.locale, selected.map((message) => message.text), options.localePreference);
|
|
249
1138
|
const sourceVersion = sourceVersionOf(options.sessionId, selected);
|
|
250
1139
|
const deadline = new AbortController();
|
|
@@ -257,6 +1146,8 @@ async function generateCurrentTitle(options) {
|
|
|
257
1146
|
plugin: PLUGIN_NAME,
|
|
258
1147
|
route: callRoute,
|
|
259
1148
|
system: systemPrompt(options.config, options.prompt),
|
|
1149
|
+
feature: TITLE_CALL_FEATURE,
|
|
1150
|
+
context: FRAME_PREFIX,
|
|
260
1151
|
text: framedInput,
|
|
261
1152
|
maxTokens: options.config.maxOutputTokens,
|
|
262
1153
|
purpose: "session-title",
|
|
@@ -811,6 +1702,8 @@ const inject = [
|
|
|
811
1702
|
];
|
|
812
1703
|
async function apply(ctx, config = {}) {
|
|
813
1704
|
createModelSettingsChanges(ctx, modelSettingsChangesIdentity(PLUGIN_NAME), PLUGIN_NAME);
|
|
1705
|
+
const release = await retainAuxiliaryModel(ctx);
|
|
1706
|
+
ctx.effect(() => release, "current-title.auxiliary-model");
|
|
814
1707
|
const host = ctx;
|
|
815
1708
|
const domain = await host.storageDomain.open(titleDomain);
|
|
816
1709
|
const table = domain.table("settings");
|
|
@@ -856,8 +1749,8 @@ async function apply(ctx, config = {}) {
|
|
|
856
1749
|
},
|
|
857
1750
|
current: settings.model,
|
|
858
1751
|
followLabel: {
|
|
859
|
-
zh: "
|
|
860
|
-
en: "
|
|
1752
|
+
zh: "默认辅助模型",
|
|
1753
|
+
en: "Default auxiliary model"
|
|
861
1754
|
},
|
|
862
1755
|
allowFollow: true
|
|
863
1756
|
}]
|