@klarkxy/dsh-git-commit 0.3.0 → 0.3.2
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 +11 -8
- package/docs/README.zh-CN.md +10 -7
- package/lib/client.inner.cjs +819 -112
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +819 -112
- package/lib/contracts.d.ts +1801 -5
- package/lib/contracts.js +2 -35
- package/lib/contracts2.d.ts +2 -0
- package/lib/contracts2.js +85 -0
- package/lib/contracts2.js.map +1 -0
- package/lib/index.d.ts +10 -3
- package/lib/index.js +3935 -22
- package/lib/index.js.map +1 -1
- package/package.json +4 -4
- package/lib/contracts.js.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
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 { modelRouteOverride } from "@klarkxy/dsh-model-route";
|
|
7
|
-
import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
|
|
1
|
+
import { _ as defaultSettings, b as modelRouteOverride, g as SETTINGS_KEY, l as MAX_UNTRACKED_PREVIEW_CHARS, m as RPC_CHANNEL, r as DEFAULT_MAX_OUTPUT_TOKENS, s as MAX_MANIFEST_CHARS, t as COMMIT_PLAN_FEATURE, u as PLUGIN_NAME, v as normalizeFallback, x as normalizeModelRoute } from "./contracts2.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
8
4
|
import { execFile } from "node:child_process";
|
|
9
5
|
import { createHash } from "node:crypto";
|
|
10
6
|
import { constants } from "node:fs";
|
|
@@ -13,6 +9,3887 @@ import { tmpdir } from "node:os";
|
|
|
13
9
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
14
10
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
15
11
|
import { z } from "zod";
|
|
12
|
+
//#region ../dsh-plugin-kit/src/contracts.ts
|
|
13
|
+
function producerMessageSource(plugin) {
|
|
14
|
+
return {
|
|
15
|
+
kind: `plugin:${plugin}`,
|
|
16
|
+
plugin
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
|
|
20
|
+
function modelSettingsChangesIdentity(packageName) {
|
|
21
|
+
const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
|
|
22
|
+
const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
23
|
+
if (!parts.length) throw new Error("Model settings package name has no RPC segment");
|
|
24
|
+
const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
|
|
25
|
+
if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
|
|
26
|
+
return {
|
|
27
|
+
service: segment,
|
|
28
|
+
namespace: segment
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Root context service key holding the shared auxiliary-model state. */
|
|
32
|
+
const AUXILIARY_MODEL_SERVICE = "dshAuxiliaryModel";
|
|
33
|
+
/** Authenticated host-RPC channel for get/set of the shared default. */
|
|
34
|
+
const AUXILIARY_MODEL_CHANNEL = "/dsh-auxiliary-model";
|
|
35
|
+
/** Model-settings namespace under which the shared default is published. */
|
|
36
|
+
const AUXILIARY_MODEL_NS = "@klarkxy/dsh-plugin-kit/auxiliary-model";
|
|
37
|
+
/**
|
|
38
|
+
* Typert identity of the shared change stream. Mounted on the root context
|
|
39
|
+
* with ownerNs filtering, so frames only carry {@link AUXILIARY_MODEL_NS}.
|
|
40
|
+
*/
|
|
41
|
+
const AUXILIARY_MODEL_CHANGES_IDENTITY = modelSettingsChangesIdentity("@klarkxy/dsh-plugin-kit");
|
|
42
|
+
function sharedAuxiliaryModel(host) {
|
|
43
|
+
const root = host?.root;
|
|
44
|
+
try {
|
|
45
|
+
const shared = root?.get?.(AUXILIARY_MODEL_SERVICE, false);
|
|
46
|
+
return shared && typeof shared.snapshot === "function" ? shared : void 0;
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Synchronously read the shared auxiliary default; `undefined` only when no
|
|
53
|
+
* participant retains the singleton or the default is legitimately unset.
|
|
54
|
+
* An initialization or snapshot failure propagates: silently falling back to
|
|
55
|
+
* a different model after a storage error would spend the wrong budget.
|
|
56
|
+
*/
|
|
57
|
+
function getAuxiliaryModel(host) {
|
|
58
|
+
const shared = sharedAuxiliaryModel(host);
|
|
59
|
+
if (!shared) return void 0;
|
|
60
|
+
return modelRouteOverride(shared.snapshot().model ?? void 0);
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region ../dsh-plugin-kit/src/host-rpc.ts
|
|
64
|
+
/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
|
|
65
|
+
function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
|
|
66
|
+
const bodyLimit = options.maxBodyBytes ?? 65536;
|
|
67
|
+
if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
|
|
68
|
+
const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
|
|
69
|
+
return ctx.webServer.register({
|
|
70
|
+
kind: "prefix",
|
|
71
|
+
path: channel,
|
|
72
|
+
handler: async (req, res) => {
|
|
73
|
+
const reject = (status, error, override) => {
|
|
74
|
+
if (res.destroyed) return;
|
|
75
|
+
if (status === 413) res.writeHead(status, { connection: "close" });
|
|
76
|
+
else res.writeHead(status);
|
|
77
|
+
res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
if (typeof ctx.connection.requestRejection !== "function") {
|
|
81
|
+
reject(503);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const rejection = ctx.connection.requestRejection(req);
|
|
85
|
+
if (rejection !== void 0) {
|
|
86
|
+
reject(rejection);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
90
|
+
const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
|
|
91
|
+
if (req.method !== "POST" || !isEndpoint(endpoint)) {
|
|
92
|
+
reject(404);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
|
|
96
|
+
if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
|
|
97
|
+
reject(415);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const guarded = options.beforeBody?.(req, endpoint);
|
|
101
|
+
if (guarded) {
|
|
102
|
+
reject(guarded.status, void 0, guarded.body);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
res.on("close", () => {
|
|
107
|
+
if (!res.writableEnded) controller.abort();
|
|
108
|
+
});
|
|
109
|
+
const chunks = [];
|
|
110
|
+
let size = 0;
|
|
111
|
+
for await (const chunk of req) {
|
|
112
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
113
|
+
size += buffer.length;
|
|
114
|
+
if (size > bodyLimit) {
|
|
115
|
+
reject(413);
|
|
116
|
+
req.destroy();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
chunks.push(buffer);
|
|
120
|
+
}
|
|
121
|
+
let body;
|
|
122
|
+
try {
|
|
123
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
|
|
124
|
+
} catch {
|
|
125
|
+
reject(400);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await dispatch(endpoint, body, controller.signal, res);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
reject(500, error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Write the native Connection response envelope after a plugin has handled a request. */
|
|
136
|
+
function sendHostRpcResponse(response, rpcId, result, noStore = true) {
|
|
137
|
+
if (response.destroyed) return;
|
|
138
|
+
response.writeHead(200, {
|
|
139
|
+
"content-type": "application/json",
|
|
140
|
+
...noStore ? { "cache-control": "no-store" } : {}
|
|
141
|
+
});
|
|
142
|
+
response.end(JSON.stringify({
|
|
143
|
+
type: "server-response",
|
|
144
|
+
rpcId,
|
|
145
|
+
result
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
/** Safe host RPC registration. Missing requestRejection fails closed. */
|
|
149
|
+
function registerHostRpc(ctx, channel, call, options = {}) {
|
|
150
|
+
return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
|
|
151
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
152
|
+
res.writeHead(400);
|
|
153
|
+
res.end();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const request = body;
|
|
157
|
+
if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
|
|
158
|
+
res.writeHead(400);
|
|
159
|
+
res.end();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const raw = await call(endpoint, request.payload, signal);
|
|
163
|
+
const result = raw.ok ? raw : {
|
|
164
|
+
...raw,
|
|
165
|
+
error: {
|
|
166
|
+
details: {},
|
|
167
|
+
...raw.error
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
sendHostRpcResponse(res, request.rpcId, result);
|
|
171
|
+
}, options);
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region ../dsh-plugin-kit/src/model-settings.ts
|
|
175
|
+
const MODEL_SETTINGS_EVENT = "model-settings/collect";
|
|
176
|
+
const MODEL_SETTINGS_CHANGED_EVENT = "model-settings/changed";
|
|
177
|
+
/** Publish only after the owner's save commits. A failed observer must never
|
|
178
|
+
* turn an already committed write into an apparent save failure. */
|
|
179
|
+
function notifyModelSettingsChanged(ctx, ns, revision) {
|
|
180
|
+
try {
|
|
181
|
+
ctx.parallel(MODEL_SETTINGS_CHANGED_EVENT, {
|
|
182
|
+
ns,
|
|
183
|
+
revision
|
|
184
|
+
}).catch(() => {});
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
/** Cordis disposes the listener with its fiber. In-flight references also fail closed. */
|
|
188
|
+
function registerModelSettings(ctx, provider) {
|
|
189
|
+
let active = true;
|
|
190
|
+
const requireActive = (signal) => {
|
|
191
|
+
signal.throwIfAborted();
|
|
192
|
+
if (!active) throw new Error("Model settings owner unloaded; refresh and retry");
|
|
193
|
+
};
|
|
194
|
+
const guarded = {
|
|
195
|
+
ns: provider.ns,
|
|
196
|
+
title: provider.title,
|
|
197
|
+
instanceId: globalThis.crypto.randomUUID(),
|
|
198
|
+
async read(signal) {
|
|
199
|
+
requireActive(signal);
|
|
200
|
+
const value = await provider.read(signal);
|
|
201
|
+
requireActive(signal);
|
|
202
|
+
return value;
|
|
203
|
+
},
|
|
204
|
+
async write(path, route, revision, signal) {
|
|
205
|
+
requireActive(signal);
|
|
206
|
+
return provider.write(path, route, revision, signal);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const off = ctx.on(MODEL_SETTINGS_EVENT, (collect) => {
|
|
210
|
+
if (active) collect(guarded);
|
|
211
|
+
});
|
|
212
|
+
return () => {
|
|
213
|
+
active = false;
|
|
214
|
+
off();
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region ../dsh-plugin-kit/src/model-settings-client.ts
|
|
219
|
+
/** This stream carries invalidations only; ready is never a settings snapshot. */
|
|
220
|
+
function parseModelSettingsChangeFrame(value) {
|
|
221
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid model settings change frame");
|
|
222
|
+
const frame = value;
|
|
223
|
+
if (frame.kind === "ready" && Object.keys(frame).length === 1) return { kind: "ready" };
|
|
224
|
+
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 {
|
|
225
|
+
kind: "changed",
|
|
226
|
+
ns: frame.ns,
|
|
227
|
+
revision: frame.revision
|
|
228
|
+
};
|
|
229
|
+
throw new Error("Invalid model settings change frame");
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region ../dsh-plugin-kit/src/model-settings-changes.ts
|
|
233
|
+
var __runInitializers$1 = function(thisArg, initializers, value) {
|
|
234
|
+
var useValue = arguments.length > 2;
|
|
235
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
236
|
+
return useValue ? value : void 0;
|
|
237
|
+
};
|
|
238
|
+
var __esDecorate$1 = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
239
|
+
function accept(f) {
|
|
240
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
241
|
+
return f;
|
|
242
|
+
}
|
|
243
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
244
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
245
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
246
|
+
var _, done = false;
|
|
247
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
248
|
+
var context = {};
|
|
249
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
250
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
251
|
+
context.addInitializer = function(f) {
|
|
252
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
253
|
+
extraInitializers.push(accept(f || null));
|
|
254
|
+
};
|
|
255
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
256
|
+
get: descriptor.get,
|
|
257
|
+
set: descriptor.set
|
|
258
|
+
} : descriptor[key], context);
|
|
259
|
+
if (kind === "accessor") {
|
|
260
|
+
if (result === void 0) continue;
|
|
261
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
262
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
263
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
264
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
265
|
+
} else if (_ = accept(result)) {
|
|
266
|
+
if (kind === "field") initializers.unshift(_);
|
|
267
|
+
else descriptor[key] = _;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
271
|
+
done = true;
|
|
272
|
+
};
|
|
273
|
+
/** Attach first, then yield ready: commits during a consumer's baseline read
|
|
274
|
+
* remain queued. Coalescing revisions never invents a settings snapshot.
|
|
275
|
+
* `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */
|
|
276
|
+
async function* watchModelSettingsChanges(events, signal, ownerNs) {
|
|
277
|
+
signal.throwIfAborted();
|
|
278
|
+
const pending = /* @__PURE__ */ new Map();
|
|
279
|
+
let full = false;
|
|
280
|
+
let wake;
|
|
281
|
+
const changed = (ns, revision) => {
|
|
282
|
+
if (ownerNs !== void 0 && ns !== ownerNs) return;
|
|
283
|
+
let frame;
|
|
284
|
+
try {
|
|
285
|
+
frame = parseModelSettingsChangeFrame({
|
|
286
|
+
kind: "changed",
|
|
287
|
+
ns,
|
|
288
|
+
revision
|
|
289
|
+
});
|
|
290
|
+
} catch {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!full) {
|
|
294
|
+
if (!pending.has(ns) && pending.size >= 256) {
|
|
295
|
+
pending.clear();
|
|
296
|
+
full = true;
|
|
297
|
+
} else pending.set(ns, frame);
|
|
298
|
+
}
|
|
299
|
+
wake?.();
|
|
300
|
+
};
|
|
301
|
+
const disposers = [];
|
|
302
|
+
const cleanup = () => {
|
|
303
|
+
for (const off of disposers.splice(0).reverse()) off();
|
|
304
|
+
pending.clear();
|
|
305
|
+
};
|
|
306
|
+
const aborted = () => {
|
|
307
|
+
cleanup();
|
|
308
|
+
wake?.();
|
|
309
|
+
};
|
|
310
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
311
|
+
try {
|
|
312
|
+
disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)));
|
|
313
|
+
disposers.push(events.on("settings/document-updated", changed));
|
|
314
|
+
signal.throwIfAborted();
|
|
315
|
+
yield { kind: "ready" };
|
|
316
|
+
while (true) {
|
|
317
|
+
signal.throwIfAborted();
|
|
318
|
+
if (full) {
|
|
319
|
+
full = false;
|
|
320
|
+
yield { kind: "ready" };
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (pending.size) {
|
|
324
|
+
const [ns, frame] = pending.entries().next().value;
|
|
325
|
+
pending.delete(ns);
|
|
326
|
+
yield frame;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
await new Promise((resolve) => {
|
|
330
|
+
wake = resolve;
|
|
331
|
+
if (signal.aborted || full || pending.size) resolve();
|
|
332
|
+
});
|
|
333
|
+
wake = void 0;
|
|
334
|
+
}
|
|
335
|
+
} finally {
|
|
336
|
+
signal.removeEventListener("abort", aborted);
|
|
337
|
+
cleanup();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** One owner's invalidation stream, registered on the calling plugin fiber.
|
|
341
|
+
* Nothing here is stored in a module-level singleton. */
|
|
342
|
+
let ModelSettingsChangesController = (() => {
|
|
343
|
+
let _classSuper = TypertRemoteService;
|
|
344
|
+
let _instanceExtraInitializers = [];
|
|
345
|
+
let _watch_decorators;
|
|
346
|
+
return class ModelSettingsChangesController extends _classSuper {
|
|
347
|
+
static {
|
|
348
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
349
|
+
_watch_decorators = [Remote({ mode: "stream" })];
|
|
350
|
+
__esDecorate$1(this, null, _watch_decorators, {
|
|
351
|
+
kind: "method",
|
|
352
|
+
name: "watch",
|
|
353
|
+
static: false,
|
|
354
|
+
private: false,
|
|
355
|
+
access: {
|
|
356
|
+
has: (obj) => "watch" in obj,
|
|
357
|
+
get: (obj) => obj.watch
|
|
358
|
+
},
|
|
359
|
+
metadata: _metadata
|
|
360
|
+
}, null, _instanceExtraInitializers);
|
|
361
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
362
|
+
enumerable: true,
|
|
363
|
+
configurable: true,
|
|
364
|
+
writable: true,
|
|
365
|
+
value: _metadata
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
lifetime = (__runInitializers$1(this, _instanceExtraInitializers), new AbortController());
|
|
369
|
+
ownerNs;
|
|
370
|
+
constructor(ctx, identity, ownerNs) {
|
|
371
|
+
super(ctx, identity.service, { namespace: identity.namespace });
|
|
372
|
+
this.ownerNs = ownerNs;
|
|
373
|
+
ctx.effect(() => () => {
|
|
374
|
+
this.lifetime.abort(/* @__PURE__ */ new Error("Model settings sync owner unloaded"));
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
watch(signal) {
|
|
378
|
+
return watchModelSettingsChanges(this.ctx, AbortSignal.any([this.lifetime.signal, signal]), this.ownerNs);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
})();
|
|
382
|
+
/** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */
|
|
383
|
+
function createModelSettingsChanges(ctx, identity, ownerNs) {
|
|
384
|
+
return new ModelSettingsChangesController(ctx, identity, ownerNs);
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region ../dsh-plugin-kit/src/auxiliary-model.ts
|
|
388
|
+
const DOMAIN_NAME = "dsh_auxiliary";
|
|
389
|
+
const DOMAIN_VERSION = 1;
|
|
390
|
+
const TABLE = "settings";
|
|
391
|
+
const KEY = "defaults";
|
|
392
|
+
const INJECT = [
|
|
393
|
+
"storageDomain",
|
|
394
|
+
"webServer",
|
|
395
|
+
"connection"
|
|
396
|
+
];
|
|
397
|
+
/** Marker on the benign "released while the domain was still opening" error. */
|
|
398
|
+
const RECOVERABLE = Symbol.for("klarkxy.dsh.auxiliary-model.recoverable");
|
|
399
|
+
function recoverable(error) {
|
|
400
|
+
error[RECOVERABLE] = true;
|
|
401
|
+
return error;
|
|
402
|
+
}
|
|
403
|
+
async function openSharedDomain(scope) {
|
|
404
|
+
const [{ defineDomain, domainTable }, { z }] = await Promise.all([import("@deepseek-ai/dsh-storage-domain"), import("zod")]);
|
|
405
|
+
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");
|
|
406
|
+
const model = z.union([z.object({
|
|
407
|
+
provider: identifier,
|
|
408
|
+
model: identifier,
|
|
409
|
+
reasoningEffort: identifier.max(80).optional()
|
|
410
|
+
}).strict(), z.null()]);
|
|
411
|
+
return scope.storageDomain.open(defineDomain({
|
|
412
|
+
name: DOMAIN_NAME,
|
|
413
|
+
version: DOMAIN_VERSION,
|
|
414
|
+
tables: { [TABLE]: domainTable(z.object({
|
|
415
|
+
revision: z.number().int().nonnegative(),
|
|
416
|
+
model
|
|
417
|
+
}).strict()) }
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
/** Marks an optimistic-lock conflict so the RPC envelope can carry 'conflict'. */
|
|
421
|
+
function conflictError(message) {
|
|
422
|
+
return Object.assign(new Error(message), { conflict: true });
|
|
423
|
+
}
|
|
424
|
+
/** RPC-visible error classification; storage failures stay sanitized. */
|
|
425
|
+
function rpcFailure(error) {
|
|
426
|
+
if (error && typeof error === "object" && "conflict" in error) return {
|
|
427
|
+
ok: false,
|
|
428
|
+
error: {
|
|
429
|
+
code: "auxiliary-model/conflict",
|
|
430
|
+
message: error instanceof Error ? error.message : "Revision conflict"
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
if (error instanceof Error && /Invalid model route|valid expected revision|Unknown auxiliary model setting/.test(error.message)) return {
|
|
434
|
+
ok: false,
|
|
435
|
+
error: {
|
|
436
|
+
code: "auxiliary-model/rejected",
|
|
437
|
+
message: error.message
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
error: {
|
|
443
|
+
code: "auxiliary-model/store",
|
|
444
|
+
message: "Auxiliary model store request failed"
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Serialized optimistic-lock write; resolves after the durable put commits.
|
|
450
|
+
* Revision verification happens inside the queue: two writers carrying the
|
|
451
|
+
* same expectedRevision race for one slot and only the first commits.
|
|
452
|
+
* Notification fires only after commit, so a failed observer can never turn
|
|
453
|
+
* an already committed write into an apparent save failure.
|
|
454
|
+
*/
|
|
455
|
+
function setSharedModel(state, ctx, input, expectedRevision) {
|
|
456
|
+
if (!Number.isSafeInteger(expectedRevision)) return Promise.reject(conflictError("A valid expected revision is required; reload and retry"));
|
|
457
|
+
const row = input && typeof input === "object" && !Array.isArray(input) ? input : void 0;
|
|
458
|
+
const validField = (value) => typeof value === "string" && value === value.trim() && !/[\x00-\x1f\x7f]/.test(value);
|
|
459
|
+
if (!row || [
|
|
460
|
+
"provider",
|
|
461
|
+
"model",
|
|
462
|
+
"reasoningEffort"
|
|
463
|
+
].some((key) => row[key] !== void 0 && !validField(row[key]))) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
|
|
464
|
+
const normalized = normalizeModelRoute(row);
|
|
465
|
+
if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
|
|
466
|
+
const commit = state.tail.then(async () => {
|
|
467
|
+
if (state.closed || !state.domain) throw new Error("Auxiliary model settings are unloading; retry in a moment");
|
|
468
|
+
if (expectedRevision !== state.data.revision) throw conflictError("Auxiliary model settings changed; reload and retry");
|
|
469
|
+
const next = {
|
|
470
|
+
revision: state.data.revision + 1,
|
|
471
|
+
model: modelRouteOverride(normalized) ?? null
|
|
472
|
+
};
|
|
473
|
+
await state.domain.table(TABLE).put(KEY, {
|
|
474
|
+
revision: next.revision,
|
|
475
|
+
model: next.model
|
|
476
|
+
});
|
|
477
|
+
state.data = next;
|
|
478
|
+
notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision);
|
|
479
|
+
return next;
|
|
480
|
+
});
|
|
481
|
+
state.tail = commit.then(() => {}, () => {});
|
|
482
|
+
return commit;
|
|
483
|
+
}
|
|
484
|
+
function initScope(state, ctx, scope) {
|
|
485
|
+
const generation = ++state.generation;
|
|
486
|
+
let disposed = false;
|
|
487
|
+
scope.effect(() => () => {
|
|
488
|
+
disposed = true;
|
|
489
|
+
});
|
|
490
|
+
const stale = () => state.closed || disposed || state.generation !== generation;
|
|
491
|
+
const run = (async () => {
|
|
492
|
+
state.available = false;
|
|
493
|
+
const domain = await openSharedDomain(scope);
|
|
494
|
+
if (stale()) {
|
|
495
|
+
await domain.close();
|
|
496
|
+
throw recoverable(/* @__PURE__ */ new Error("Auxiliary model store released during open"));
|
|
497
|
+
}
|
|
498
|
+
state.domain = domain;
|
|
499
|
+
try {
|
|
500
|
+
scope.effect(() => () => domain.close());
|
|
501
|
+
} catch (error) {
|
|
502
|
+
await domain.close();
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
const stored = domain.table(TABLE).get(KEY);
|
|
506
|
+
if (stale()) {
|
|
507
|
+
await domain.close();
|
|
508
|
+
throw recoverable(/* @__PURE__ */ new Error("Auxiliary model store superseded during open"));
|
|
509
|
+
}
|
|
510
|
+
state.data = {
|
|
511
|
+
revision: stored?.revision ?? 0,
|
|
512
|
+
model: stored?.model ?? null
|
|
513
|
+
};
|
|
514
|
+
state.error = void 0;
|
|
515
|
+
state.available = true;
|
|
516
|
+
scope.effect(() => registerModelSettings(scope, {
|
|
517
|
+
ns: AUXILIARY_MODEL_NS,
|
|
518
|
+
title: {
|
|
519
|
+
zh: "辅助模型",
|
|
520
|
+
en: "Auxiliary model"
|
|
521
|
+
},
|
|
522
|
+
read(signal) {
|
|
523
|
+
signal.throwIfAborted();
|
|
524
|
+
const { revision, model } = state.data;
|
|
525
|
+
return {
|
|
526
|
+
revision,
|
|
527
|
+
fields: [{
|
|
528
|
+
path: ["model"],
|
|
529
|
+
label: {
|
|
530
|
+
zh: "全局辅助模型",
|
|
531
|
+
en: "Global auxiliary model"
|
|
532
|
+
},
|
|
533
|
+
current: model ?? void 0,
|
|
534
|
+
allowFollow: true,
|
|
535
|
+
followLabel: {
|
|
536
|
+
zh: "当前会话模型",
|
|
537
|
+
en: "Current conversation model"
|
|
538
|
+
}
|
|
539
|
+
}]
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
async write(path, route, expectedRevision, signal) {
|
|
543
|
+
signal.throwIfAborted();
|
|
544
|
+
if (path.length !== 1 || path[0] !== "model") throw new Error("Unknown auxiliary model setting");
|
|
545
|
+
return setSharedModel(state, ctx, route, expectedRevision);
|
|
546
|
+
}
|
|
547
|
+
}));
|
|
548
|
+
scope.effect(() => registerHostRpc(scope, AUXILIARY_MODEL_CHANNEL, async (endpoint, payload, signal) => {
|
|
549
|
+
signal.throwIfAborted();
|
|
550
|
+
if (state.closed) return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: {
|
|
553
|
+
code: "auxiliary-model/unloading",
|
|
554
|
+
message: "Auxiliary model settings are unloading; retry in a moment"
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
if (!state.available) return state.error ? {
|
|
558
|
+
ok: false,
|
|
559
|
+
error: {
|
|
560
|
+
code: "auxiliary-model/store",
|
|
561
|
+
message: "Auxiliary model store request failed"
|
|
562
|
+
}
|
|
563
|
+
} : {
|
|
564
|
+
ok: false,
|
|
565
|
+
error: {
|
|
566
|
+
code: "auxiliary-model/unavailable",
|
|
567
|
+
message: "Auxiliary model settings are unavailable; retry in a moment"
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
if (endpoint === "defaults.get") {
|
|
571
|
+
const { revision, model } = state.data;
|
|
572
|
+
return {
|
|
573
|
+
ok: true,
|
|
574
|
+
value: {
|
|
575
|
+
revision,
|
|
576
|
+
model
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (endpoint === "defaults.set") try {
|
|
581
|
+
const body = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
582
|
+
const value = await setSharedModel(state, ctx, body.model, body.expectedRevision);
|
|
583
|
+
signal.throwIfAborted();
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
value
|
|
587
|
+
};
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (signal.aborted) throw error;
|
|
590
|
+
return rpcFailure(error);
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: {
|
|
595
|
+
code: "auxiliary-model/unknown-endpoint",
|
|
596
|
+
message: `Unknown endpoint: ${endpoint}`
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}, { isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint) }));
|
|
600
|
+
createModelSettingsChanges(scope, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS);
|
|
601
|
+
scope.effect(() => () => {
|
|
602
|
+
if (state.generation === generation) {
|
|
603
|
+
state.available = false;
|
|
604
|
+
state.domain = void 0;
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
})().catch((error) => {
|
|
608
|
+
if (error && typeof error === "object") error.auxGeneration = generation;
|
|
609
|
+
if (state.generation === generation) {
|
|
610
|
+
state.available = false;
|
|
611
|
+
state.error = error;
|
|
612
|
+
}
|
|
613
|
+
throw error;
|
|
614
|
+
});
|
|
615
|
+
run.catch(() => {});
|
|
616
|
+
const armed = run.then(() => {});
|
|
617
|
+
armed.catch(() => {});
|
|
618
|
+
state.ready = armed;
|
|
619
|
+
return run;
|
|
620
|
+
}
|
|
621
|
+
function teardown(state, ctx) {
|
|
622
|
+
if (!state.closing) {
|
|
623
|
+
state.closed = true;
|
|
624
|
+
const root = ctx.root;
|
|
625
|
+
state.closing = (async () => {
|
|
626
|
+
await state.scopeInit?.catch(() => {});
|
|
627
|
+
await state.tail.catch(() => {});
|
|
628
|
+
try {
|
|
629
|
+
await state.fiber?.dispose();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
try {
|
|
632
|
+
ctx.logger?.warn("auxiliary-model: fiber dispose failed", error);
|
|
633
|
+
} catch {}
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
if (root.get("dshAuxiliaryModel", false) === state) state.stopUnprovide?.();
|
|
637
|
+
} catch {}
|
|
638
|
+
state.stopUnprovide = void 0;
|
|
639
|
+
})();
|
|
640
|
+
}
|
|
641
|
+
return state.closing;
|
|
642
|
+
}
|
|
643
|
+
function createSharedState(ctx) {
|
|
644
|
+
const root = ctx.root;
|
|
645
|
+
const state = {
|
|
646
|
+
users: 0,
|
|
647
|
+
closed: false,
|
|
648
|
+
available: false,
|
|
649
|
+
generation: 0,
|
|
650
|
+
tail: Promise.resolve(),
|
|
651
|
+
data: {
|
|
652
|
+
revision: 0,
|
|
653
|
+
model: null
|
|
654
|
+
},
|
|
655
|
+
snapshot() {
|
|
656
|
+
if (this.error) throw this.error;
|
|
657
|
+
if (!this.available) throw new Error("Auxiliary model store is unavailable; retry in a moment");
|
|
658
|
+
return this.data;
|
|
659
|
+
},
|
|
660
|
+
ready: Promise.resolve(),
|
|
661
|
+
teardown() {
|
|
662
|
+
return teardown(this, ctx);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
const inactive = INJECT.filter((service) => {
|
|
666
|
+
try {
|
|
667
|
+
return root.get(service, true) === void 0;
|
|
668
|
+
} catch {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
if (inactive.length) throw new Error(`retainAuxiliaryModel requires active ${inactive.join(", ")}; declare them in inject`);
|
|
673
|
+
state.fiber = root.inject(INJECT, (scope) => {
|
|
674
|
+
state.scopeInit = initScope(state, ctx, scope);
|
|
675
|
+
});
|
|
676
|
+
state.ready = Promise.resolve(state.fiber).then(() => {
|
|
677
|
+
if (!state.scopeInit) throw recoverable(/* @__PURE__ */ new Error("Auxiliary model owner fiber settled without initializing its scope"));
|
|
678
|
+
return state.scopeInit;
|
|
679
|
+
});
|
|
680
|
+
state.ready.catch((error) => {
|
|
681
|
+
const tag = error.auxGeneration;
|
|
682
|
+
if (tag !== void 0 && tag !== state.generation) return;
|
|
683
|
+
state.error = state.error ?? error;
|
|
684
|
+
state.closed = true;
|
|
685
|
+
state.teardown();
|
|
686
|
+
});
|
|
687
|
+
state.stopUnprovide = root.provide(AUXILIARY_MODEL_SERVICE, state);
|
|
688
|
+
return state;
|
|
689
|
+
}
|
|
690
|
+
async function obtainShared(ctx) {
|
|
691
|
+
const root = ctx.root;
|
|
692
|
+
for (;;) {
|
|
693
|
+
const existing = root.get(AUXILIARY_MODEL_SERVICE, false);
|
|
694
|
+
if (existing) {
|
|
695
|
+
if (!existing.closed) return existing;
|
|
696
|
+
await existing.closing;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
return createSharedState(ctx);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Retain the shared auxiliary-model store for the calling plugin.
|
|
704
|
+
*
|
|
705
|
+
* Resolves once storage is open and the read/write surfaces are live, with a
|
|
706
|
+
* release function; register it as `ctx.effect(() => release)` so the store
|
|
707
|
+
* unloads with the plugin. The first participant opens the shared owner
|
|
708
|
+
* fiber; `await release()` drains pending writes and fully tears the owner
|
|
709
|
+
* down when the last participant leaves. Requires `storageDomain`,
|
|
710
|
+
* `webServer` and `connection` to exist on the host.
|
|
711
|
+
*
|
|
712
|
+
* Concurrent retainers share one owner; a retain arriving during teardown
|
|
713
|
+
* waits for the closing owner and then reinstalls — it never inherits a
|
|
714
|
+
* half-torn-down state.
|
|
715
|
+
*/
|
|
716
|
+
async function retainAuxiliaryModel(ctx) {
|
|
717
|
+
for (;;) {
|
|
718
|
+
const state = await obtainShared(ctx);
|
|
719
|
+
await state.ready;
|
|
720
|
+
if (state.closed) continue;
|
|
721
|
+
state.users += 1;
|
|
722
|
+
let released = false;
|
|
723
|
+
return async () => {
|
|
724
|
+
if (released) return;
|
|
725
|
+
released = true;
|
|
726
|
+
state.users -= 1;
|
|
727
|
+
if (state.users <= 0) await state.teardown();
|
|
728
|
+
else await state.tail.catch(() => {});
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.2.1-rc.1_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
|
|
734
|
+
/**
|
|
735
|
+
* Lazily scanned view of one JSON object's top-level fields, built from text
|
|
736
|
+
* that may still be streaming or from an already parsed object. Nothing is
|
|
737
|
+
* scanned until a reader asks; the view remembers every question it answered
|
|
738
|
+
* and reports changed answers when the owner refreshes for publication.
|
|
739
|
+
* Used for model tool-call arguments: a row reads the fields it
|
|
740
|
+
* cares about at whatever granularity it displays, at every stage of the call.
|
|
741
|
+
* @module @deepseek-ai/dsh-util-values/src/partial-json
|
|
742
|
+
*/
|
|
743
|
+
const SIMPLE_ESCAPES = {
|
|
744
|
+
"\"": "\"",
|
|
745
|
+
"\\": "\\",
|
|
746
|
+
"/": "/",
|
|
747
|
+
b: "\b",
|
|
748
|
+
f: "\f",
|
|
749
|
+
n: "\n",
|
|
750
|
+
r: "\r",
|
|
751
|
+
t: " "
|
|
752
|
+
};
|
|
753
|
+
const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
|
|
754
|
+
function isWhitespace(c) {
|
|
755
|
+
return c === " " || c === "\n" || c === "\r" || c === " ";
|
|
756
|
+
}
|
|
757
|
+
function isHex(c) {
|
|
758
|
+
return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
|
|
759
|
+
}
|
|
760
|
+
(class PartialArguments {
|
|
761
|
+
/** The view of a call with no arguments available. */
|
|
762
|
+
static EMPTY = PartialArguments.fromObject({});
|
|
763
|
+
/**
|
|
764
|
+
* View finished argument text without scanning it until a reader asks.
|
|
765
|
+
* @param text - the complete argument JSON text.
|
|
766
|
+
* @returns a sealed view.
|
|
767
|
+
*/
|
|
768
|
+
static fromText(text) {
|
|
769
|
+
const view = new PartialArguments();
|
|
770
|
+
view.append(text);
|
|
771
|
+
view.sealed = true;
|
|
772
|
+
return view;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* View an already parsed argument payload, such as a PTC dispatch object.
|
|
776
|
+
* @param value - the parsed argument value.
|
|
777
|
+
* @returns a sealed view; a non-object payload has no fields.
|
|
778
|
+
*/
|
|
779
|
+
static fromObject(value) {
|
|
780
|
+
const view = new PartialArguments();
|
|
781
|
+
view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
782
|
+
view.sealed = true;
|
|
783
|
+
return view;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* The source: text so far or a parsed object, plus whether it can still grow.
|
|
787
|
+
* These are the only enumerable fields, so two views over the same source
|
|
788
|
+
* compare equal structurally however far each has been read.
|
|
789
|
+
*/
|
|
790
|
+
chunks = [];
|
|
791
|
+
object;
|
|
792
|
+
sealed = false;
|
|
793
|
+
#ends = [];
|
|
794
|
+
#size = 0;
|
|
795
|
+
#consumed = 0;
|
|
796
|
+
#mode = "root";
|
|
797
|
+
#escape = false;
|
|
798
|
+
#keyStart = 0;
|
|
799
|
+
#keyEscaped = false;
|
|
800
|
+
#key = "";
|
|
801
|
+
#current = null;
|
|
802
|
+
#nestedEnds = [];
|
|
803
|
+
#nestedInString = false;
|
|
804
|
+
#invalidAt;
|
|
805
|
+
#invalidValue = false;
|
|
806
|
+
#entries = /* @__PURE__ */ new Map();
|
|
807
|
+
#order = [];
|
|
808
|
+
#reads = /* @__PURE__ */ new Map();
|
|
809
|
+
/** Whether this view rejects further appends; does not scan text or register reads. */
|
|
810
|
+
get isSealed() {
|
|
811
|
+
return this.sealed;
|
|
812
|
+
}
|
|
813
|
+
/** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
|
|
814
|
+
get invalid() {
|
|
815
|
+
this.scan();
|
|
816
|
+
return this.#mode === "invalid" || this.#invalidValue;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Retain streamed argument text without scanning or comparing observed answers.
|
|
820
|
+
* @param fragment - the text following every fragment appended before.
|
|
821
|
+
*/
|
|
822
|
+
append(fragment) {
|
|
823
|
+
if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
|
|
824
|
+
if (fragment.length === 0) return;
|
|
825
|
+
this.chunks.push(fragment);
|
|
826
|
+
this.#size += fragment.length;
|
|
827
|
+
this.#ends.push(this.#size);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Reconcile a streamed prefix with authoritative complete text without joining the fragments.
|
|
831
|
+
* @param text - the final argument text, which replaces missing or conflicting deltas.
|
|
832
|
+
* @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
|
|
833
|
+
*/
|
|
834
|
+
settle(text) {
|
|
835
|
+
if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
|
|
836
|
+
let offset = 0;
|
|
837
|
+
for (const chunk of this.chunks) {
|
|
838
|
+
if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
|
|
839
|
+
offset += chunk.length;
|
|
840
|
+
}
|
|
841
|
+
this.chunks = text.length === 0 ? [] : [text];
|
|
842
|
+
this.#ends = text.length === 0 ? [] : [text.length];
|
|
843
|
+
this.sealed = true;
|
|
844
|
+
return this;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Compare observed answers and advance their publication baseline. Unread views remain unscanned.
|
|
848
|
+
* @returns whether any observed answer changed since its first read or the preceding refresh.
|
|
849
|
+
*/
|
|
850
|
+
refresh() {
|
|
851
|
+
if (this.#reads.size === 0) return false;
|
|
852
|
+
this.scan();
|
|
853
|
+
let changed = false;
|
|
854
|
+
let completions = false;
|
|
855
|
+
for (const read of this.#reads.values()) {
|
|
856
|
+
if (read.completion) {
|
|
857
|
+
completions = true;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
changed = this.refreshRead(read) || changed;
|
|
861
|
+
}
|
|
862
|
+
if (completions) {
|
|
863
|
+
for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
|
|
864
|
+
}
|
|
865
|
+
if (this.sealed) this.#reads.clear();
|
|
866
|
+
return changed;
|
|
867
|
+
}
|
|
868
|
+
refreshRead(read) {
|
|
869
|
+
const now = read.answer();
|
|
870
|
+
if (Object.is(now, read.last)) return false;
|
|
871
|
+
read.last = now;
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Check whether no further fields can arrive.
|
|
876
|
+
* @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
|
|
877
|
+
*/
|
|
878
|
+
closed() {
|
|
879
|
+
return this.remember("closed", "", () => this.closedNow());
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* List discovered fields in first-appearance order.
|
|
883
|
+
* @returns top-level keys seen so far, in first-appearance order.
|
|
884
|
+
*/
|
|
885
|
+
keys() {
|
|
886
|
+
return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Check whether a top-level field has appeared.
|
|
890
|
+
* @param key - argument name.
|
|
891
|
+
* @returns whether the field has appeared (a string opened or another value began).
|
|
892
|
+
*/
|
|
893
|
+
has(key) {
|
|
894
|
+
return this.remember("has", key, () => this.hasNow(key));
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Check whether a field's closing delimiter has arrived, without validating its contents.
|
|
898
|
+
* @param key - argument name.
|
|
899
|
+
* @returns whether its delimiter arrived and no content reader has reported an error for this value.
|
|
900
|
+
*/
|
|
901
|
+
complete(key) {
|
|
902
|
+
return this.remember("complete", key, () => this.completeNow(key));
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Read string length without materializing its text.
|
|
906
|
+
* @param key - argument name.
|
|
907
|
+
* @param options - change granularity for a streaming string.
|
|
908
|
+
* @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
|
|
909
|
+
*/
|
|
910
|
+
stringLength(key, options) {
|
|
911
|
+
const step = Math.max(1, Math.floor(options?.step ?? 1));
|
|
912
|
+
const offset = options?.offset ?? 0;
|
|
913
|
+
return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Check a string against a decoded UTF-16 length limit without materializing it.
|
|
917
|
+
* @param key - argument name.
|
|
918
|
+
* @param maxLength - decoded UTF-16 limit, floored to at least zero.
|
|
919
|
+
* @returns whether the string is longer than the limit; false when absent or not a string.
|
|
920
|
+
*/
|
|
921
|
+
stringExceeds(key, maxLength) {
|
|
922
|
+
const limit = Math.max(0, Math.floor(maxLength));
|
|
923
|
+
return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Read a decoded string, including a streaming prefix.
|
|
927
|
+
* @param key - argument name.
|
|
928
|
+
* @returns the string field's decoded text so far; undefined when absent or not a string.
|
|
929
|
+
*/
|
|
930
|
+
text(key) {
|
|
931
|
+
return this.remember("text", key, () => this.textNow(key));
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Read at most the first decoded UTF-16 units of a string.
|
|
935
|
+
* @param key - argument name.
|
|
936
|
+
* @param maxLength - maximum decoded UTF-16 length, floored to at least one.
|
|
937
|
+
* @returns the bounded string prefix; undefined when absent or not a string.
|
|
938
|
+
*/
|
|
939
|
+
textPrefix(key, maxLength) {
|
|
940
|
+
const limit = Math.max(1, Math.floor(maxLength));
|
|
941
|
+
return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Read a completed non-string argument.
|
|
945
|
+
* @param key - argument name.
|
|
946
|
+
* @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
|
|
947
|
+
*/
|
|
948
|
+
value(key) {
|
|
949
|
+
return this.remember("value", key, () => this.valueNow(key));
|
|
950
|
+
}
|
|
951
|
+
/** Answer a question and, on a streaming view, remember it for change detection. */
|
|
952
|
+
remember(kind, key, read, comparison) {
|
|
953
|
+
this.scan();
|
|
954
|
+
const result = read();
|
|
955
|
+
if (!this.sealed) {
|
|
956
|
+
const id = `${kind}/${key}`;
|
|
957
|
+
if (!this.#reads.has(id)) this.#reads.set(id, {
|
|
958
|
+
completion: kind === "complete",
|
|
959
|
+
answer: comparison === void 0 ? read : () => comparison(read()),
|
|
960
|
+
last: comparison === void 0 ? result : comparison(result)
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
closedNow() {
|
|
966
|
+
return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
|
|
967
|
+
}
|
|
968
|
+
keysNow() {
|
|
969
|
+
return this.object === void 0 ? this.#order : Object.keys(this.object);
|
|
970
|
+
}
|
|
971
|
+
hasNow(key) {
|
|
972
|
+
return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
|
|
973
|
+
}
|
|
974
|
+
completeNow(key) {
|
|
975
|
+
if (this.object !== void 0) return Object.hasOwn(this.object, key);
|
|
976
|
+
const entry = this.#entries.get(key);
|
|
977
|
+
return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
|
|
978
|
+
}
|
|
979
|
+
lengthNow(key, limit = Number.POSITIVE_INFINITY) {
|
|
980
|
+
if (this.object !== void 0) {
|
|
981
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
982
|
+
return typeof field === "string" ? field.length : void 0;
|
|
983
|
+
}
|
|
984
|
+
const entry = this.#entries.get(key);
|
|
985
|
+
if (entry?.kind !== "string") return void 0;
|
|
986
|
+
if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
|
|
987
|
+
const read = entry.length ??= {
|
|
988
|
+
at: entry.start,
|
|
989
|
+
length: 0,
|
|
990
|
+
text: ""
|
|
991
|
+
};
|
|
992
|
+
this.readString(entry, read, limit, false);
|
|
993
|
+
return read.length;
|
|
994
|
+
}
|
|
995
|
+
textNow(key) {
|
|
996
|
+
if (this.object !== void 0) {
|
|
997
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
998
|
+
return typeof field === "string" ? field : void 0;
|
|
999
|
+
}
|
|
1000
|
+
const entry = this.#entries.get(key);
|
|
1001
|
+
if (entry?.kind !== "string") return void 0;
|
|
1002
|
+
if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
|
|
1003
|
+
let text;
|
|
1004
|
+
try {
|
|
1005
|
+
text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
|
|
1006
|
+
} catch (_error) {}
|
|
1007
|
+
if (text !== void 0) entry.text = {
|
|
1008
|
+
at: entry.end,
|
|
1009
|
+
length: text.length,
|
|
1010
|
+
text
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const read = entry.text ??= {
|
|
1014
|
+
at: entry.start,
|
|
1015
|
+
length: 0,
|
|
1016
|
+
text: ""
|
|
1017
|
+
};
|
|
1018
|
+
this.readString(entry, read, Number.POSITIVE_INFINITY, true);
|
|
1019
|
+
return read.text;
|
|
1020
|
+
}
|
|
1021
|
+
textPrefixNow(key, maxLength) {
|
|
1022
|
+
if (this.object !== void 0) {
|
|
1023
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
1024
|
+
return typeof field === "string" ? field.slice(0, maxLength) : void 0;
|
|
1025
|
+
}
|
|
1026
|
+
const entry = this.#entries.get(key);
|
|
1027
|
+
if (entry?.kind !== "string") return void 0;
|
|
1028
|
+
const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
|
|
1029
|
+
let read = prefixes.get(maxLength);
|
|
1030
|
+
if (read === void 0) {
|
|
1031
|
+
read = {
|
|
1032
|
+
at: entry.start,
|
|
1033
|
+
length: 0,
|
|
1034
|
+
text: ""
|
|
1035
|
+
};
|
|
1036
|
+
prefixes.set(maxLength, read);
|
|
1037
|
+
}
|
|
1038
|
+
this.readString(entry, read, maxLength, true);
|
|
1039
|
+
return read.text;
|
|
1040
|
+
}
|
|
1041
|
+
valueNow(key) {
|
|
1042
|
+
if (this.object !== void 0) {
|
|
1043
|
+
if (!Object.hasOwn(this.object, key)) return void 0;
|
|
1044
|
+
const field = this.object[key];
|
|
1045
|
+
return typeof field === "string" ? void 0 : field;
|
|
1046
|
+
}
|
|
1047
|
+
const entry = this.#entries.get(key);
|
|
1048
|
+
if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
|
|
1049
|
+
if (entry.parsed === void 0) try {
|
|
1050
|
+
entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
|
|
1051
|
+
} catch (_error) {
|
|
1052
|
+
entry.invalid = true;
|
|
1053
|
+
this.#invalidValue = true;
|
|
1054
|
+
}
|
|
1055
|
+
return entry.parsed;
|
|
1056
|
+
}
|
|
1057
|
+
chunkAt(at) {
|
|
1058
|
+
let low = 0;
|
|
1059
|
+
let high = this.#ends.length;
|
|
1060
|
+
while (low < high) {
|
|
1061
|
+
const mid = low + high >>> 1;
|
|
1062
|
+
if (this.#ends[mid] <= at) low = mid + 1;
|
|
1063
|
+
else high = mid;
|
|
1064
|
+
}
|
|
1065
|
+
return low;
|
|
1066
|
+
}
|
|
1067
|
+
/** Materialize only a requested range, never the cumulative source. */
|
|
1068
|
+
slice(start, end) {
|
|
1069
|
+
if (start >= end) return "";
|
|
1070
|
+
const first = this.chunkAt(start);
|
|
1071
|
+
const last = this.chunkAt(end - 1);
|
|
1072
|
+
const base = first === 0 ? 0 : this.#ends[first - 1];
|
|
1073
|
+
if (first === last) return this.chunks[first].slice(start - base, end - base);
|
|
1074
|
+
const parts = [this.chunks[first].slice(start - base)];
|
|
1075
|
+
for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
|
|
1076
|
+
parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
|
|
1077
|
+
return parts.join("");
|
|
1078
|
+
}
|
|
1079
|
+
readString(entry, read, limit, materialize) {
|
|
1080
|
+
const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
|
|
1081
|
+
if (!entry.needsDecoding) {
|
|
1082
|
+
const length = Math.min(end - read.at, limit - read.length);
|
|
1083
|
+
if (length <= 0) return;
|
|
1084
|
+
if (materialize) read.text += this.slice(read.at, read.at + length);
|
|
1085
|
+
read.at += length;
|
|
1086
|
+
read.length += length;
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
let chunkIndex = this.chunkAt(read.at);
|
|
1090
|
+
while (read.at < end && read.length < limit) {
|
|
1091
|
+
const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
|
|
1092
|
+
const chunk = this.chunks[chunkIndex];
|
|
1093
|
+
const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
|
|
1094
|
+
const boundary = remaining.search(CONTENT_ESCAPE);
|
|
1095
|
+
const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
|
|
1096
|
+
if (length > 0) {
|
|
1097
|
+
if (materialize) read.text += remaining.slice(0, length);
|
|
1098
|
+
read.at += length;
|
|
1099
|
+
read.length += length;
|
|
1100
|
+
if (read.at === base + chunk.length) chunkIndex++;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
|
|
1104
|
+
let decoded;
|
|
1105
|
+
let width = 2;
|
|
1106
|
+
if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
|
|
1107
|
+
if (remaining[0] === "\\" && type === "u") {
|
|
1108
|
+
const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
|
|
1109
|
+
let valid = true;
|
|
1110
|
+
for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
|
|
1111
|
+
if (valid) {
|
|
1112
|
+
if (hex.length < 4 && entry.end < 0) return;
|
|
1113
|
+
if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
|
|
1114
|
+
}
|
|
1115
|
+
width = 6;
|
|
1116
|
+
} else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
|
|
1117
|
+
if (decoded === void 0) {
|
|
1118
|
+
entry.invalidAt = read.at;
|
|
1119
|
+
this.#invalidValue = true;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (materialize) read.text += decoded;
|
|
1123
|
+
read.length++;
|
|
1124
|
+
read.at += width;
|
|
1125
|
+
while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/** Locate new field ranges without decoding or parsing their contents. */
|
|
1129
|
+
scan() {
|
|
1130
|
+
if (this.object !== void 0 || this.#consumed === this.#size) return;
|
|
1131
|
+
for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
|
|
1132
|
+
const pending = this.chunks[i];
|
|
1133
|
+
const base = i === 0 ? 0 : this.#ends[i - 1];
|
|
1134
|
+
for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
|
|
1135
|
+
if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
|
|
1136
|
+
const end = this.stringBoundary(pending, index);
|
|
1137
|
+
this.#consumed += end - index;
|
|
1138
|
+
index = end;
|
|
1139
|
+
if (index === pending.length) break;
|
|
1140
|
+
}
|
|
1141
|
+
this.step(pending[index], this.#consumed);
|
|
1142
|
+
this.#consumed++;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
/** Only raw quotes and their preceding backslash runs can terminate a string. */
|
|
1147
|
+
stringBoundary(fragment, start) {
|
|
1148
|
+
let at = start;
|
|
1149
|
+
while (true) {
|
|
1150
|
+
const quote = fragment.indexOf("\"", at);
|
|
1151
|
+
const end = quote < 0 ? fragment.length : quote;
|
|
1152
|
+
if (this.#mode === "string") {
|
|
1153
|
+
const entry = this.#current;
|
|
1154
|
+
if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
|
|
1155
|
+
}
|
|
1156
|
+
let slashStart = end;
|
|
1157
|
+
while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
|
|
1158
|
+
const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
|
|
1159
|
+
this.#escape = quote < 0 && escaped;
|
|
1160
|
+
if (quote < 0 || !escaped) return end;
|
|
1161
|
+
at = quote + 1;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
step(c, at) {
|
|
1165
|
+
switch (this.#mode) {
|
|
1166
|
+
case "root":
|
|
1167
|
+
if (isWhitespace(c)) return;
|
|
1168
|
+
if (c === "{") {
|
|
1169
|
+
this.#mode = "key-or-end";
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
this.fail();
|
|
1173
|
+
return;
|
|
1174
|
+
case "key-or-end":
|
|
1175
|
+
if (isWhitespace(c)) return;
|
|
1176
|
+
if (c === "}") {
|
|
1177
|
+
this.#mode = "closed";
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (c === "\"") {
|
|
1181
|
+
this.beginKey(at);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
this.fail();
|
|
1185
|
+
return;
|
|
1186
|
+
case "key-only":
|
|
1187
|
+
if (isWhitespace(c)) return;
|
|
1188
|
+
if (c === "\"") {
|
|
1189
|
+
this.beginKey(at);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
this.fail();
|
|
1193
|
+
return;
|
|
1194
|
+
case "key":
|
|
1195
|
+
this.stepKey(c, at);
|
|
1196
|
+
return;
|
|
1197
|
+
case "colon":
|
|
1198
|
+
if (isWhitespace(c)) return;
|
|
1199
|
+
if (c === ":") {
|
|
1200
|
+
this.#mode = "value";
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
this.fail();
|
|
1204
|
+
return;
|
|
1205
|
+
case "value":
|
|
1206
|
+
this.beginValue(c, at);
|
|
1207
|
+
return;
|
|
1208
|
+
case "string": {
|
|
1209
|
+
const entry = this.#current;
|
|
1210
|
+
entry.end = at;
|
|
1211
|
+
this.#current = null;
|
|
1212
|
+
this.#mode = "comma-or-end";
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
case "scalar":
|
|
1216
|
+
this.stepScalar(c, at);
|
|
1217
|
+
return;
|
|
1218
|
+
case "nested":
|
|
1219
|
+
this.stepNested(c, at);
|
|
1220
|
+
return;
|
|
1221
|
+
case "comma-or-end":
|
|
1222
|
+
if (isWhitespace(c)) return;
|
|
1223
|
+
if (c === ",") {
|
|
1224
|
+
this.#mode = "key-only";
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (c === "}") {
|
|
1228
|
+
this.#mode = "closed";
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
this.fail();
|
|
1232
|
+
return;
|
|
1233
|
+
case "closed":
|
|
1234
|
+
if (isWhitespace(c)) return;
|
|
1235
|
+
this.fail();
|
|
1236
|
+
return;
|
|
1237
|
+
/* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
|
|
1238
|
+
case "invalid": return;
|
|
1239
|
+
/* v8 ignore next 2 -- Every scanner mode has a handler above. */
|
|
1240
|
+
default: assertNever(this.#mode);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
fail() {
|
|
1244
|
+
this.#invalidAt = this.#consumed;
|
|
1245
|
+
this.#mode = "invalid";
|
|
1246
|
+
this.#current = null;
|
|
1247
|
+
}
|
|
1248
|
+
beginKey(at) {
|
|
1249
|
+
this.#mode = "key";
|
|
1250
|
+
this.#keyStart = at + 1;
|
|
1251
|
+
this.#keyEscaped = false;
|
|
1252
|
+
this.#escape = false;
|
|
1253
|
+
}
|
|
1254
|
+
stepKey(c, at) {
|
|
1255
|
+
if (c < " ") {
|
|
1256
|
+
this.fail();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
if (this.#escape) {
|
|
1260
|
+
this.#escape = false;
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (c === "\\") {
|
|
1264
|
+
this.#escape = true;
|
|
1265
|
+
this.#keyEscaped = true;
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (c !== "\"") return;
|
|
1269
|
+
const raw = this.slice(this.#keyStart, at);
|
|
1270
|
+
if (this.#keyEscaped) try {
|
|
1271
|
+
this.#key = JSON.parse(`"${raw}"`);
|
|
1272
|
+
} catch (_error) {
|
|
1273
|
+
this.fail();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
else this.#key = raw;
|
|
1277
|
+
this.#mode = "colon";
|
|
1278
|
+
}
|
|
1279
|
+
open(entry) {
|
|
1280
|
+
if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
|
|
1281
|
+
this.#entries.set(this.#key, entry);
|
|
1282
|
+
this.#current = entry;
|
|
1283
|
+
}
|
|
1284
|
+
beginValue(c, at) {
|
|
1285
|
+
if (isWhitespace(c)) return;
|
|
1286
|
+
if (c === "\"") {
|
|
1287
|
+
this.open({
|
|
1288
|
+
kind: "string",
|
|
1289
|
+
start: at + 1,
|
|
1290
|
+
end: -1,
|
|
1291
|
+
needsDecoding: false,
|
|
1292
|
+
invalidAt: void 0,
|
|
1293
|
+
length: void 0,
|
|
1294
|
+
text: void 0,
|
|
1295
|
+
prefixes: void 0
|
|
1296
|
+
});
|
|
1297
|
+
this.#escape = false;
|
|
1298
|
+
this.#mode = "string";
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (c === "}" || c === "," || c === ":" || c === "]") {
|
|
1302
|
+
this.fail();
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
this.open({
|
|
1306
|
+
kind: "value",
|
|
1307
|
+
start: at,
|
|
1308
|
+
end: -1,
|
|
1309
|
+
parsed: void 0,
|
|
1310
|
+
invalid: false
|
|
1311
|
+
});
|
|
1312
|
+
if (c === "{" || c === "[") {
|
|
1313
|
+
this.#mode = "nested";
|
|
1314
|
+
this.#nestedEnds = [c === "{" ? "}" : "]"];
|
|
1315
|
+
this.#nestedInString = false;
|
|
1316
|
+
this.#escape = false;
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
this.#mode = "scalar";
|
|
1320
|
+
}
|
|
1321
|
+
stepScalar(c, at) {
|
|
1322
|
+
if (c !== "," && c !== "}" && !isWhitespace(c)) return;
|
|
1323
|
+
this.closeValue(at);
|
|
1324
|
+
this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
|
|
1325
|
+
}
|
|
1326
|
+
stepNested(c, at) {
|
|
1327
|
+
if (this.#nestedInString) {
|
|
1328
|
+
this.#nestedInString = false;
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (c === "\"") {
|
|
1332
|
+
this.#nestedInString = true;
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (c === "{" || c === "[") {
|
|
1336
|
+
this.#nestedEnds.push(c === "{" ? "}" : "]");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (c === "}" || c === "]") {
|
|
1340
|
+
if (this.#nestedEnds.pop() !== c) {
|
|
1341
|
+
this.fail();
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
if (this.#nestedEnds.length === 0) {
|
|
1345
|
+
this.closeValue(at + 1);
|
|
1346
|
+
this.#mode = "comma-or-end";
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
closeValue(end) {
|
|
1351
|
+
const entry = this.#current;
|
|
1352
|
+
entry.end = end;
|
|
1353
|
+
this.#current = null;
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
/** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
|
|
1357
|
+
/**
|
|
1358
|
+
* Mark an unreachable closed-union branch.
|
|
1359
|
+
* @param value - impossible value; an unhandled typed variant fails at the call site.
|
|
1360
|
+
* @param context - optional switch-site label included in the failure message.
|
|
1361
|
+
* @returns never; a runtime value that escaped its type always throws.
|
|
1362
|
+
*/
|
|
1363
|
+
function assertNever(value, context) {
|
|
1364
|
+
const rendered = JSON.stringify(value) ?? String(value);
|
|
1365
|
+
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
|
|
1369
|
+
* @param value - value to freeze.
|
|
1370
|
+
* @returns the same value after every reachable enumerable child is frozen.
|
|
1371
|
+
*/
|
|
1372
|
+
function deepFreeze(value) {
|
|
1373
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1374
|
+
const pending = [{
|
|
1375
|
+
kind: "visit",
|
|
1376
|
+
node: value
|
|
1377
|
+
}];
|
|
1378
|
+
while (pending.length > 0) {
|
|
1379
|
+
const task = pending.pop();
|
|
1380
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
1381
|
+
if (task === void 0) continue;
|
|
1382
|
+
if (task.kind === "property") {
|
|
1383
|
+
pending.push({
|
|
1384
|
+
kind: "visit",
|
|
1385
|
+
node: task.source[task.key]
|
|
1386
|
+
});
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const node = task.node;
|
|
1390
|
+
if (node === null || typeof node !== "object") continue;
|
|
1391
|
+
if (node instanceof AbortSignal) continue;
|
|
1392
|
+
if (seen.has(node)) continue;
|
|
1393
|
+
seen.add(node);
|
|
1394
|
+
Object.freeze(node);
|
|
1395
|
+
const keys = Object.keys(node);
|
|
1396
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
1397
|
+
const key = keys[index];
|
|
1398
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
1399
|
+
if (key === void 0) continue;
|
|
1400
|
+
pending.push({
|
|
1401
|
+
kind: "property",
|
|
1402
|
+
source: node,
|
|
1403
|
+
key
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return value;
|
|
1408
|
+
}
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.2.1-rc.1_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
|
|
1411
|
+
/**
|
|
1412
|
+
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
1413
|
+
* @returns the UUID string.
|
|
1414
|
+
*/
|
|
1415
|
+
function randomUUID() {
|
|
1416
|
+
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
1417
|
+
const hex = Array.from(bytes, (byte, index) => {
|
|
1418
|
+
return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
|
|
1419
|
+
}).join("");
|
|
1420
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1421
|
+
}
|
|
1422
|
+
//#endregion
|
|
1423
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.2.1-rc.1_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-brand/lib/index.js
|
|
1424
|
+
/**
|
|
1425
|
+
* Duplicate-install-safe nominal primitive helpers.
|
|
1426
|
+
*
|
|
1427
|
+
* A brand makes structurally identical strings or numbers non-interchangeable
|
|
1428
|
+
* at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
|
|
1429
|
+
* expected, and an event sequence cannot be passed as a log offset. Comparison,
|
|
1430
|
+
* logging, and serialization retain the underlying primitive behavior.
|
|
1431
|
+
*
|
|
1432
|
+
* This package owns no concrete domain value and keeps no runtime identity or mutable
|
|
1433
|
+
* state, so independently installed copies produce interchangeable values.
|
|
1434
|
+
*
|
|
1435
|
+
* @module @deepseek-ai/dsh-brand
|
|
1436
|
+
*/
|
|
1437
|
+
/**
|
|
1438
|
+
* Apply a compile-time string brand without changing the value.
|
|
1439
|
+
* @param value - string admitted by the domain that owns the target brand.
|
|
1440
|
+
* @returns the same string with the requested compile-time brand.
|
|
1441
|
+
*/
|
|
1442
|
+
function brandString(value) {
|
|
1443
|
+
return value;
|
|
1444
|
+
}
|
|
1445
|
+
//#endregion
|
|
1446
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.6/node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
1447
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
1448
|
+
function isNullable(value) {
|
|
1449
|
+
return value === null || value === void 0;
|
|
1450
|
+
}
|
|
1451
|
+
/** Return true for non-array object values. */
|
|
1452
|
+
function isPlainObject(data) {
|
|
1453
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
1454
|
+
}
|
|
1455
|
+
/** Filter object entries and return a new object. */
|
|
1456
|
+
function filterKeys(object, filter) {
|
|
1457
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
1458
|
+
}
|
|
1459
|
+
/** Map object values while preserving the original key set. */
|
|
1460
|
+
function mapValues(object, transform) {
|
|
1461
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
1462
|
+
}
|
|
1463
|
+
/** Pick selected keys from an object, optionally including `undefined` values. */
|
|
1464
|
+
function pick(source, keys, forced) {
|
|
1465
|
+
if (!keys) return { ...source };
|
|
1466
|
+
const result = {};
|
|
1467
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
1468
|
+
return result;
|
|
1469
|
+
}
|
|
1470
|
+
/** Shared config references used by schema validators and plugin runtimes. */
|
|
1471
|
+
const write = Symbol.for("cosmokit.volatile.write");
|
|
1472
|
+
function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
1473
|
+
if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
|
|
1474
|
+
if (value === null || typeof value !== "object") return value;
|
|
1475
|
+
if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
|
|
1476
|
+
ancestors.add(value);
|
|
1477
|
+
try {
|
|
1478
|
+
if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
|
|
1479
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
|
|
1480
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
|
|
1481
|
+
} finally {
|
|
1482
|
+
ancestors.delete(value);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Create a detached reference containing an immutable copy of the supplied data.
|
|
1487
|
+
* @param value - validated config data; class instances and functions are unsupported.
|
|
1488
|
+
* @returns a reference whose value is updated only by its owning runtime.
|
|
1489
|
+
*/
|
|
1490
|
+
function createVolatile(value) {
|
|
1491
|
+
let current = snapshot(value);
|
|
1492
|
+
return Object.freeze({
|
|
1493
|
+
get: () => current,
|
|
1494
|
+
[write]: (value) => {
|
|
1495
|
+
current = value;
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Identify references across ESM/CJS copies of the shared library.
|
|
1501
|
+
* @param value - a parsed config value.
|
|
1502
|
+
* @returns whether the value implements the shared reference protocol.
|
|
1503
|
+
*/
|
|
1504
|
+
function isVolatile(value) {
|
|
1505
|
+
return typeof value === "object" && value !== null && write in value;
|
|
1506
|
+
}
|
|
1507
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
1508
|
+
function is(type, value) {
|
|
1509
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
1510
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
1511
|
+
}
|
|
1512
|
+
function isArrayBufferLike(value) {
|
|
1513
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
1514
|
+
}
|
|
1515
|
+
function isArrayBufferSource(value) {
|
|
1516
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
1517
|
+
}
|
|
1518
|
+
/** Binary source detection and base64/hex conversion helpers. */
|
|
1519
|
+
var Binary;
|
|
1520
|
+
(function(Binary) {
|
|
1521
|
+
Binary.is = isArrayBufferLike;
|
|
1522
|
+
Binary.isSource = isArrayBufferSource;
|
|
1523
|
+
function fromSource(source) {
|
|
1524
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1525
|
+
else return source;
|
|
1526
|
+
}
|
|
1527
|
+
Binary.fromSource = fromSource;
|
|
1528
|
+
function toBase64(source) {
|
|
1529
|
+
source = fromSource(source);
|
|
1530
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
1531
|
+
let binary = "";
|
|
1532
|
+
const bytes = new Uint8Array(source);
|
|
1533
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
1534
|
+
return btoa(binary);
|
|
1535
|
+
}
|
|
1536
|
+
Binary.toBase64 = toBase64;
|
|
1537
|
+
function fromBase64(source) {
|
|
1538
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
1539
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
1540
|
+
}
|
|
1541
|
+
Binary.fromBase64 = fromBase64;
|
|
1542
|
+
function toHex(source) {
|
|
1543
|
+
source = fromSource(source);
|
|
1544
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
1545
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1546
|
+
}
|
|
1547
|
+
Binary.toHex = toHex;
|
|
1548
|
+
function fromHex(source) {
|
|
1549
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
1550
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
1551
|
+
const buffer = [];
|
|
1552
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
1553
|
+
return Uint8Array.from(buffer).buffer;
|
|
1554
|
+
}
|
|
1555
|
+
Binary.fromHex = fromHex;
|
|
1556
|
+
})(Binary || (Binary = {}));
|
|
1557
|
+
Binary.fromBase64;
|
|
1558
|
+
Binary.toBase64;
|
|
1559
|
+
Binary.fromHex;
|
|
1560
|
+
Binary.toHex;
|
|
1561
|
+
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
|
1562
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
1563
|
+
if (!source || typeof source !== "object") return source;
|
|
1564
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
1565
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
1566
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
1567
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1568
|
+
const cached = refs.get(source);
|
|
1569
|
+
if (cached) return cached;
|
|
1570
|
+
if (Array.isArray(source)) {
|
|
1571
|
+
const result = [];
|
|
1572
|
+
refs.set(source, result);
|
|
1573
|
+
source.forEach((value, index) => {
|
|
1574
|
+
result[index] = Reflect.apply(clone, null, [value, refs]);
|
|
1575
|
+
});
|
|
1576
|
+
return result;
|
|
1577
|
+
}
|
|
1578
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
1579
|
+
refs.set(source, result);
|
|
1580
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
1581
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
1582
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
1583
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
1584
|
+
}
|
|
1585
|
+
return result;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Compare values recursively, treating two volatile references as equal regardless of value.
|
|
1589
|
+
* Strict comparison distinguishes null/undefined, treats opaque objects by identity,
|
|
1590
|
+
* compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
|
|
1591
|
+
* @param a - first value.
|
|
1592
|
+
* @param b - second value.
|
|
1593
|
+
* @param strict - whether to require strict data equality outside volatile references.
|
|
1594
|
+
* @returns whether the values compare equal.
|
|
1595
|
+
*/
|
|
1596
|
+
function deepEqual(a, b, strict) {
|
|
1597
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
1598
|
+
function compare(a, b) {
|
|
1599
|
+
if (a === b) return true;
|
|
1600
|
+
if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
|
|
1601
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
1602
|
+
if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
|
|
1603
|
+
if (ancestors.has(a)) return false;
|
|
1604
|
+
function check(test, then) {
|
|
1605
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
1606
|
+
}
|
|
1607
|
+
ancestors.add(a);
|
|
1608
|
+
try {
|
|
1609
|
+
return check(Array.isArray, (a, b) => {
|
|
1610
|
+
if (a.length !== b.length) return false;
|
|
1611
|
+
for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
|
|
1612
|
+
return true;
|
|
1613
|
+
}) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
1614
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
1615
|
+
const viewA = new Uint8Array(a);
|
|
1616
|
+
const viewB = new Uint8Array(b);
|
|
1617
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
1618
|
+
return true;
|
|
1619
|
+
}) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
|
|
1620
|
+
...a,
|
|
1621
|
+
...b
|
|
1622
|
+
}).every((key) => compare(a[key], b[key])));
|
|
1623
|
+
} finally {
|
|
1624
|
+
ancestors.delete(a);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return compare(a, b);
|
|
1628
|
+
}
|
|
1629
|
+
/** Time constants plus parsing and formatting helpers. */
|
|
1630
|
+
var Time;
|
|
1631
|
+
(function(Time) {
|
|
1632
|
+
Time.millisecond = 1;
|
|
1633
|
+
Time.second = 1e3;
|
|
1634
|
+
Time.minute = Time.second * 60;
|
|
1635
|
+
Time.hour = Time.minute * 60;
|
|
1636
|
+
Time.day = Time.hour * 24;
|
|
1637
|
+
Time.week = Time.day * 7;
|
|
1638
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
1639
|
+
function setTimezoneOffset(offset) {
|
|
1640
|
+
timezoneOffset = offset;
|
|
1641
|
+
}
|
|
1642
|
+
Time.setTimezoneOffset = setTimezoneOffset;
|
|
1643
|
+
function getTimezoneOffset() {
|
|
1644
|
+
return timezoneOffset;
|
|
1645
|
+
}
|
|
1646
|
+
Time.getTimezoneOffset = getTimezoneOffset;
|
|
1647
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
1648
|
+
if (typeof date === "number") date = new Date(date);
|
|
1649
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
1650
|
+
return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
|
|
1651
|
+
}
|
|
1652
|
+
Time.getDateNumber = getDateNumber;
|
|
1653
|
+
function fromDateNumber(value, offset) {
|
|
1654
|
+
const date = new Date(value * Time.day);
|
|
1655
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
1656
|
+
return new Date(+date + offset * Time.minute);
|
|
1657
|
+
}
|
|
1658
|
+
Time.fromDateNumber = fromDateNumber;
|
|
1659
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
1660
|
+
const timeRegExp = new RegExp(`^${[
|
|
1661
|
+
"w(?:eek(?:s)?)?",
|
|
1662
|
+
"d(?:ay(?:s)?)?",
|
|
1663
|
+
"h(?:our(?:s)?)?",
|
|
1664
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
1665
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
1666
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
1667
|
+
function parseTime(source) {
|
|
1668
|
+
const capture = timeRegExp.exec(source);
|
|
1669
|
+
if (!capture) return 0;
|
|
1670
|
+
return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
|
|
1671
|
+
}
|
|
1672
|
+
Time.parseTime = parseTime;
|
|
1673
|
+
function parseDate(date) {
|
|
1674
|
+
const parsed = parseTime(date);
|
|
1675
|
+
if (parsed) date = Date.now() + parsed;
|
|
1676
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
1677
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
1678
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
1679
|
+
}
|
|
1680
|
+
Time.parseDate = parseDate;
|
|
1681
|
+
function format(ms) {
|
|
1682
|
+
const abs = Math.abs(ms);
|
|
1683
|
+
if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
|
|
1684
|
+
else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
|
|
1685
|
+
else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
|
|
1686
|
+
else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
|
|
1687
|
+
return ms + "ms";
|
|
1688
|
+
}
|
|
1689
|
+
Time.format = format;
|
|
1690
|
+
function toDigits(source, length = 2) {
|
|
1691
|
+
return source.toString().padStart(length, "0");
|
|
1692
|
+
}
|
|
1693
|
+
Time.toDigits = toDigits;
|
|
1694
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
1695
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
1696
|
+
}
|
|
1697
|
+
Time.template = template;
|
|
1698
|
+
})(Time || (Time = {}));
|
|
1699
|
+
//#endregion
|
|
1700
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+schemastery@3.18.5/node_modules/@deepseek-ai/schemastery/lib/index.mjs
|
|
1701
|
+
const kSchema = Symbol.for("schemastery");
|
|
1702
|
+
const kValidationError = Symbol.for("ValidationError");
|
|
1703
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
1704
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
1705
|
+
var ValidationError = class extends TypeError {
|
|
1706
|
+
options;
|
|
1707
|
+
name = "ValidationError";
|
|
1708
|
+
constructor(message, options) {
|
|
1709
|
+
let prefix = "$";
|
|
1710
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
1711
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
1712
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
1713
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
1714
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
1715
|
+
this.options = options;
|
|
1716
|
+
}
|
|
1717
|
+
static is(error) {
|
|
1718
|
+
return !!error?.[kValidationError];
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
1722
|
+
const Schema = function(options) {
|
|
1723
|
+
const schema = function(data, options = {}) {
|
|
1724
|
+
return Schema.resolve(data, schema, options)[0];
|
|
1725
|
+
};
|
|
1726
|
+
if (options.refs) {
|
|
1727
|
+
const refs = mapValues(options.refs, (options) => new Schema(options));
|
|
1728
|
+
const getRef = (uid) => refs[uid];
|
|
1729
|
+
for (const key in refs) {
|
|
1730
|
+
const options = refs[key];
|
|
1731
|
+
options.sKey = getRef(options.sKey);
|
|
1732
|
+
options.inner = getRef(options.inner);
|
|
1733
|
+
options.list = options.list && options.list.map(getRef);
|
|
1734
|
+
options.dict = options.dict && mapValues(options.dict, getRef);
|
|
1735
|
+
}
|
|
1736
|
+
return refs[options.uid];
|
|
1737
|
+
}
|
|
1738
|
+
Object.assign(schema, options);
|
|
1739
|
+
if (typeof schema.callback === "string") try {
|
|
1740
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
1741
|
+
} catch {}
|
|
1742
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
1743
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
1744
|
+
schema.meta ||= {};
|
|
1745
|
+
schema.toString = schema.toString.bind(schema);
|
|
1746
|
+
return schema;
|
|
1747
|
+
};
|
|
1748
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
1749
|
+
Schema.prototype[kSchema] = true;
|
|
1750
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
1751
|
+
return {
|
|
1752
|
+
version: 1,
|
|
1753
|
+
vendor: "schemastery",
|
|
1754
|
+
validate: (value) => {
|
|
1755
|
+
try {
|
|
1756
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
1759
|
+
message: error.message,
|
|
1760
|
+
path: error.options.path
|
|
1761
|
+
}] };
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
} });
|
|
1767
|
+
Schema.ValidationError = ValidationError;
|
|
1768
|
+
Schema.prototype.toJSON = function toJSON() {
|
|
1769
|
+
if (globalThis.__schemastery_refs__) {
|
|
1770
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
1771
|
+
return this.uid;
|
|
1772
|
+
}
|
|
1773
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
1774
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
1775
|
+
const result = {
|
|
1776
|
+
uid: this.uid,
|
|
1777
|
+
refs: globalThis.__schemastery_refs__
|
|
1778
|
+
};
|
|
1779
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
1780
|
+
return result;
|
|
1781
|
+
};
|
|
1782
|
+
Schema.prototype.set = function set(key, value) {
|
|
1783
|
+
this.dict[key] = value;
|
|
1784
|
+
return this;
|
|
1785
|
+
};
|
|
1786
|
+
Schema.prototype.push = function push(value) {
|
|
1787
|
+
this.list.push(value);
|
|
1788
|
+
return this;
|
|
1789
|
+
};
|
|
1790
|
+
function mergeDesc(original, messages) {
|
|
1791
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
1792
|
+
for (const locale in messages) {
|
|
1793
|
+
const value = messages[locale];
|
|
1794
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
1795
|
+
else if (typeof value === "string") result[locale] = value;
|
|
1796
|
+
}
|
|
1797
|
+
return result;
|
|
1798
|
+
}
|
|
1799
|
+
function getInner(value) {
|
|
1800
|
+
return value?.$value ?? value?.$inner;
|
|
1801
|
+
}
|
|
1802
|
+
function extractKeys(data) {
|
|
1803
|
+
return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
|
|
1804
|
+
}
|
|
1805
|
+
Schema.prototype.i18n = function i18n(messages) {
|
|
1806
|
+
const schema = Schema(this);
|
|
1807
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
1808
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
1809
|
+
if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
|
|
1810
|
+
return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
1811
|
+
});
|
|
1812
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
1813
|
+
return inner.i18n(mapValues(messages, (data = {}) => {
|
|
1814
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
1815
|
+
if (Array.isArray(data)) return data[index];
|
|
1816
|
+
return extractKeys(data);
|
|
1817
|
+
}));
|
|
1818
|
+
});
|
|
1819
|
+
if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
|
|
1820
|
+
if (getInner(data)) return getInner(data);
|
|
1821
|
+
return extractKeys(data);
|
|
1822
|
+
}));
|
|
1823
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
|
|
1824
|
+
return schema;
|
|
1825
|
+
};
|
|
1826
|
+
Schema.prototype.extra = function extra(key, value) {
|
|
1827
|
+
const schema = Schema(this);
|
|
1828
|
+
schema.meta = {
|
|
1829
|
+
...schema.meta,
|
|
1830
|
+
[key]: value
|
|
1831
|
+
};
|
|
1832
|
+
return schema;
|
|
1833
|
+
};
|
|
1834
|
+
for (const key of [
|
|
1835
|
+
"required",
|
|
1836
|
+
"disabled",
|
|
1837
|
+
"collapse",
|
|
1838
|
+
"hidden",
|
|
1839
|
+
"loose"
|
|
1840
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
1841
|
+
const schema = Schema(this);
|
|
1842
|
+
schema.meta = {
|
|
1843
|
+
...schema.meta,
|
|
1844
|
+
[key]: value
|
|
1845
|
+
};
|
|
1846
|
+
return schema;
|
|
1847
|
+
} });
|
|
1848
|
+
Schema.prototype.deprecated = function deprecated() {
|
|
1849
|
+
const schema = Schema(this);
|
|
1850
|
+
schema.meta.badges ||= [];
|
|
1851
|
+
schema.meta.badges.push({
|
|
1852
|
+
text: "deprecated",
|
|
1853
|
+
type: "danger"
|
|
1854
|
+
});
|
|
1855
|
+
return schema;
|
|
1856
|
+
};
|
|
1857
|
+
Schema.prototype.experimental = function experimental() {
|
|
1858
|
+
const schema = Schema(this);
|
|
1859
|
+
schema.meta.badges ||= [];
|
|
1860
|
+
schema.meta.badges.push({
|
|
1861
|
+
text: "experimental",
|
|
1862
|
+
type: "warning"
|
|
1863
|
+
});
|
|
1864
|
+
return schema;
|
|
1865
|
+
};
|
|
1866
|
+
Schema.prototype.pattern = function pattern(regexp) {
|
|
1867
|
+
const schema = Schema(this);
|
|
1868
|
+
const pattern = pick(regexp, ["source", "flags"]);
|
|
1869
|
+
schema.meta = {
|
|
1870
|
+
...schema.meta,
|
|
1871
|
+
pattern
|
|
1872
|
+
};
|
|
1873
|
+
return schema;
|
|
1874
|
+
};
|
|
1875
|
+
Schema.prototype.simplify = function simplify(value) {
|
|
1876
|
+
if (isVolatile(value)) value = value.get();
|
|
1877
|
+
if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
|
|
1878
|
+
if (isNullable(value)) return value;
|
|
1879
|
+
if (this.type === "object" || this.type === "dict") {
|
|
1880
|
+
const result = {};
|
|
1881
|
+
for (const key in value) {
|
|
1882
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
1883
|
+
if (this.type === "dict" || !isNullable(item)) result[key] = item;
|
|
1884
|
+
}
|
|
1885
|
+
if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
|
|
1886
|
+
return result;
|
|
1887
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
1888
|
+
const result = [];
|
|
1889
|
+
value.forEach((value, index) => {
|
|
1890
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
1891
|
+
const item = schema ? schema.simplify(value) : value;
|
|
1892
|
+
result.push(item);
|
|
1893
|
+
});
|
|
1894
|
+
return result;
|
|
1895
|
+
} else if (this.type === "intersect") {
|
|
1896
|
+
const result = {};
|
|
1897
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
1898
|
+
return result;
|
|
1899
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
1900
|
+
Schema.resolve(value, schema, {});
|
|
1901
|
+
return schema.simplify(value);
|
|
1902
|
+
} catch {}
|
|
1903
|
+
return value;
|
|
1904
|
+
};
|
|
1905
|
+
Schema.prototype.toString = function toString(inline) {
|
|
1906
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
1907
|
+
};
|
|
1908
|
+
Schema.prototype.role = function role(role, extra) {
|
|
1909
|
+
const schema = Schema(this);
|
|
1910
|
+
schema.meta = {
|
|
1911
|
+
...schema.meta,
|
|
1912
|
+
role,
|
|
1913
|
+
extra
|
|
1914
|
+
};
|
|
1915
|
+
return schema;
|
|
1916
|
+
};
|
|
1917
|
+
for (const key of [
|
|
1918
|
+
"default",
|
|
1919
|
+
"link",
|
|
1920
|
+
"comment",
|
|
1921
|
+
"description",
|
|
1922
|
+
"max",
|
|
1923
|
+
"min",
|
|
1924
|
+
"step"
|
|
1925
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
1926
|
+
const schema = Schema(this);
|
|
1927
|
+
schema.meta = {
|
|
1928
|
+
...schema.meta,
|
|
1929
|
+
[key]: value
|
|
1930
|
+
};
|
|
1931
|
+
return schema;
|
|
1932
|
+
} });
|
|
1933
|
+
Schema.prototype.volatile = function volatile() {
|
|
1934
|
+
if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
|
|
1935
|
+
return this.extra("volatile", true);
|
|
1936
|
+
};
|
|
1937
|
+
const resolvers = {};
|
|
1938
|
+
const checkedVolatile = Symbol("checked-volatile-schema");
|
|
1939
|
+
function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
|
|
1940
|
+
const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
|
|
1941
|
+
if (states.has(blocked)) return;
|
|
1942
|
+
states.add(blocked);
|
|
1943
|
+
seen.set(schema, states);
|
|
1944
|
+
if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
|
|
1945
|
+
const nested = blocked || !!schema.meta?.volatile;
|
|
1946
|
+
if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
|
|
1947
|
+
if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
|
|
1948
|
+
if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
|
|
1949
|
+
if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
|
|
1950
|
+
}
|
|
1951
|
+
Schema.extend = function extend(type, resolve) {
|
|
1952
|
+
resolvers[type] = resolve;
|
|
1953
|
+
};
|
|
1954
|
+
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
|
1955
|
+
if (!schema) return [data];
|
|
1956
|
+
if (!options[checkedVolatile]) {
|
|
1957
|
+
validateVolatileSchema(schema, options.path);
|
|
1958
|
+
options = {
|
|
1959
|
+
...options,
|
|
1960
|
+
[checkedVolatile]: true
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
if (schema.meta?.volatile) {
|
|
1964
|
+
const inner = Schema(schema);
|
|
1965
|
+
inner.meta = {
|
|
1966
|
+
...schema.meta,
|
|
1967
|
+
volatile: false
|
|
1968
|
+
};
|
|
1969
|
+
const [value, adapted] = Schema.resolve(data, inner, options, strict);
|
|
1970
|
+
try {
|
|
1971
|
+
return [createVolatile(value), adapted];
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error), options);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
1977
|
+
if (isNullable(data) && schema.type !== "lazy") {
|
|
1978
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
1979
|
+
let current = schema;
|
|
1980
|
+
let fallback = schema.meta.default;
|
|
1981
|
+
while (current?.type === "intersect" && isNullable(fallback)) {
|
|
1982
|
+
current = current.list[0];
|
|
1983
|
+
fallback = current?.meta.default;
|
|
1984
|
+
}
|
|
1985
|
+
if (isNullable(fallback)) return [data];
|
|
1986
|
+
data = clone(fallback);
|
|
1987
|
+
}
|
|
1988
|
+
const callback = resolvers[schema.type];
|
|
1989
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
1990
|
+
try {
|
|
1991
|
+
return callback(data, schema, options, strict);
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
if (!schema.meta.loose) throw error;
|
|
1994
|
+
return [schema.meta.default];
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
Schema.from = function from(source) {
|
|
1998
|
+
if (isNullable(source)) return Schema.any();
|
|
1999
|
+
else if ([
|
|
2000
|
+
"string",
|
|
2001
|
+
"number",
|
|
2002
|
+
"boolean"
|
|
2003
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
2004
|
+
else if (source[kSchema]) return source;
|
|
2005
|
+
else if (typeof source === "function") switch (source) {
|
|
2006
|
+
case String: return Schema.string().required();
|
|
2007
|
+
case Number: return Schema.number().required();
|
|
2008
|
+
case Boolean: return Schema.boolean().required();
|
|
2009
|
+
case Function: return Schema.function().required();
|
|
2010
|
+
default: return Schema.is(source).required();
|
|
2011
|
+
}
|
|
2012
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
2013
|
+
};
|
|
2014
|
+
Schema.lazy = function lazy(builder) {
|
|
2015
|
+
const toJSON = () => {
|
|
2016
|
+
if (!schema.inner[kSchema]) {
|
|
2017
|
+
schema.inner = schema.builder();
|
|
2018
|
+
schema.inner.meta = {
|
|
2019
|
+
...schema.meta,
|
|
2020
|
+
...schema.inner.meta
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
return schema.inner.toJSON();
|
|
2024
|
+
};
|
|
2025
|
+
const schema = new Schema({
|
|
2026
|
+
type: "lazy",
|
|
2027
|
+
builder,
|
|
2028
|
+
inner: { toJSON }
|
|
2029
|
+
});
|
|
2030
|
+
return schema;
|
|
2031
|
+
};
|
|
2032
|
+
Schema.natural = function natural() {
|
|
2033
|
+
return Schema.number().step(1).min(0);
|
|
2034
|
+
};
|
|
2035
|
+
Schema.percent = function percent() {
|
|
2036
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
2037
|
+
};
|
|
2038
|
+
Schema.date = function date() {
|
|
2039
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
2040
|
+
const date = new Date(value);
|
|
2041
|
+
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
2042
|
+
return date;
|
|
2043
|
+
}, true)]);
|
|
2044
|
+
};
|
|
2045
|
+
Schema.regExp = function regExp(flag = "") {
|
|
2046
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
2047
|
+
try {
|
|
2048
|
+
return new RegExp(value, flag);
|
|
2049
|
+
} catch (e) {
|
|
2050
|
+
throw new ValidationError(e.message, options);
|
|
2051
|
+
}
|
|
2052
|
+
}, true)]);
|
|
2053
|
+
};
|
|
2054
|
+
Schema.arrayBuffer = function arrayBuffer(encoding) {
|
|
2055
|
+
return Schema.union([
|
|
2056
|
+
Schema.is(ArrayBuffer),
|
|
2057
|
+
Schema.is(SharedArrayBuffer),
|
|
2058
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
2059
|
+
if (Binary.isSource(value)) return Binary.fromSource(value);
|
|
2060
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
2061
|
+
}, true),
|
|
2062
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
2063
|
+
try {
|
|
2064
|
+
return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
|
|
2065
|
+
} catch (e) {
|
|
2066
|
+
throw new ValidationError(e.message, options);
|
|
2067
|
+
}
|
|
2068
|
+
}, true)] : []
|
|
2069
|
+
]);
|
|
2070
|
+
};
|
|
2071
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
2072
|
+
if (!schema.inner[kSchema]) {
|
|
2073
|
+
schema.inner = schema.builder();
|
|
2074
|
+
schema.inner.meta = {
|
|
2075
|
+
...schema.meta,
|
|
2076
|
+
...schema.inner.meta
|
|
2077
|
+
};
|
|
2078
|
+
validateVolatileSchema(schema.inner, options.path, true);
|
|
2079
|
+
}
|
|
2080
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
2081
|
+
});
|
|
2082
|
+
Schema.extend("any", (data) => {
|
|
2083
|
+
return [data];
|
|
2084
|
+
});
|
|
2085
|
+
Schema.extend("never", (data, _, options) => {
|
|
2086
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
2087
|
+
});
|
|
2088
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
2089
|
+
if (deepEqual(data, value)) return [value];
|
|
2090
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
2091
|
+
});
|
|
2092
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
2093
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
2094
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
2095
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
2096
|
+
}
|
|
2097
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
2098
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
2099
|
+
if (meta.pattern) {
|
|
2100
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
2101
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
2102
|
+
}
|
|
2103
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
2104
|
+
return [data];
|
|
2105
|
+
});
|
|
2106
|
+
function decimalShift(data, digits) {
|
|
2107
|
+
const str = data.toString();
|
|
2108
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
2109
|
+
const index = str.indexOf(".");
|
|
2110
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
2111
|
+
const frac = str.slice(index + 1);
|
|
2112
|
+
const integer = str.slice(0, index);
|
|
2113
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
2114
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
2115
|
+
}
|
|
2116
|
+
function isMultipleOf(data, min, step) {
|
|
2117
|
+
step = Math.abs(step);
|
|
2118
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
2119
|
+
const index = step.toString().indexOf(".");
|
|
2120
|
+
const digits = step.toString().slice(index + 1).length;
|
|
2121
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
2122
|
+
}
|
|
2123
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
2124
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
2125
|
+
checkWithinRange(data, meta, "number", options);
|
|
2126
|
+
const { step } = meta;
|
|
2127
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
2128
|
+
return [data];
|
|
2129
|
+
});
|
|
2130
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
2131
|
+
if (typeof data === "boolean") return [data];
|
|
2132
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
2133
|
+
});
|
|
2134
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
2135
|
+
let value = 0, keys = [];
|
|
2136
|
+
if (typeof data === "number") {
|
|
2137
|
+
value = data;
|
|
2138
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
2139
|
+
} else if (Array.isArray(data)) {
|
|
2140
|
+
keys = data;
|
|
2141
|
+
for (const key of keys) {
|
|
2142
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
2143
|
+
if (key in bits) value |= bits[key];
|
|
2144
|
+
}
|
|
2145
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
2146
|
+
if (value === meta.default) return [value];
|
|
2147
|
+
return [value, keys];
|
|
2148
|
+
});
|
|
2149
|
+
Schema.extend("function", (data, _, options) => {
|
|
2150
|
+
if (typeof data === "function") return [data];
|
|
2151
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
2152
|
+
});
|
|
2153
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
2154
|
+
if (typeof constructor === "function") {
|
|
2155
|
+
if (data instanceof constructor) return [data];
|
|
2156
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
2157
|
+
} else {
|
|
2158
|
+
if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
2159
|
+
let prototype = Object.getPrototypeOf(data);
|
|
2160
|
+
while (prototype) {
|
|
2161
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
2162
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
2163
|
+
}
|
|
2164
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
function property(data, key, schema, options) {
|
|
2168
|
+
try {
|
|
2169
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
2170
|
+
...options,
|
|
2171
|
+
path: [...options.path || [], key]
|
|
2172
|
+
});
|
|
2173
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
2174
|
+
return value;
|
|
2175
|
+
} catch (e) {
|
|
2176
|
+
if (!options?.autofix) throw e;
|
|
2177
|
+
delete data[key];
|
|
2178
|
+
return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
2182
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
2183
|
+
checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
|
|
2184
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
2185
|
+
});
|
|
2186
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
2187
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
2188
|
+
const result = {};
|
|
2189
|
+
for (const key in data) {
|
|
2190
|
+
let rKey;
|
|
2191
|
+
try {
|
|
2192
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
if (strict) continue;
|
|
2195
|
+
throw error;
|
|
2196
|
+
}
|
|
2197
|
+
result[rKey] = property(data, key, inner, options);
|
|
2198
|
+
data[rKey] = data[key];
|
|
2199
|
+
if (key !== rKey) delete data[key];
|
|
2200
|
+
}
|
|
2201
|
+
return [result];
|
|
2202
|
+
});
|
|
2203
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
2204
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
2205
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
2206
|
+
if (strict) return [result];
|
|
2207
|
+
result.push(...data.slice(list.length));
|
|
2208
|
+
return [result];
|
|
2209
|
+
});
|
|
2210
|
+
function merge(result, data) {
|
|
2211
|
+
for (const key in data) {
|
|
2212
|
+
if (key in result) continue;
|
|
2213
|
+
result[key] = data[key];
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
2217
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
2218
|
+
const result = {};
|
|
2219
|
+
for (const key in dict) {
|
|
2220
|
+
const value = property(data, key, dict[key], options);
|
|
2221
|
+
if (!isNullable(value) || key in data) result[key] = value;
|
|
2222
|
+
}
|
|
2223
|
+
if (!strict) merge(result, data);
|
|
2224
|
+
return [result];
|
|
2225
|
+
});
|
|
2226
|
+
Schema.extend("union", (data, { list, toString }, options, strict) => {
|
|
2227
|
+
const messages = [];
|
|
2228
|
+
for (const inner of list) try {
|
|
2229
|
+
return Schema.resolve(data, inner, options, strict);
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
messages.push(error);
|
|
2232
|
+
}
|
|
2233
|
+
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2234
|
+
});
|
|
2235
|
+
Schema.extend("intersect", (data, { list, toString }, options, strict) => {
|
|
2236
|
+
if (!list.length) return [data];
|
|
2237
|
+
let result;
|
|
2238
|
+
for (const inner of list) {
|
|
2239
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
2240
|
+
if (isNullable(value)) continue;
|
|
2241
|
+
if (isNullable(result)) result = value;
|
|
2242
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2243
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
2244
|
+
else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2245
|
+
}
|
|
2246
|
+
if (!strict && isPlainObject(data)) merge(result, data);
|
|
2247
|
+
return [result];
|
|
2248
|
+
});
|
|
2249
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
2250
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
2251
|
+
if (preserve) return [callback(result)];
|
|
2252
|
+
else return [callback(result), callback(adapted)];
|
|
2253
|
+
});
|
|
2254
|
+
const formatters = {};
|
|
2255
|
+
function defineMethod(name, keys, format) {
|
|
2256
|
+
formatters[name] = format;
|
|
2257
|
+
Object.assign(Schema, { [name](...args) {
|
|
2258
|
+
const schema = new Schema({ type: name });
|
|
2259
|
+
keys.forEach((key, index) => {
|
|
2260
|
+
switch (key) {
|
|
2261
|
+
case "sKey":
|
|
2262
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
2263
|
+
break;
|
|
2264
|
+
case "inner":
|
|
2265
|
+
schema.inner = Schema.from(args[index]);
|
|
2266
|
+
break;
|
|
2267
|
+
case "list":
|
|
2268
|
+
schema.list = args[index].map(Schema.from);
|
|
2269
|
+
break;
|
|
2270
|
+
case "dict":
|
|
2271
|
+
schema.dict = mapValues(args[index], Schema.from);
|
|
2272
|
+
break;
|
|
2273
|
+
case "bits":
|
|
2274
|
+
schema.bits = {};
|
|
2275
|
+
for (const key in args[index]) {
|
|
2276
|
+
if (typeof args[index][key] !== "number") continue;
|
|
2277
|
+
schema.bits[key] = args[index][key];
|
|
2278
|
+
}
|
|
2279
|
+
break;
|
|
2280
|
+
case "callback": {
|
|
2281
|
+
const callback = schema.callback = args[index];
|
|
2282
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
case "constructor": {
|
|
2286
|
+
const constructor = schema.constructor = args[index];
|
|
2287
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
default: schema[key] = args[index];
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
if (name === "object" || name === "dict") schema.meta.default = {};
|
|
2294
|
+
else if (name === "array" || name === "tuple") schema.meta.default = [];
|
|
2295
|
+
else if (name === "bitset") schema.meta.default = 0;
|
|
2296
|
+
return schema;
|
|
2297
|
+
} });
|
|
2298
|
+
}
|
|
2299
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
2300
|
+
if (typeof constructor === "function") return constructor.name;
|
|
2301
|
+
else return constructor;
|
|
2302
|
+
});
|
|
2303
|
+
defineMethod("any", [], () => "any");
|
|
2304
|
+
defineMethod("never", [], () => "never");
|
|
2305
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
2306
|
+
defineMethod("string", [], () => "string");
|
|
2307
|
+
defineMethod("number", [], () => "number");
|
|
2308
|
+
defineMethod("boolean", [], () => "boolean");
|
|
2309
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
2310
|
+
defineMethod("function", [], () => "function");
|
|
2311
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
2312
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
2313
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
2314
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
2315
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
2316
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
2317
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
2318
|
+
}).join(", ")} }`;
|
|
2319
|
+
});
|
|
2320
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
2321
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
2322
|
+
return inline ? `(${result})` : result;
|
|
2323
|
+
});
|
|
2324
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
2325
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
2326
|
+
});
|
|
2327
|
+
defineMethod("transform", [
|
|
2328
|
+
"inner",
|
|
2329
|
+
"callback",
|
|
2330
|
+
"preserve"
|
|
2331
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.2.1-rc.1_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
|
|
2334
|
+
/** Largest delay Node schedules without clamping it to one millisecond. */
|
|
2335
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
2336
|
+
//#endregion
|
|
2337
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+dsh-llm@0.2.1-rc.1_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-llm/lib/index.js
|
|
2338
|
+
/**
|
|
2339
|
+
* Detach and deep-freeze a message whose identity already exists.
|
|
2340
|
+
* @param message - complete message, including its stable identity.
|
|
2341
|
+
* @returns an immutable snapshot that preserves the identity.
|
|
2342
|
+
*/
|
|
2343
|
+
function freezeMessage(message) {
|
|
2344
|
+
return deepFreeze(structuredClone(message));
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Create one identified message and freeze it before publication.
|
|
2348
|
+
* @param input - complete role, content, and source for a new message.
|
|
2349
|
+
* @returns an immutable message with a fresh stable identity.
|
|
2350
|
+
*/
|
|
2351
|
+
function createMessage(input) {
|
|
2352
|
+
return deepFreeze(structuredClone({
|
|
2353
|
+
...input,
|
|
2354
|
+
id: brandString(randomUUID())
|
|
2355
|
+
}));
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Create one identified user-role message and freeze it before publication.
|
|
2359
|
+
* @param input - complete content and source for a new user message.
|
|
2360
|
+
* @returns an immutable user message with a fresh stable identity.
|
|
2361
|
+
*/
|
|
2362
|
+
function createUserMessage(input) {
|
|
2363
|
+
return createMessage({
|
|
2364
|
+
...input,
|
|
2365
|
+
role: "user"
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Create one identified model-produced assistant message and freeze it before publication.
|
|
2370
|
+
* @param input - complete content plus the provider, model, and optional replay state for a new assistant message.
|
|
2371
|
+
* @returns an immutable assistant message with fixed role/source tags and a fresh stable identity.
|
|
2372
|
+
*/
|
|
2373
|
+
function createAssistantMessage(input) {
|
|
2374
|
+
return createMessage({
|
|
2375
|
+
role: "assistant",
|
|
2376
|
+
content: input.content,
|
|
2377
|
+
source: {
|
|
2378
|
+
kind: "model",
|
|
2379
|
+
...input.source
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Harness error base with a stable machine-routable code and chained cause.
|
|
2385
|
+
* Package errors extend it so tool results and replay can retain failure class.
|
|
2386
|
+
* @module @deepseek-ai/dsh-llm/error
|
|
2387
|
+
*/
|
|
2388
|
+
/**
|
|
2389
|
+
* Base class for all harness errors. Carries a `code` (stable, programmatic —
|
|
2390
|
+
* e.g. `NO_ADAPTER`, `INVALID_ARGS`) distinct from the
|
|
2391
|
+
* human-readable `message`, and supports `cause` chaining via the standard
|
|
2392
|
+
* `ErrorOptions`. `name` defaults to the subclass constructor name.
|
|
2393
|
+
*/
|
|
2394
|
+
var HarnessError = class extends Error {
|
|
2395
|
+
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
|
|
2396
|
+
code;
|
|
2397
|
+
constructor(message, code, options) {
|
|
2398
|
+
super(message, options);
|
|
2399
|
+
this.code = code;
|
|
2400
|
+
this.name = new.target.name;
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
/**
|
|
2404
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
2405
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
2406
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
2407
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
2408
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
2409
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
2410
|
+
* to repeat.
|
|
2411
|
+
*/
|
|
2412
|
+
const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
2413
|
+
new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
|
|
2414
|
+
new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
|
|
2415
|
+
new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
|
|
2416
|
+
/**
|
|
2417
|
+
* Provider-owned request-retry policy configuration and resolution.
|
|
2418
|
+
*
|
|
2419
|
+
* Adapters expose one resolved policy per registered provider route; the
|
|
2420
|
+
* optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
|
|
2421
|
+
*
|
|
2422
|
+
* @module @deepseek-ai/dsh-llm/retry-policy
|
|
2423
|
+
*/
|
|
2424
|
+
const DEFAULT_MAX_RETRIES = 5;
|
|
2425
|
+
const DEFAULT_INITIAL_DELAY_MS = 500;
|
|
2426
|
+
const DEFAULT_MAX_DELAY_MS = 1e4;
|
|
2427
|
+
const DEFAULT_JITTER_RATIO = .1;
|
|
2428
|
+
const DEFAULT_RETRYABLE_CODES = Object.freeze([
|
|
2429
|
+
EMPTY_RESPONSE_CODE,
|
|
2430
|
+
"RATE_LIMIT",
|
|
2431
|
+
"SERVER",
|
|
2432
|
+
"TIMEOUT",
|
|
2433
|
+
"TRANSPORT"
|
|
2434
|
+
]);
|
|
2435
|
+
const backoffSchema = Schema.object({
|
|
2436
|
+
initialDelayMs: Schema.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
|
|
2437
|
+
maxDelayMs: Schema.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
|
|
2438
|
+
jitterRatio: Schema.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
|
|
2439
|
+
});
|
|
2440
|
+
const normalPolicySchema = Schema.object({
|
|
2441
|
+
mode: Schema.const("normal").required(),
|
|
2442
|
+
maxRetries: Schema.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
|
|
2443
|
+
retryableCodes: Schema.array(Schema.string()).default([...DEFAULT_RETRYABLE_CODES]),
|
|
2444
|
+
backoff: backoffSchema
|
|
2445
|
+
});
|
|
2446
|
+
const alwaysPolicySchema = Schema.object({
|
|
2447
|
+
mode: Schema.const("always").required(),
|
|
2448
|
+
backoff: backoffSchema
|
|
2449
|
+
});
|
|
2450
|
+
Schema.union([normalPolicySchema, alwaysPolicySchema]);
|
|
2451
|
+
const NORMAL_POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
2452
|
+
"mode",
|
|
2453
|
+
"maxRetries",
|
|
2454
|
+
"retryableCodes",
|
|
2455
|
+
"backoff"
|
|
2456
|
+
]);
|
|
2457
|
+
const ALWAYS_POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
2458
|
+
"mode",
|
|
2459
|
+
"maxRetries",
|
|
2460
|
+
"retryableCodes",
|
|
2461
|
+
"backoff"
|
|
2462
|
+
]);
|
|
2463
|
+
const BACKOFF_KEYS = /* @__PURE__ */ new Set([
|
|
2464
|
+
"initialDelayMs",
|
|
2465
|
+
"maxDelayMs",
|
|
2466
|
+
"jitterRatio"
|
|
2467
|
+
]);
|
|
2468
|
+
function validateKeys(value, allowed, path) {
|
|
2469
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
|
|
2470
|
+
}
|
|
2471
|
+
function resolveBackoff(config, path) {
|
|
2472
|
+
if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
|
|
2473
|
+
const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
2474
|
+
const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
2475
|
+
const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
|
|
2476
|
+
if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > 2147483647) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
2477
|
+
if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > 2147483647) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
2478
|
+
if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
|
|
2479
|
+
if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
|
|
2480
|
+
return Object.freeze({
|
|
2481
|
+
initialDelayMs,
|
|
2482
|
+
maxDelayMs,
|
|
2483
|
+
jitterRatio
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Validate, default, and detach one provider-owned retry policy.
|
|
2488
|
+
* @param config - optional provider configuration; omission selects normal defaults.
|
|
2489
|
+
* @param path - diagnostic path naming the provider config that owns the value.
|
|
2490
|
+
* @returns an immutable policy safe to capture in provider registration state.
|
|
2491
|
+
*/
|
|
2492
|
+
function resolveRetryPolicy(config, path) {
|
|
2493
|
+
if (config === void 0) return Object.freeze({
|
|
2494
|
+
mode: "normal",
|
|
2495
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
2496
|
+
retryableCodes: DEFAULT_RETRYABLE_CODES,
|
|
2497
|
+
...resolveBackoff(void 0, `${path}.backoff`)
|
|
2498
|
+
});
|
|
2499
|
+
switch (config.mode) {
|
|
2500
|
+
case "normal": {
|
|
2501
|
+
validateKeys(config, NORMAL_POLICY_KEYS, path);
|
|
2502
|
+
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
2503
|
+
const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
|
|
2504
|
+
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
|
|
2505
|
+
if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
|
|
2506
|
+
if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
|
|
2507
|
+
if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
|
|
2508
|
+
return Object.freeze({
|
|
2509
|
+
mode: "normal",
|
|
2510
|
+
maxRetries,
|
|
2511
|
+
retryableCodes: Object.freeze([...retryableCodes]),
|
|
2512
|
+
...resolveBackoff(config.backoff, `${path}.backoff`)
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
case "always":
|
|
2516
|
+
validateKeys(config, ALWAYS_POLICY_KEYS, path);
|
|
2517
|
+
return Object.freeze({
|
|
2518
|
+
mode: "always",
|
|
2519
|
+
...resolveBackoff(config.backoff, `${path}.backoff`)
|
|
2520
|
+
});
|
|
2521
|
+
default: throw new Error(`${path}.mode must be "normal" or "always"`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
/**
|
|
2525
|
+
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
|
|
2526
|
+
* runs to decide whether a proposed configuration is a real change (worth a
|
|
2527
|
+
* logged header snapshot) or the held one restated.
|
|
2528
|
+
* @param a - one configuration.
|
|
2529
|
+
* @param b - the other.
|
|
2530
|
+
* @returns whether every field (including the `stop` list, element-wise) matches.
|
|
2531
|
+
*/
|
|
2532
|
+
function callConfigEquals(a, b) {
|
|
2533
|
+
if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
|
|
2534
|
+
if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
|
|
2535
|
+
return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
|
|
2536
|
+
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Normalization for values thrown by a final LLM adapter boundary.
|
|
2539
|
+
*
|
|
2540
|
+
* @module @deepseek-ai/dsh-llm/adapter-failure
|
|
2541
|
+
*/
|
|
2542
|
+
/**
|
|
2543
|
+
* Detach serializable provider facts from a value thrown by an adapter.
|
|
2544
|
+
* @param value - arbitrary value thrown during adapter dispatch or iteration.
|
|
2545
|
+
* @returns immutable provider-neutral facts suitable for a terminal finish chunk.
|
|
2546
|
+
* @internal
|
|
2547
|
+
*/
|
|
2548
|
+
function normalizeLlmFailure(value) {
|
|
2549
|
+
const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
|
|
2550
|
+
const carried = ownFailureSnapshot(error);
|
|
2551
|
+
if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
|
|
2552
|
+
return Object.freeze({
|
|
2553
|
+
message: errorMessage(error),
|
|
2554
|
+
code: harnessErrorCode(error)
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
/** Render a non-Error throw without letting hostile coercion escape normalization. */
|
|
2558
|
+
function thrownMessage(value) {
|
|
2559
|
+
try {
|
|
2560
|
+
const message = String(value);
|
|
2561
|
+
return message.length > 0 ? message : "LLM adapter failed";
|
|
2562
|
+
} catch (_hostileThrownValue) {
|
|
2563
|
+
return "LLM adapter failed";
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
/** Read a foreign error's own data-backed `code` without invoking accessors. */
|
|
2567
|
+
function ownErrorCode(error) {
|
|
2568
|
+
try {
|
|
2569
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "code");
|
|
2570
|
+
return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
|
|
2571
|
+
} catch (_sdkPropertyTrap) {
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
/** Snapshot an own data property without invoking an SDK-defined accessor. */
|
|
2576
|
+
function ownFailureSnapshot(error) {
|
|
2577
|
+
try {
|
|
2578
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
|
|
2579
|
+
return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
|
|
2580
|
+
} catch (_sdkPropertyTrap) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
/** Validate and detach an arbitrary serializable failure payload. */
|
|
2585
|
+
function failureSnapshot(value) {
|
|
2586
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2587
|
+
try {
|
|
2588
|
+
const candidate = value;
|
|
2589
|
+
const message = candidate.message;
|
|
2590
|
+
const code = candidate.code;
|
|
2591
|
+
const status = candidate.status;
|
|
2592
|
+
const providerRetryAfterMs = candidate.providerRetryAfterMs;
|
|
2593
|
+
const requestId = candidate.requestId;
|
|
2594
|
+
const offloadImages = candidate.offloadImages;
|
|
2595
|
+
if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
|
|
2596
|
+
return Object.freeze({
|
|
2597
|
+
message,
|
|
2598
|
+
code,
|
|
2599
|
+
...status === void 0 ? {} : { status },
|
|
2600
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
2601
|
+
...requestId === void 0 ? {} : { requestId },
|
|
2602
|
+
...offloadImages === void 0 ? {} : { offloadImages }
|
|
2603
|
+
});
|
|
2604
|
+
} catch (_sdkFailureGetter) {
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
/** Read an SDK error message without letting an accessor replace the primary failure. */
|
|
2609
|
+
function errorMessage(error) {
|
|
2610
|
+
try {
|
|
2611
|
+
const message = error.message;
|
|
2612
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
2613
|
+
} catch (_sdkMessageGetter) {}
|
|
2614
|
+
return "LLM adapter failed";
|
|
2615
|
+
}
|
|
2616
|
+
/** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
|
|
2617
|
+
function harnessErrorCode(error) {
|
|
2618
|
+
return error instanceof HarnessError ? error.code : "UNKNOWN";
|
|
2619
|
+
}
|
|
2620
|
+
function quoted(value) {
|
|
2621
|
+
return JSON.stringify(value);
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Stable text shown to a model that cannot accept one durable image reference.
|
|
2625
|
+
* @param ref - durable normalized attachment omitted from the request.
|
|
2626
|
+
* @returns deterministic text-only placeholder.
|
|
2627
|
+
*/
|
|
2628
|
+
function textOnlyImageText(ref) {
|
|
2629
|
+
return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
|
|
2630
|
+
}
|
|
2631
|
+
/**
|
|
2632
|
+
* True when typed model content contains an image block. This is the one image
|
|
2633
|
+
* walk shared by every image policy (capability gating, text-only
|
|
2634
|
+
* serialization, compaction survey), so a consumer cannot silently diverge.
|
|
2635
|
+
* @param content - typed model content blocks.
|
|
2636
|
+
* @returns whether any block is an image.
|
|
2637
|
+
*/
|
|
2638
|
+
function contentHasImage(content) {
|
|
2639
|
+
return content.some((block) => block.type === "image");
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* True when typed model content contains a file block.
|
|
2643
|
+
* Reads current content on every call without retaining scan results.
|
|
2644
|
+
* @param content - typed model content blocks.
|
|
2645
|
+
* @returns whether any block is a file.
|
|
2646
|
+
*/
|
|
2647
|
+
function contentHasFile(content) {
|
|
2648
|
+
for (const block of content) if (block.type === "file") return true;
|
|
2649
|
+
return false;
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Stable model-facing handle for one durable file reference: the address of
|
|
2653
|
+
* the verbatim stored copy and the instruction to read it on demand. This is
|
|
2654
|
+
* the only representation a provider ever receives for a file.
|
|
2655
|
+
* @param ref - durable verbatim file reference.
|
|
2656
|
+
* @param readonlyPath - execution-world path of the stored copy, when resolvable.
|
|
2657
|
+
* @returns deterministic handle text naming the file, its size, and its address.
|
|
2658
|
+
*/
|
|
2659
|
+
function fileHandleText(ref, readonlyPath) {
|
|
2660
|
+
const digest = String(ref.attachmentId).slice(7, 15);
|
|
2661
|
+
const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
|
|
2662
|
+
if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
|
|
2663
|
+
return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
|
|
2664
|
+
}
|
|
2665
|
+
/** Replace every file occurrence with handle text. */
|
|
2666
|
+
function replaceFilesWithHandles(blocks, resolvePath) {
|
|
2667
|
+
let next;
|
|
2668
|
+
for (const [index, block] of blocks.entries()) {
|
|
2669
|
+
if (block.type === "file") {
|
|
2670
|
+
next ??= blocks.slice(0, index);
|
|
2671
|
+
next.push({
|
|
2672
|
+
type: "text",
|
|
2673
|
+
text: fileHandleText(block.attachment, resolvePath(block.attachment))
|
|
2674
|
+
});
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2677
|
+
next?.push(block);
|
|
2678
|
+
}
|
|
2679
|
+
return next ?? blocks;
|
|
2680
|
+
}
|
|
2681
|
+
function projectFilesToText(messages, resolvePath) {
|
|
2682
|
+
if (!messages.some((message) => contentHasFile(message.content))) return messages;
|
|
2683
|
+
return messages.map((message) => {
|
|
2684
|
+
const content = replaceFilesWithHandles(message.content, resolvePath);
|
|
2685
|
+
return content === message.content ? message : {
|
|
2686
|
+
...message,
|
|
2687
|
+
content
|
|
2688
|
+
};
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
/** Replace every image occurrence for a text-only model. */
|
|
2692
|
+
function replaceImagesForTextModel(blocks) {
|
|
2693
|
+
let next;
|
|
2694
|
+
for (const [index, block] of blocks.entries()) {
|
|
2695
|
+
if (block.type === "image") {
|
|
2696
|
+
next ??= blocks.slice(0, index);
|
|
2697
|
+
next.push({
|
|
2698
|
+
type: "text",
|
|
2699
|
+
text: textOnlyImageText(block.attachment)
|
|
2700
|
+
});
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
next?.push(block);
|
|
2704
|
+
}
|
|
2705
|
+
return next ?? blocks;
|
|
2706
|
+
}
|
|
2707
|
+
function projectImagesForTextModel(messages) {
|
|
2708
|
+
if (!messages.some((message) => contentHasImage(message.content))) return messages;
|
|
2709
|
+
return messages.map((message) => {
|
|
2710
|
+
const content = replaceImagesForTextModel(message.content);
|
|
2711
|
+
return content === message.content ? message : {
|
|
2712
|
+
...message,
|
|
2713
|
+
content
|
|
2714
|
+
};
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
function withoutDeveloperMessages(messages) {
|
|
2718
|
+
const retained = messages.filter((message) => message.role !== "developer");
|
|
2719
|
+
return retained.length === messages.length ? messages : retained;
|
|
2720
|
+
}
|
|
2721
|
+
function toolDeclarations(tools, mode, history) {
|
|
2722
|
+
const declarations = new Map(history.tools.map((tool) => [tool.name, tool]));
|
|
2723
|
+
for (const update of history.updates) for (const tool of update.additions) if (!declarations.has(tool.name)) declarations.set(tool.name, {
|
|
2724
|
+
...tool,
|
|
2725
|
+
deferLoading: true
|
|
2726
|
+
});
|
|
2727
|
+
switch (mode) {
|
|
2728
|
+
case "in-history": return declarations;
|
|
2729
|
+
case "addition-only": {
|
|
2730
|
+
const activeNames = new Set(tools?.map((tool) => tool.name));
|
|
2731
|
+
for (const name of declarations.keys()) if (!activeNames.has(name)) declarations.delete(name);
|
|
2732
|
+
return declarations;
|
|
2733
|
+
}
|
|
2734
|
+
/* v8 ignore next 2 -- closed-union exhaustiveness guard */
|
|
2735
|
+
default: return assertNever(mode);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Construct provider declarations from session-folded history without changing logged active tools.
|
|
2740
|
+
* Unsupported routes and incomplete history use current declarations without developer updates.
|
|
2741
|
+
* Explicitly deferred baseline tools become available only after their first retained addition.
|
|
2742
|
+
* @param messages - complete request inputs, or the prefix selected for an auxiliary call.
|
|
2743
|
+
* @param tools - currently active tool schemas.
|
|
2744
|
+
* @param toolUpdate - the resolved route's update mode.
|
|
2745
|
+
* @param history - immutable state folded from committed headers and developer messages.
|
|
2746
|
+
* @returns provider declarations and the corresponding filtered history.
|
|
2747
|
+
*/
|
|
2748
|
+
function projectToolUpdates(messages, tools, toolUpdate, history) {
|
|
2749
|
+
if (toolUpdate === void 0) {
|
|
2750
|
+
let immediateTools = tools;
|
|
2751
|
+
if (tools?.some((tool) => tool.deferLoading === true)) immediateTools = tools.map(({ deferLoading: _loading, ...tool }) => tool);
|
|
2752
|
+
return {
|
|
2753
|
+
messages: withoutDeveloperMessages(messages),
|
|
2754
|
+
tools: immediateTools
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
if (history === void 0) return {
|
|
2758
|
+
messages: withoutDeveloperMessages(messages),
|
|
2759
|
+
tools
|
|
2760
|
+
};
|
|
2761
|
+
const messageIds = new Set(messages.flatMap((message) => message.role === "developer" ? [message.id] : []));
|
|
2762
|
+
if (history.updates.some((update) => !messageIds.has(update.messageId))) return {
|
|
2763
|
+
messages: withoutDeveloperMessages(messages),
|
|
2764
|
+
tools
|
|
2765
|
+
};
|
|
2766
|
+
const declarations = toolDeclarations(tools, toolUpdate, history);
|
|
2767
|
+
const updateIds = new Set(history.updates.map((update) => update.messageId));
|
|
2768
|
+
const offered = new Set(history.tools.filter((tool) => !tool.deferLoading).map((tool) => tool.name));
|
|
2769
|
+
const projectedMessages = [];
|
|
2770
|
+
for (const message of messages) {
|
|
2771
|
+
if (message.role !== "developer") {
|
|
2772
|
+
projectedMessages.push(message);
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
if (!updateIds.has(message.id)) continue;
|
|
2776
|
+
const content = message.content.filter((block) => {
|
|
2777
|
+
switch (block.type) {
|
|
2778
|
+
case "tool-addition":
|
|
2779
|
+
if (!declarations.has(block.toolName) || offered.has(block.toolName)) return false;
|
|
2780
|
+
offered.add(block.toolName);
|
|
2781
|
+
return true;
|
|
2782
|
+
case "tool-removal":
|
|
2783
|
+
if (toolUpdate !== "in-history") return false;
|
|
2784
|
+
return offered.delete(block.toolName);
|
|
2785
|
+
default: return true;
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
if (content.length === 0) continue;
|
|
2789
|
+
if (content.length === message.content.length) projectedMessages.push(message);
|
|
2790
|
+
else projectedMessages.push({
|
|
2791
|
+
...message,
|
|
2792
|
+
content
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
return {
|
|
2796
|
+
messages: projectedMessages.length === messages.length && projectedMessages.every((message, index) => message === messages[index]) ? messages : projectedMessages,
|
|
2797
|
+
tools: [...declarations.values()]
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
2802
|
+
* adapters from drifting. See
|
|
2803
|
+
* `docs/subsystems/llm-streaming.md#appidentity--app-attribution`.
|
|
2804
|
+
*
|
|
2805
|
+
* App-attribution vocabulary for provider requests.
|
|
2806
|
+
* @module @deepseek-ai/dsh-llm/attribution
|
|
2807
|
+
*/
|
|
2808
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
2809
|
+
/**
|
|
2810
|
+
* Brand an adapter-owned reasoning-effort identifier.
|
|
2811
|
+
* @param id - the opaque identifier exposed by one model capability.
|
|
2812
|
+
* @returns the same string, branded; no validation is performed.
|
|
2813
|
+
*/
|
|
2814
|
+
function ReasoningEffortId(id) {
|
|
2815
|
+
return brandString(id);
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Incremental chunk-to-message assembler. This is the single canonical assembly
|
|
2819
|
+
* algorithm used by the agent loop to build an assistant message from a chunk
|
|
2820
|
+
* stream while logging the raw chunks for replay fidelity.
|
|
2821
|
+
*
|
|
2822
|
+
* @module @deepseek-ai/dsh-llm/assembler
|
|
2823
|
+
*/
|
|
2824
|
+
/**
|
|
2825
|
+
* Incrementally assembles raw {@link StreamChunk}s into complete
|
|
2826
|
+
* {@link ContentBlock}s and a final assistant {@link Message}.
|
|
2827
|
+
*
|
|
2828
|
+
* The agent loop feeds it while logging raw chunks for replay fidelity, then
|
|
2829
|
+
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
|
|
2830
|
+
* or `interruptedBlocks()` when cancellation cut the stream short.
|
|
2831
|
+
*
|
|
2832
|
+
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
|
|
2833
|
+
* an index already closed by `block-end` are ignored (malformed stream) so a
|
|
2834
|
+
* misbehaving adapter cannot grow memory or corrupt a completed block.
|
|
2835
|
+
*/
|
|
2836
|
+
var BlockAssembler = class {
|
|
2837
|
+
partials = /* @__PURE__ */ new Map();
|
|
2838
|
+
order = [];
|
|
2839
|
+
_usage;
|
|
2840
|
+
_finish;
|
|
2841
|
+
_replayState;
|
|
2842
|
+
/**
|
|
2843
|
+
* Feed one chunk into the assembly state.
|
|
2844
|
+
* @param chunk - the next raw chunk, in stream order.
|
|
2845
|
+
*/
|
|
2846
|
+
push(chunk) {
|
|
2847
|
+
switch (chunk.type) {
|
|
2848
|
+
case "block-start":
|
|
2849
|
+
if (!this.partials.has(chunk.index)) {
|
|
2850
|
+
this.order.push(chunk.index);
|
|
2851
|
+
this.partials.set(chunk.index, {
|
|
2852
|
+
blockType: chunk.blockType,
|
|
2853
|
+
text: "",
|
|
2854
|
+
toolCallArguments: ""
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
return;
|
|
2858
|
+
case "text-delta":
|
|
2859
|
+
case "reasoning-delta": {
|
|
2860
|
+
const partial = this.ensure(chunk.index, chunk.type === "text-delta" ? "text" : "reasoning");
|
|
2861
|
+
if (partial.block) return;
|
|
2862
|
+
partial.text += chunk.text;
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
case "tool-call-delta": {
|
|
2866
|
+
const partial = this.ensure(chunk.index, "tool-call");
|
|
2867
|
+
if (partial.block) return;
|
|
2868
|
+
partial.toolCallId = chunk.id;
|
|
2869
|
+
if (chunk.name) partial.toolCallName = chunk.name;
|
|
2870
|
+
partial.toolCallArguments += chunk.argumentsDelta;
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
case "block-end": {
|
|
2874
|
+
const partial = this.ensure(chunk.index, chunk.block.type);
|
|
2875
|
+
if (partial.block) return;
|
|
2876
|
+
partial.block = chunk.block;
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
case "usage":
|
|
2880
|
+
this._usage = chunk.usage;
|
|
2881
|
+
return;
|
|
2882
|
+
case "finish":
|
|
2883
|
+
this._finish = chunk.reason;
|
|
2884
|
+
this._replayState = chunk.replayState;
|
|
2885
|
+
return;
|
|
2886
|
+
default: return assertNever(chunk, "BlockAssembler.push");
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
ensure(index, blockType) {
|
|
2890
|
+
let partial = this.partials.get(index);
|
|
2891
|
+
if (!partial) {
|
|
2892
|
+
partial = {
|
|
2893
|
+
blockType,
|
|
2894
|
+
text: "",
|
|
2895
|
+
toolCallArguments: ""
|
|
2896
|
+
};
|
|
2897
|
+
this.partials.set(index, partial);
|
|
2898
|
+
this.order.push(index);
|
|
2899
|
+
}
|
|
2900
|
+
return partial;
|
|
2901
|
+
}
|
|
2902
|
+
assemble(partial, index) {
|
|
2903
|
+
if (partial.block) return partial.block;
|
|
2904
|
+
switch (partial.blockType) {
|
|
2905
|
+
case "text": return {
|
|
2906
|
+
type: "text",
|
|
2907
|
+
text: partial.text
|
|
2908
|
+
};
|
|
2909
|
+
case "reasoning": return {
|
|
2910
|
+
type: "reasoning",
|
|
2911
|
+
text: partial.text
|
|
2912
|
+
};
|
|
2913
|
+
case "tool-call": return {
|
|
2914
|
+
type: "tool-call",
|
|
2915
|
+
id: partial.toolCallId ?? brandString(`call-${index}`),
|
|
2916
|
+
name: partial.toolCallName ?? "",
|
|
2917
|
+
arguments: partial.toolCallArguments
|
|
2918
|
+
};
|
|
2919
|
+
default: throw new Error(`cannot assemble incomplete block of type "${partial.blockType}"`);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
/** Invariant accessor: every index in `order` has a partial. */
|
|
2923
|
+
mustGet(index) {
|
|
2924
|
+
const partial = this.partials.get(index);
|
|
2925
|
+
if (!partial) throw new Error(`BlockAssembler invariant violated: no partial for index ${index}`);
|
|
2926
|
+
return partial;
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* The one shared keep/drop decision over all seen blocks: max-token
|
|
2930
|
+
* truncation drops tool calls that cannot be executed safely. Emitted blocks
|
|
2931
|
+
* and replay metadata both derive from this result, so they cannot disagree.
|
|
2932
|
+
*/
|
|
2933
|
+
assembled() {
|
|
2934
|
+
const all = this.order.map((index) => this.assemble(this.mustGet(index), index));
|
|
2935
|
+
const kept = this.finish.kind === "max-tokens" ? all.map((block) => block.type !== "tool-call") : void 0;
|
|
2936
|
+
const blocks = kept === void 0 ? all : all.filter((_, position) => kept[position]);
|
|
2937
|
+
const envelope = this._replayState;
|
|
2938
|
+
if (envelope?.blocks === void 0) return {
|
|
2939
|
+
blocks,
|
|
2940
|
+
replay: envelope
|
|
2941
|
+
};
|
|
2942
|
+
if (envelope.blocks.length !== all.length) return {
|
|
2943
|
+
blocks,
|
|
2944
|
+
replay: void 0
|
|
2945
|
+
};
|
|
2946
|
+
return {
|
|
2947
|
+
blocks,
|
|
2948
|
+
replay: kept === void 0 || blocks.length === all.length ? envelope : {
|
|
2949
|
+
response: envelope.response,
|
|
2950
|
+
blocks: envelope.blocks.filter((_, position) => kept[position])
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Assemble all blocks seen so far, in stream order.
|
|
2956
|
+
* @returns one block per seen index, except that max-token truncation drops
|
|
2957
|
+
* tool calls that cannot be executed safely; an open block assembles from
|
|
2958
|
+
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
|
|
2959
|
+
*/
|
|
2960
|
+
blocks() {
|
|
2961
|
+
return this.assembled().blocks;
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Assemble the prefix an interrupted stream can safely finalize: closed and
|
|
2965
|
+
* open text/reasoning blocks with non-whitespace content, in stream order.
|
|
2966
|
+
* Tool calls are omitted because interruption precedes dispatch; retaining
|
|
2967
|
+
* one would require a fabricated result. Open unknown blocks are also omitted.
|
|
2968
|
+
* @returns the kept blocks; empty when nothing streamed before the interruption.
|
|
2969
|
+
*/
|
|
2970
|
+
interruptedBlocks() {
|
|
2971
|
+
return this.order.map((index) => {
|
|
2972
|
+
const partial = this.mustGet(index);
|
|
2973
|
+
const type = partial.block?.type ?? partial.blockType;
|
|
2974
|
+
if (type !== "text" && type !== "reasoning") return void 0;
|
|
2975
|
+
return this.assemble(partial, index);
|
|
2976
|
+
}).filter((block) => (block?.type === "text" || block?.type === "reasoning") && block.text.trim() !== "");
|
|
2977
|
+
}
|
|
2978
|
+
/** Usage from the `usage` chunk; undefined until one arrives. */
|
|
2979
|
+
get usage() {
|
|
2980
|
+
return this._usage;
|
|
2981
|
+
}
|
|
2982
|
+
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
|
|
2983
|
+
get finish() {
|
|
2984
|
+
return this._finish ?? { kind: "stop" };
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Replay metadata from the terminal finish chunk, if any, with per-block
|
|
2988
|
+
* entries pruned in step with {@link blocks}. Undefined when the envelope's
|
|
2989
|
+
* entries do not align with the emitted blocks.
|
|
2990
|
+
*/
|
|
2991
|
+
get replayState() {
|
|
2992
|
+
return this.assembled().replay;
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* The assembled assistant message.
|
|
2996
|
+
* @param source - provider/model attribution (without the `kind` tag) for the assembled message.
|
|
2997
|
+
* @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
|
|
2998
|
+
*/
|
|
2999
|
+
message(source) {
|
|
3000
|
+
return createAssistantMessage({
|
|
3001
|
+
content: this.blocks(),
|
|
3002
|
+
source
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
/**
|
|
3007
|
+
* LLM service: adapter registry with a waterfall-interceptable streaming call
|
|
3008
|
+
* API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
|
|
3009
|
+
* provider backends, and `BlockAssembler` for chunk assembly.
|
|
3010
|
+
*
|
|
3011
|
+
* @module @deepseek-ai/dsh-llm
|
|
3012
|
+
*/
|
|
3013
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
3014
|
+
var useValue = arguments.length > 2;
|
|
3015
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
3016
|
+
return useValue ? value : void 0;
|
|
3017
|
+
};
|
|
3018
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
3019
|
+
function accept(f) {
|
|
3020
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
3021
|
+
return f;
|
|
3022
|
+
}
|
|
3023
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
3024
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
3025
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
3026
|
+
var _, done = false;
|
|
3027
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
3028
|
+
var context = {};
|
|
3029
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
3030
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
3031
|
+
context.addInitializer = function(f) {
|
|
3032
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
3033
|
+
extraInitializers.push(accept(f || null));
|
|
3034
|
+
};
|
|
3035
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
3036
|
+
get: descriptor.get,
|
|
3037
|
+
set: descriptor.set
|
|
3038
|
+
} : descriptor[key], context);
|
|
3039
|
+
if (kind === "accessor") {
|
|
3040
|
+
if (result === void 0) continue;
|
|
3041
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
3042
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
3043
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
3044
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
3045
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
3046
|
+
else descriptor[key] = _;
|
|
3047
|
+
}
|
|
3048
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
3049
|
+
done = true;
|
|
3050
|
+
};
|
|
3051
|
+
/**
|
|
3052
|
+
* Typed error for LLM-related failures. Extends {@link HarnessError}, so the
|
|
3053
|
+
* `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
|
|
3054
|
+
*/
|
|
3055
|
+
var LlmError = class extends HarnessError {
|
|
3056
|
+
/** Serializable facts retained beside this live Error. */
|
|
3057
|
+
failure;
|
|
3058
|
+
/**
|
|
3059
|
+
* @param message - non-empty human-readable failure summary.
|
|
3060
|
+
* @param code - non-empty stable provider-neutral machine code.
|
|
3061
|
+
* @param options - optional cause and validated serializable provider facts.
|
|
3062
|
+
*/
|
|
3063
|
+
constructor(message, code, options) {
|
|
3064
|
+
if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
|
|
3065
|
+
if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
|
|
3066
|
+
if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
|
|
3067
|
+
if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
|
|
3068
|
+
if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
|
|
3069
|
+
super(message, code, options);
|
|
3070
|
+
this.name = "LlmError";
|
|
3071
|
+
this.failure = Object.freeze({
|
|
3072
|
+
message,
|
|
3073
|
+
code,
|
|
3074
|
+
...options?.status === void 0 ? {} : { status: options.status },
|
|
3075
|
+
...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
|
|
3076
|
+
...options?.requestId === void 0 ? {} : { requestId: options.requestId },
|
|
3077
|
+
...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
(() => {
|
|
3082
|
+
let _classSuper = TypertRemoteService;
|
|
3083
|
+
let _instanceExtraInitializers = [];
|
|
3084
|
+
let _listProviders_decorators;
|
|
3085
|
+
let _listConfigurableProviders_decorators;
|
|
3086
|
+
let _remoteDiscoverModels_decorators;
|
|
3087
|
+
return class LlmRuntime extends _classSuper {
|
|
3088
|
+
static {
|
|
3089
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
3090
|
+
_listProviders_decorators = [Remote];
|
|
3091
|
+
_listConfigurableProviders_decorators = [Remote];
|
|
3092
|
+
_remoteDiscoverModels_decorators = [Remote("discoverModels")];
|
|
3093
|
+
__esDecorate(this, null, _listProviders_decorators, {
|
|
3094
|
+
kind: "method",
|
|
3095
|
+
name: "listProviders",
|
|
3096
|
+
static: false,
|
|
3097
|
+
private: false,
|
|
3098
|
+
access: {
|
|
3099
|
+
has: (obj) => "listProviders" in obj,
|
|
3100
|
+
get: (obj) => obj.listProviders
|
|
3101
|
+
},
|
|
3102
|
+
metadata: _metadata
|
|
3103
|
+
}, null, _instanceExtraInitializers);
|
|
3104
|
+
__esDecorate(this, null, _listConfigurableProviders_decorators, {
|
|
3105
|
+
kind: "method",
|
|
3106
|
+
name: "listConfigurableProviders",
|
|
3107
|
+
static: false,
|
|
3108
|
+
private: false,
|
|
3109
|
+
access: {
|
|
3110
|
+
has: (obj) => "listConfigurableProviders" in obj,
|
|
3111
|
+
get: (obj) => obj.listConfigurableProviders
|
|
3112
|
+
},
|
|
3113
|
+
metadata: _metadata
|
|
3114
|
+
}, null, _instanceExtraInitializers);
|
|
3115
|
+
__esDecorate(this, null, _remoteDiscoverModels_decorators, {
|
|
3116
|
+
kind: "method",
|
|
3117
|
+
name: "remoteDiscoverModels",
|
|
3118
|
+
static: false,
|
|
3119
|
+
private: false,
|
|
3120
|
+
access: {
|
|
3121
|
+
has: (obj) => "remoteDiscoverModels" in obj,
|
|
3122
|
+
get: (obj) => obj.remoteDiscoverModels
|
|
3123
|
+
},
|
|
3124
|
+
metadata: _metadata
|
|
3125
|
+
}, null, _instanceExtraInitializers);
|
|
3126
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
3127
|
+
enumerable: true,
|
|
3128
|
+
configurable: true,
|
|
3129
|
+
writable: true,
|
|
3130
|
+
value: _metadata
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
|
|
3134
|
+
directory = /* @__PURE__ */ new Map();
|
|
3135
|
+
discoveries = /* @__PURE__ */ new Map();
|
|
3136
|
+
constructor(ctx) {
|
|
3137
|
+
super(ctx, "llm");
|
|
3138
|
+
}
|
|
3139
|
+
/** Notify topology observers without letting one broken listener veto the commit. */
|
|
3140
|
+
emitAdaptersUpdated() {
|
|
3141
|
+
for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
|
|
3142
|
+
const returned = listener();
|
|
3143
|
+
if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
|
|
3144
|
+
this.warnAdaptersListenerFailure(error);
|
|
3145
|
+
});
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
this.warnAdaptersListenerFailure(error);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
/** Contained-listener diagnostic shared by the sync and async failure paths. */
|
|
3151
|
+
warnAdaptersListenerFailure(error) {
|
|
3152
|
+
this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
|
|
3153
|
+
this.ctx.logger.warn(error);
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Register an adapter for the given provider routes. Throws `LlmError` with code
|
|
3157
|
+
* `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
|
|
3158
|
+
* Disposed with the fiber.
|
|
3159
|
+
* @param providers - every provider route this adapter should serve.
|
|
3160
|
+
* @param adapter - the adapter that streams calls for those providers.
|
|
3161
|
+
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
|
|
3162
|
+
*/
|
|
3163
|
+
registerAdapter(providers, adapter) {
|
|
3164
|
+
const owned = /* @__PURE__ */ new Set();
|
|
3165
|
+
let released = false;
|
|
3166
|
+
const dispose = this.ctx.effect(function* () {
|
|
3167
|
+
if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
|
|
3168
|
+
this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
|
|
3169
|
+
yield () => {
|
|
3170
|
+
released = true;
|
|
3171
|
+
for (const provider of owned) this.adapters.delete(provider);
|
|
3172
|
+
owned.clear();
|
|
3173
|
+
this.emitAdaptersUpdated();
|
|
3174
|
+
};
|
|
3175
|
+
}.bind(this), "llm.registerAdapter()");
|
|
3176
|
+
const handle = (() => void dispose());
|
|
3177
|
+
handle.replace = (next) => {
|
|
3178
|
+
if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
|
|
3179
|
+
this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
|
|
3180
|
+
};
|
|
3181
|
+
return handle;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Validate one candidate route set for `adapter`, treating routes this
|
|
3185
|
+
* registration already holds as available. Nothing is mutated: a rejected
|
|
3186
|
+
* candidate leaves the registry exactly as it was.
|
|
3187
|
+
*/
|
|
3188
|
+
prepareRoutes(providers, adapter, owned) {
|
|
3189
|
+
const unique = /* @__PURE__ */ new Set();
|
|
3190
|
+
const registrations = [];
|
|
3191
|
+
for (const provider of providers) {
|
|
3192
|
+
if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
|
|
3193
|
+
if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
|
|
3194
|
+
const info = adapter.providerInfo(provider);
|
|
3195
|
+
if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
|
|
3196
|
+
unique.add(provider);
|
|
3197
|
+
const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
|
|
3198
|
+
registrations.push({
|
|
3199
|
+
adapter,
|
|
3200
|
+
provider: {
|
|
3201
|
+
id: info.id,
|
|
3202
|
+
name: info.name
|
|
3203
|
+
},
|
|
3204
|
+
retryPolicy
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
return registrations;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Swap this registration's routes for the prepared ones in one synchronous
|
|
3211
|
+
* section, so no observer can see the registry between the release and the
|
|
3212
|
+
* re-registration. The route set's one mutation point is also where
|
|
3213
|
+
* `llm/adapters-updated` is published, so a `replace` announces itself
|
|
3214
|
+
* exactly like a first registration.
|
|
3215
|
+
*/
|
|
3216
|
+
commitRoutes(owned, registrations) {
|
|
3217
|
+
for (const provider of owned) this.adapters.delete(provider);
|
|
3218
|
+
owned.clear();
|
|
3219
|
+
for (const registration of registrations) {
|
|
3220
|
+
this.adapters.set(registration.provider.id, registration);
|
|
3221
|
+
owned.add(registration.provider.id);
|
|
3222
|
+
}
|
|
3223
|
+
this.emitAdaptersUpdated();
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Describe provider routes with a registered adapter.
|
|
3227
|
+
* @returns detached provider metadata in registration order.
|
|
3228
|
+
*/
|
|
3229
|
+
listProviders() {
|
|
3230
|
+
return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Declare provider routes an adapter plugin can activate through
|
|
3234
|
+
* configuration. Registration is all-or-nothing: an empty list, invalid
|
|
3235
|
+
* entry, or a provider already declared by any registration throws
|
|
3236
|
+
* `LlmError` without registering the rest. Disposed with the fiber.
|
|
3237
|
+
* @param entries - every configurable provider this plugin owns.
|
|
3238
|
+
* @returns a handle that withdraws all of them, and can atomically replace them.
|
|
3239
|
+
*/
|
|
3240
|
+
registerConfigurableProviders(entries) {
|
|
3241
|
+
let held = [];
|
|
3242
|
+
let disposed = false;
|
|
3243
|
+
/**
|
|
3244
|
+
* Validate a candidate set in full against everything this registration
|
|
3245
|
+
* does not already hold, then publish it. Nothing is written until the
|
|
3246
|
+
* whole set passes, so a refused candidate leaves the current entries in
|
|
3247
|
+
* place — the property that makes `replace` a swap rather than a
|
|
3248
|
+
* delete-then-add that can strand the directory empty.
|
|
3249
|
+
*/
|
|
3250
|
+
const commit = (candidates) => {
|
|
3251
|
+
const detached = [];
|
|
3252
|
+
const own = new Set(held.map((entry) => entry.provider));
|
|
3253
|
+
for (const entry of candidates) {
|
|
3254
|
+
if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
|
|
3255
|
+
if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
|
|
3256
|
+
if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
|
|
3257
|
+
detached.push(detachDirectoryEntry(entry));
|
|
3258
|
+
}
|
|
3259
|
+
for (const entry of held) this.directory.delete(entry.provider);
|
|
3260
|
+
for (const entry of detached) this.directory.set(entry.provider, entry);
|
|
3261
|
+
held = detached;
|
|
3262
|
+
this.emitAdaptersUpdated();
|
|
3263
|
+
};
|
|
3264
|
+
const dispose = this.ctx.effect(function* () {
|
|
3265
|
+
if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
|
|
3266
|
+
commit(entries);
|
|
3267
|
+
yield () => {
|
|
3268
|
+
disposed = true;
|
|
3269
|
+
for (const entry of held) this.directory.delete(entry.provider);
|
|
3270
|
+
held = [];
|
|
3271
|
+
this.emitAdaptersUpdated();
|
|
3272
|
+
};
|
|
3273
|
+
}.bind(this), "llm.registerConfigurableProviders()");
|
|
3274
|
+
const handle = (() => void dispose());
|
|
3275
|
+
handle.replace = (next) => {
|
|
3276
|
+
if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
|
|
3277
|
+
commit(next);
|
|
3278
|
+
};
|
|
3279
|
+
return handle;
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* List every declared configurable provider, registered or dormant.
|
|
3283
|
+
* @returns detached directory entries in declaration order.
|
|
3284
|
+
*/
|
|
3285
|
+
listConfigurableProviders() {
|
|
3286
|
+
return [...this.directory.values()].map(detachDirectoryEntry);
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Offer to interrogate provider endpoints on behalf of the settings
|
|
3290
|
+
* namespace this plugin owns. The namespace is the key because that is what
|
|
3291
|
+
* a configuration surface already holds from the configurable-provider
|
|
3292
|
+
* directory, and because a provider being *added* has no route to name yet.
|
|
3293
|
+
* Disposed with the fiber.
|
|
3294
|
+
* @param settingsNs - the namespace whose profiles this discovery serves.
|
|
3295
|
+
* @param discover - interrogates one endpoint and must honor the supplied signal.
|
|
3296
|
+
* @returns the disposer that withdraws the offer.
|
|
3297
|
+
*/
|
|
3298
|
+
registerModelDiscovery(settingsNs, discover) {
|
|
3299
|
+
const dispose = this.ctx.effect(function* () {
|
|
3300
|
+
if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
|
|
3301
|
+
if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
|
|
3302
|
+
this.discoveries.set(settingsNs, discover);
|
|
3303
|
+
yield () => {
|
|
3304
|
+
this.discoveries.delete(settingsNs);
|
|
3305
|
+
};
|
|
3306
|
+
}.bind(this), "llm.registerModelDiscovery()");
|
|
3307
|
+
return () => void dispose();
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Interrogate one provider endpoint for the models it advertises. The
|
|
3311
|
+
* request describes a draft, not a stored route, so nothing here reads or
|
|
3312
|
+
* writes settings or credentials — the caller owns both, and the reply is
|
|
3313
|
+
* candidate metadata a surface may offer for adoption.
|
|
3314
|
+
* @param settingsNs - namespace whose registered discovery serves this draft.
|
|
3315
|
+
* @param request - the endpoint, protocol, and one-shot credential to use.
|
|
3316
|
+
* @param signal - caller cancellation.
|
|
3317
|
+
* @returns the advertised models, deduplicated in endpoint order.
|
|
3318
|
+
*/
|
|
3319
|
+
async discoverModels(settingsNs, request, signal) {
|
|
3320
|
+
const discover = this.discoveries.get(settingsNs);
|
|
3321
|
+
if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
|
|
3322
|
+
if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
|
|
3323
|
+
const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
|
|
3324
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3325
|
+
const models = [];
|
|
3326
|
+
for (const model of discovered) {
|
|
3327
|
+
if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
|
|
3328
|
+
seen.add(model.id);
|
|
3329
|
+
models.push({
|
|
3330
|
+
id: model.id,
|
|
3331
|
+
...model.name === void 0 ? {} : { name: model.name },
|
|
3332
|
+
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
3333
|
+
...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
|
|
3334
|
+
...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
return models;
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* Remote adapter for one draft provider interrogation.
|
|
3341
|
+
* @param settingsNs - namespace whose registered discovery serves this draft.
|
|
3342
|
+
* @param request - endpoint, protocol, and one-shot credential to use.
|
|
3343
|
+
* @param signal - caller cancellation supplied by the Remote carrier.
|
|
3344
|
+
* @returns advertised models in endpoint order.
|
|
3345
|
+
* @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
|
|
3346
|
+
*/
|
|
3347
|
+
async remoteDiscoverModels(settingsNs, request, signal) {
|
|
3348
|
+
try {
|
|
3349
|
+
return await this.discoverModels(settingsNs, request, signal);
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
|
|
3352
|
+
settingsNs,
|
|
3353
|
+
...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
|
|
3354
|
+
}, { cause: error });
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Resolve the retry policy captured when one provider route was registered.
|
|
3359
|
+
* @param provider - registered provider route to inspect.
|
|
3360
|
+
* @returns the provider-owned policy, with normal defaults already resolved.
|
|
3361
|
+
*/
|
|
3362
|
+
providerRetryPolicy(provider) {
|
|
3363
|
+
return this.registration(provider).retryPolicy;
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Resolve provider-side request-image pricing for one exact route, or
|
|
3367
|
+
* `undefined` when the provider is unregistered or declares none. Unknown
|
|
3368
|
+
* providers degrade to `undefined` rather than throwing because callers
|
|
3369
|
+
* price durable history whose route may no longer be mounted.
|
|
3370
|
+
* @param provider - provider route named by a request header.
|
|
3371
|
+
* @param model - exact model id named by the same header.
|
|
3372
|
+
* @returns the owning adapter's image pricing for the route, when declared.
|
|
3373
|
+
*/
|
|
3374
|
+
imageRequestPricing(provider, model) {
|
|
3375
|
+
return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Resolve the image projection for an exact registered model route.
|
|
3379
|
+
* @param provider - registered provider route.
|
|
3380
|
+
* @param model - exact model id.
|
|
3381
|
+
* @param image - normalized image reference.
|
|
3382
|
+
* @returns dimensions and byte target used by the adapter's request serializer.
|
|
3383
|
+
*/
|
|
3384
|
+
imageRequestTarget(provider, model, image) {
|
|
3385
|
+
return this.registration(provider).adapter.imageRequestTarget(provider, model, image);
|
|
3386
|
+
}
|
|
3387
|
+
/**
|
|
3388
|
+
* Resolve the exact text one durable file occurrence contributes to every
|
|
3389
|
+
* provider request in the current execution environment.
|
|
3390
|
+
* @param ref - durable verbatim file reference from model history.
|
|
3391
|
+
* @returns the same deterministic handle text used at adapter dispatch.
|
|
3392
|
+
*/
|
|
3393
|
+
fileRequestText(ref) {
|
|
3394
|
+
return fileHandleText(ref, this.fileReadPath(ref));
|
|
3395
|
+
}
|
|
3396
|
+
/** Detach typed adapter-owned modality metadata. */
|
|
3397
|
+
detachedModalities(modalities) {
|
|
3398
|
+
return modalities === void 0 ? void 0 : [...modalities];
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Discover models advertised by one registered provider. Catalog membership
|
|
3402
|
+
* does not constrain core routing. Catalog-driven entry points may restrict
|
|
3403
|
+
* selection and submission to the advertised models.
|
|
3404
|
+
* @param provider - registered provider route to inspect.
|
|
3405
|
+
* @returns detached model metadata in adapter-preferred order.
|
|
3406
|
+
*/
|
|
3407
|
+
async listModels(provider) {
|
|
3408
|
+
const models = await this.registration(provider).adapter.listModels(provider);
|
|
3409
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3410
|
+
return models.map((model) => {
|
|
3411
|
+
if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
|
|
3412
|
+
seen.add(model.id);
|
|
3413
|
+
const inputModalities = this.detachedModalities(model.inputModalities);
|
|
3414
|
+
return {
|
|
3415
|
+
provider: model.provider,
|
|
3416
|
+
id: model.id,
|
|
3417
|
+
name: model.name,
|
|
3418
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
3419
|
+
...inputModalities === void 0 ? {} : { inputModalities }
|
|
3420
|
+
};
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Resolve and validate all metadata from the adapter that owns one exact
|
|
3425
|
+
* route. The result is detached from adapter-owned objects; catalog
|
|
3426
|
+
* membership remains advisory and does not control request routing.
|
|
3427
|
+
* @param provider - registered provider route to inspect.
|
|
3428
|
+
* @param model - exact model id passed to the adapter.
|
|
3429
|
+
* @param signal - optional cancellation for adapter-owned asynchronous lookup.
|
|
3430
|
+
* @returns exact model identity plus available context and reasoning metadata.
|
|
3431
|
+
*/
|
|
3432
|
+
async resolveModelInfo(provider, model, signal) {
|
|
3433
|
+
return this.resolveModelInfoFor(this.registration(provider), model, signal);
|
|
3434
|
+
}
|
|
3435
|
+
async resolveModelInfoFor(registration, model, signal) {
|
|
3436
|
+
const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
|
|
3437
|
+
return this.normalizeModelInfo(registration, model, resolved);
|
|
3438
|
+
}
|
|
3439
|
+
/** Validate and detach one adapter-returned exact model result. */
|
|
3440
|
+
normalizeModelInfo(registration, model, resolved) {
|
|
3441
|
+
const provider = registration.provider.id;
|
|
3442
|
+
if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
3443
|
+
const context = resolved.context;
|
|
3444
|
+
if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
|
|
3445
|
+
const inputModalities = this.detachedModalities(resolved.inputModalities);
|
|
3446
|
+
const systemPromptUpdate = resolved.systemPromptUpdate;
|
|
3447
|
+
if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
3448
|
+
const toolUpdate = resolved.toolUpdate;
|
|
3449
|
+
if (toolUpdate !== void 0 && toolUpdate !== "in-history" && toolUpdate !== "addition-only") throw new LlmError(`adapter returned invalid tool update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
|
|
3450
|
+
const defaultMaxTokens = resolved.defaultMaxTokens;
|
|
3451
|
+
if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
|
|
3452
|
+
const info = {
|
|
3453
|
+
provider,
|
|
3454
|
+
id: model,
|
|
3455
|
+
name: resolved.name,
|
|
3456
|
+
...resolved.description === void 0 ? {} : { description: resolved.description },
|
|
3457
|
+
...inputModalities === void 0 ? {} : { inputModalities },
|
|
3458
|
+
...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
|
|
3459
|
+
...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
|
|
3460
|
+
...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate },
|
|
3461
|
+
...resolved.toolUpdate === void 0 ? {} : { toolUpdate: resolved.toolUpdate }
|
|
3462
|
+
};
|
|
3463
|
+
const reasoning = resolved.reasoning;
|
|
3464
|
+
if (reasoning === void 0) return info;
|
|
3465
|
+
if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
3466
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3467
|
+
const efforts = reasoning.efforts.map((effort) => {
|
|
3468
|
+
if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
3469
|
+
seen.add(effort.id);
|
|
3470
|
+
return {
|
|
3471
|
+
id: effort.id,
|
|
3472
|
+
name: effort.name,
|
|
3473
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
3474
|
+
};
|
|
3475
|
+
});
|
|
3476
|
+
if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
|
|
3477
|
+
return {
|
|
3478
|
+
...info,
|
|
3479
|
+
reasoning: {
|
|
3480
|
+
efforts,
|
|
3481
|
+
...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Validate a conversation call config against its exact model capability and
|
|
3487
|
+
* materialize adapter-configured defaults. Unsupported explicit efforts
|
|
3488
|
+
* reject before provider I/O; no clamping or aliasing is performed. This
|
|
3489
|
+
* standalone query does not bind a later dispatch; use {@link prepareCall}
|
|
3490
|
+
* when logging and streaming must share one adapter registration.
|
|
3491
|
+
* @param config - provider/model route and optional request controls.
|
|
3492
|
+
* @param signal - optional cancellation for adapter-owned capability lookup.
|
|
3493
|
+
* @returns a detached config only when a default must be materialized.
|
|
3494
|
+
*/
|
|
3495
|
+
async resolveCallConfig(config, signal) {
|
|
3496
|
+
return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
|
|
3497
|
+
}
|
|
3498
|
+
async resolveCallFor(registration, config, signal) {
|
|
3499
|
+
const info = await this.resolveModelInfoFor(registration, config.model, signal);
|
|
3500
|
+
return this.resolveCallWithInfo(config, info);
|
|
3501
|
+
}
|
|
3502
|
+
/** Validate request controls against one already-bound exact model result. */
|
|
3503
|
+
resolveCallWithInfo(config, info) {
|
|
3504
|
+
const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
|
|
3505
|
+
...config,
|
|
3506
|
+
maxTokens: info.defaultMaxTokens
|
|
3507
|
+
} : config;
|
|
3508
|
+
const reasoning = info.reasoning;
|
|
3509
|
+
const requested = defaulted.reasoningEffort;
|
|
3510
|
+
let resolvedConfig = defaulted;
|
|
3511
|
+
if (reasoning === void 0) {
|
|
3512
|
+
if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
3513
|
+
} else {
|
|
3514
|
+
const effective = requested ?? reasoning.defaultEffort;
|
|
3515
|
+
if (effective !== void 0) {
|
|
3516
|
+
if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
|
|
3517
|
+
if (requested !== effective) resolvedConfig = {
|
|
3518
|
+
...defaulted,
|
|
3519
|
+
reasoningEffort: effective
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
return {
|
|
3524
|
+
config: resolvedConfig,
|
|
3525
|
+
...info.context === void 0 ? {} : { context: info.context },
|
|
3526
|
+
modelInfo: info
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
/**
|
|
3530
|
+
* Resolve one call under its current adapter registration. The returned
|
|
3531
|
+
* one-shot handle keeps that registration across header logging and dispatch,
|
|
3532
|
+
* so HMR cannot combine one adapter's capability result with another adapter.
|
|
3533
|
+
* An optional synchronous callback selects concrete controls using captured
|
|
3534
|
+
* model metadata. Defaults and validation apply to its result. Callback
|
|
3535
|
+
* failures and cancellation reject preparation before dispatch.
|
|
3536
|
+
* @param config - provider/model route and optional concrete request controls.
|
|
3537
|
+
* @param signal - optional cancellation for adapter-owned capability lookup.
|
|
3538
|
+
* @param configure - pure control selection, called once before defaults and validation.
|
|
3539
|
+
* @returns a prepared config and its registration-bound stream entry point.
|
|
3540
|
+
*/
|
|
3541
|
+
async prepareCall(config, signal, configure) {
|
|
3542
|
+
signal?.throwIfAborted();
|
|
3543
|
+
const { provider, model, ...controls } = config;
|
|
3544
|
+
const registration = this.registration(provider);
|
|
3545
|
+
const adapterCall = await registration.adapter.prepareCall(provider, model, signal);
|
|
3546
|
+
signal?.throwIfAborted();
|
|
3547
|
+
const modelInfo = this.normalizeModelInfo(registration, model, adapterCall.model);
|
|
3548
|
+
const proposed = configure === void 0 ? config : {
|
|
3549
|
+
...configure(deepFreeze(structuredClone(controls)), deepFreeze(modelInfo)),
|
|
3550
|
+
provider,
|
|
3551
|
+
model
|
|
3552
|
+
};
|
|
3553
|
+
signal?.throwIfAborted();
|
|
3554
|
+
const resolved = this.resolveCallWithInfo(proposed, modelInfo);
|
|
3555
|
+
const resolvedConfig = deepFreeze(structuredClone(resolved.config));
|
|
3556
|
+
const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
|
|
3557
|
+
const adapterDefaults = deepFreeze({
|
|
3558
|
+
...proposed.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
|
|
3559
|
+
...proposed.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
|
|
3560
|
+
});
|
|
3561
|
+
let dispatched = false;
|
|
3562
|
+
return Object.freeze({
|
|
3563
|
+
config: resolvedConfig,
|
|
3564
|
+
retryPolicy: registration.retryPolicy,
|
|
3565
|
+
adapterDefaults,
|
|
3566
|
+
...context === void 0 ? {} : { context },
|
|
3567
|
+
...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
|
|
3568
|
+
...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
|
|
3569
|
+
...modelInfo.toolUpdate === void 0 ? {} : { toolUpdate: modelInfo.toolUpdate },
|
|
3570
|
+
stream: (options) => {
|
|
3571
|
+
if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
|
|
3572
|
+
if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
3573
|
+
dispatched = true;
|
|
3574
|
+
return this.streamWithRegistration(options, {
|
|
3575
|
+
registration,
|
|
3576
|
+
config: resolvedConfig,
|
|
3577
|
+
modelInfo,
|
|
3578
|
+
dispatch: (options) => adapterCall.stream(options)
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3583
|
+
registration(provider) {
|
|
3584
|
+
const registration = this.adapters.get(provider);
|
|
3585
|
+
if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
|
|
3586
|
+
return registration;
|
|
3587
|
+
}
|
|
3588
|
+
/** Remove replay state whose historical route is owned by another adapter. */
|
|
3589
|
+
forAdapter(options, adapter) {
|
|
3590
|
+
const messages = options.messages.map((message) => {
|
|
3591
|
+
if (message.role !== "assistant") return message;
|
|
3592
|
+
const source = message.source;
|
|
3593
|
+
if (source.replayState === void 0) return message;
|
|
3594
|
+
if (this.adapters.get(source.provider)?.adapter === adapter) return message;
|
|
3595
|
+
return freezeMessage({
|
|
3596
|
+
...message,
|
|
3597
|
+
source: {
|
|
3598
|
+
kind: "model",
|
|
3599
|
+
provider: source.provider,
|
|
3600
|
+
model: source.model
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
3603
|
+
});
|
|
3604
|
+
if (messages.every((message, index) => message === options.messages[index])) return options;
|
|
3605
|
+
const filtered = {
|
|
3606
|
+
...options,
|
|
3607
|
+
messages
|
|
3608
|
+
};
|
|
3609
|
+
return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
|
|
3610
|
+
}
|
|
3611
|
+
/**
|
|
3612
|
+
* Resolve the current execution-world read path of one durable file
|
|
3613
|
+
* reference through the mounted attachment and filesystem providers.
|
|
3614
|
+
*/
|
|
3615
|
+
fileReadPath(ref) {
|
|
3616
|
+
let hostPath;
|
|
3617
|
+
try {
|
|
3618
|
+
hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
|
|
3619
|
+
} catch {
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3622
|
+
if (hostPath === void 0) return void 0;
|
|
3623
|
+
return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Final adapter boundary. Adapter selection, dispatch, iterator construction,
|
|
3627
|
+
* and iteration failures become one terminal failure chunk. Middleware and
|
|
3628
|
+
* downstream consumer failures remain thrown plugin or consumer errors.
|
|
3629
|
+
*/
|
|
3630
|
+
async *adapterStream(options, prepared) {
|
|
3631
|
+
let iterator;
|
|
3632
|
+
try {
|
|
3633
|
+
const registration = prepared?.registration ?? this.registration(options.provider);
|
|
3634
|
+
const adapter = registration.adapter;
|
|
3635
|
+
let modelInfo;
|
|
3636
|
+
let resolvedConfig;
|
|
3637
|
+
let dispatch;
|
|
3638
|
+
if (prepared === void 0) {
|
|
3639
|
+
const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
|
|
3640
|
+
modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
|
|
3641
|
+
resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
|
|
3642
|
+
dispatch = (options) => adapterCall.stream(options);
|
|
3643
|
+
} else {
|
|
3644
|
+
modelInfo = prepared.modelInfo;
|
|
3645
|
+
resolvedConfig = prepared.config;
|
|
3646
|
+
dispatch = prepared.dispatch;
|
|
3647
|
+
}
|
|
3648
|
+
if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
|
|
3649
|
+
const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
|
|
3650
|
+
...options,
|
|
3651
|
+
...resolvedConfig
|
|
3652
|
+
}) : {
|
|
3653
|
+
...options,
|
|
3654
|
+
...resolvedConfig
|
|
3655
|
+
};
|
|
3656
|
+
let projectedMessages = resolvedOptions.messages;
|
|
3657
|
+
if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
|
|
3658
|
+
if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
|
|
3659
|
+
const projectedTools = projectToolUpdates(projectedMessages, resolvedOptions.tools, modelInfo.toolUpdate, resolvedOptions.toolHistory);
|
|
3660
|
+
projectedMessages = projectedTools.messages;
|
|
3661
|
+
let projectedOptions = resolvedOptions;
|
|
3662
|
+
if (projectedMessages !== resolvedOptions.messages || projectedTools.tools !== resolvedOptions.tools) {
|
|
3663
|
+
projectedOptions = {
|
|
3664
|
+
...resolvedOptions,
|
|
3665
|
+
messages: projectedMessages,
|
|
3666
|
+
...projectedTools.tools === void 0 ? {} : { tools: projectedTools.tools }
|
|
3667
|
+
};
|
|
3668
|
+
if (Object.isFrozen(resolvedOptions)) deepFreeze(projectedOptions);
|
|
3669
|
+
}
|
|
3670
|
+
iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
|
|
3671
|
+
} catch (error) {
|
|
3672
|
+
yield adapterFailureChunk(error, options.signal);
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
let completed = false;
|
|
3676
|
+
try {
|
|
3677
|
+
while (true) {
|
|
3678
|
+
let item;
|
|
3679
|
+
try {
|
|
3680
|
+
const next = await iterator.next();
|
|
3681
|
+
item = next.done ? { done: true } : {
|
|
3682
|
+
done: false,
|
|
3683
|
+
value: next.value
|
|
3684
|
+
};
|
|
3685
|
+
} catch (error) {
|
|
3686
|
+
completed = true;
|
|
3687
|
+
yield adapterFailureChunk(error, options.signal);
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
if (item.done) {
|
|
3691
|
+
completed = true;
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
yield item.value;
|
|
3695
|
+
}
|
|
3696
|
+
} finally {
|
|
3697
|
+
if (!completed) {
|
|
3698
|
+
const close = iterator.return?.bind(iterator);
|
|
3699
|
+
if (close) await close();
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
/**
|
|
3704
|
+
* Stream one model call as raw chunks (token-level deltas). Replay state is
|
|
3705
|
+
* retained only when the same adapter instance owns its historical provider
|
|
3706
|
+
* and the target provider. Final adapter selection remains fixed through
|
|
3707
|
+
* asynchronous exact-model resolution and dispatch. Adapter selection,
|
|
3708
|
+
* dispatch, and iteration failures become terminal `error` or `aborted`
|
|
3709
|
+
* finish chunks; middleware, nested-call, cleanup, and consumer failures
|
|
3710
|
+
* remain thrown.
|
|
3711
|
+
* @param options - the full request; `options.provider` selects the adapter.
|
|
3712
|
+
* @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
|
|
3713
|
+
*/
|
|
3714
|
+
stream(options) {
|
|
3715
|
+
return this.streamWithRegistration(options);
|
|
3716
|
+
}
|
|
3717
|
+
streamWithRegistration(options, prepared) {
|
|
3718
|
+
return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
})();
|
|
3722
|
+
/** Convert one adapter throw into the stream protocol's terminal outcome. */
|
|
3723
|
+
function adapterFailureChunk(error, signal) {
|
|
3724
|
+
const failure = normalizeLlmFailure(error);
|
|
3725
|
+
return {
|
|
3726
|
+
type: "finish",
|
|
3727
|
+
reason: signal?.aborted || failure.code === "ABORTED" ? {
|
|
3728
|
+
kind: "aborted",
|
|
3729
|
+
failure
|
|
3730
|
+
} : {
|
|
3731
|
+
kind: "error",
|
|
3732
|
+
failure
|
|
3733
|
+
}
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
/** Copy a directory entry, including its arrays, so neither side can mutate the other's copy. */
|
|
3737
|
+
function detachDirectoryEntry(entry) {
|
|
3738
|
+
const { nativeAuth } = entry;
|
|
3739
|
+
return {
|
|
3740
|
+
...entry,
|
|
3741
|
+
settingsPath: [...entry.settingsPath],
|
|
3742
|
+
...entry.baseURLEnv === void 0 ? {} : { baseURLEnv: [...entry.baseURLEnv] },
|
|
3743
|
+
...nativeAuth === void 0 ? {} : { nativeAuth: {
|
|
3744
|
+
...nativeAuth,
|
|
3745
|
+
env: [...nativeAuth.env]
|
|
3746
|
+
} }
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
new TextEncoder();
|
|
3750
|
+
function normalizeSegments(context) {
|
|
3751
|
+
if (context === void 0) return [];
|
|
3752
|
+
return (typeof context === "string" ? [context] : context).filter((segment) => typeof segment === "string" && segment.length > 0);
|
|
3753
|
+
}
|
|
3754
|
+
/** Only word characters, dots, colons and dashes are kept inside the feature attribute. */
|
|
3755
|
+
function featureAttribute(feature) {
|
|
3756
|
+
return feature.replaceAll(/[^\w.:-]/g, "_");
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Assemble the single user message for an auxiliary model call: stable
|
|
3760
|
+
* material in one labelled block first, the live request last. With no
|
|
3761
|
+
* context the result is byte-identical to `text`, so callers without
|
|
3762
|
+
* material keep exactly the previous wire shape.
|
|
3763
|
+
*/
|
|
3764
|
+
function buildAuxiliaryUserText(request) {
|
|
3765
|
+
const context = normalizeSegments(request.context);
|
|
3766
|
+
if (!context.length) return request.text;
|
|
3767
|
+
return `<auxiliary-context${request.feature ? ` feature="${featureAttribute(request.feature)}"` : ""}>\n${context.join("\n")}\n</auxiliary-context>\n<request>\n${request.text}\n</request>`;
|
|
3768
|
+
}
|
|
3769
|
+
//#endregion
|
|
3770
|
+
//#region ../dsh-plugin-kit/src/llm-call.ts
|
|
3771
|
+
function routeOf(value) {
|
|
3772
|
+
if (typeof value?.provider !== "string" || !value.provider.trim()) return void 0;
|
|
3773
|
+
if (typeof value.model !== "string" || !value.model.trim()) return void 0;
|
|
3774
|
+
const effort = typeof value.reasoningEffort === "string" ? value.reasoningEffort.trim() : "";
|
|
3775
|
+
return effort ? {
|
|
3776
|
+
provider: value.provider.trim(),
|
|
3777
|
+
model: value.model.trim(),
|
|
3778
|
+
reasoningEffort: effort
|
|
3779
|
+
} : {
|
|
3780
|
+
provider: value.provider.trim(),
|
|
3781
|
+
model: value.model.trim()
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
/**
|
|
3785
|
+
* Plugin selection first; then the shared auxiliary default (when any
|
|
3786
|
+
* participant retains it); otherwise the live session model, then the host
|
|
3787
|
+
* default. Cordis callers must declare agents, sessionProjections and
|
|
3788
|
+
* agentDefaultModel in their inject list so the fallback reads use the
|
|
3789
|
+
* caller's live services.
|
|
3790
|
+
*/
|
|
3791
|
+
function resolveFeatureModel(host, saved, sessionId) {
|
|
3792
|
+
const explicit = routeOf(saved);
|
|
3793
|
+
if (explicit) return explicit;
|
|
3794
|
+
const auxiliary = getAuxiliaryModel(host);
|
|
3795
|
+
if (auxiliary) return auxiliary;
|
|
3796
|
+
const face = host;
|
|
3797
|
+
if (sessionId && face?.agents && face.sessionProjections) {
|
|
3798
|
+
const session = face.agents.get(sessionId)?.session;
|
|
3799
|
+
if (session) {
|
|
3800
|
+
try {
|
|
3801
|
+
const pending = face.sessionProjections.stateOf(session, "modelSelection")?.pending;
|
|
3802
|
+
const selected = routeOf(pending ?? void 0);
|
|
3803
|
+
if (selected) return selected;
|
|
3804
|
+
} catch {}
|
|
3805
|
+
const header = session.requestHeader?.();
|
|
3806
|
+
const recorded = header?.config;
|
|
3807
|
+
if (recorded) {
|
|
3808
|
+
const route = routeOf(header?.adapterDefaults?.reasoningEffort === true ? {
|
|
3809
|
+
provider: recorded.provider,
|
|
3810
|
+
model: recorded.model
|
|
3811
|
+
} : recorded);
|
|
3812
|
+
if (route) return route;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
return routeOf(face?.agentDefaultModel?.currentSelection());
|
|
3817
|
+
}
|
|
3818
|
+
function joinText(assembler) {
|
|
3819
|
+
return assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
3820
|
+
}
|
|
3821
|
+
async function readStream(stream, signal, live) {
|
|
3822
|
+
const assembler = new BlockAssembler();
|
|
3823
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
3824
|
+
try {
|
|
3825
|
+
while (live()) {
|
|
3826
|
+
if (signal.aborted) break;
|
|
3827
|
+
const item = await iterator.next();
|
|
3828
|
+
if (item.done) break;
|
|
3829
|
+
assembler.push(item.value);
|
|
3830
|
+
}
|
|
3831
|
+
} finally {
|
|
3832
|
+
await iterator.return?.();
|
|
3833
|
+
}
|
|
3834
|
+
return assembler;
|
|
3835
|
+
}
|
|
3836
|
+
/** One native model call. No retry, timeout policy, or provider queue. */
|
|
3837
|
+
async function callLlmText(llm, request) {
|
|
3838
|
+
request.signal.throwIfAborted();
|
|
3839
|
+
const live = () => request.isCurrent?.() !== false && !request.signal.aborted;
|
|
3840
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
3841
|
+
const effort = request.route.reasoningEffort?.trim();
|
|
3842
|
+
const prepared = await llm.prepareCall({
|
|
3843
|
+
provider: request.route.provider,
|
|
3844
|
+
model: request.route.model,
|
|
3845
|
+
...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
|
|
3846
|
+
maxTokens: request.maxTokens
|
|
3847
|
+
}, request.signal, request.configure);
|
|
3848
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
3849
|
+
const options = {
|
|
3850
|
+
provider: prepared.config.provider,
|
|
3851
|
+
model: prepared.config.model,
|
|
3852
|
+
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
3853
|
+
...prepared.config.maxTokens !== void 0 ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens },
|
|
3854
|
+
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
3855
|
+
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
3856
|
+
system: request.system,
|
|
3857
|
+
messages: [createUserMessage({
|
|
3858
|
+
source: producerMessageSource(request.plugin),
|
|
3859
|
+
content: [{
|
|
3860
|
+
type: "text",
|
|
3861
|
+
text: buildAuxiliaryUserText(request)
|
|
3862
|
+
}]
|
|
3863
|
+
})],
|
|
3864
|
+
signal: request.signal,
|
|
3865
|
+
...request.purpose ? { purpose: request.purpose } : {},
|
|
3866
|
+
...request.sessionId ? { sessionId: request.sessionId } : {}
|
|
3867
|
+
};
|
|
3868
|
+
const assembler = await readStream(prepared.stream(options), request.signal, live);
|
|
3869
|
+
if (request.signal.aborted || assembler.finish.kind === "aborted") throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
3870
|
+
if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
|
|
3871
|
+
if (assembler.finish.kind === "error") {
|
|
3872
|
+
const code = assembler.finish.failure.code;
|
|
3873
|
+
throw Object.assign(new Error(assembler.finish.failure.message || "模型调用失败。"), { code });
|
|
3874
|
+
}
|
|
3875
|
+
if (assembler.blocks().some((block) => block.type === "tool-call") || assembler.finish.kind === "tool-calls") throw new Error("这次调用不使用工具。");
|
|
3876
|
+
if (assembler.finish.kind !== "stop") throw new Error("模型调用未完成。");
|
|
3877
|
+
const usage = assembler.usage;
|
|
3878
|
+
return {
|
|
3879
|
+
text: joinText(assembler),
|
|
3880
|
+
provider: prepared.config.provider,
|
|
3881
|
+
model: prepared.config.model,
|
|
3882
|
+
...usage ? {
|
|
3883
|
+
inputTokens: usage.inputTokens,
|
|
3884
|
+
outputTokens: usage.outputTokens,
|
|
3885
|
+
...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {},
|
|
3886
|
+
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
3887
|
+
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
3888
|
+
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {}
|
|
3889
|
+
} : {}
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
//#endregion
|
|
16
3893
|
//#region src/git.ts
|
|
17
3894
|
var GitError = class extends Error {
|
|
18
3895
|
stderr;
|
|
@@ -551,16 +4428,35 @@ function truncateMiddle(text, maxChars) {
|
|
|
551
4428
|
const half = Math.floor((maxChars - 40) / 2);
|
|
552
4429
|
return `${text.slice(0, half)}\n… [${text.length - maxChars} chars omitted] …\n${text.slice(-half)}`;
|
|
553
4430
|
}
|
|
554
|
-
|
|
4431
|
+
/**
|
|
4432
|
+
* Stable style material for the auxiliary-context block: the recent commit
|
|
4433
|
+
* subjects, ordered oldest to newest as recorded. Kept within a quarter of
|
|
4434
|
+
* the manifest budget so the live change set always dominates the request.
|
|
4435
|
+
*/
|
|
4436
|
+
function buildPlanContext(input) {
|
|
4437
|
+
if (input.recentSubjects.length === 0) return [];
|
|
4438
|
+
return [truncateMiddle(["Recent commit subjects (style reference):", ...input.recentSubjects.map((subject) => `- ${subject}`)].join("\n"), Math.floor(MAX_MANIFEST_CHARS / 4))];
|
|
4439
|
+
}
|
|
4440
|
+
/** The live request: scope, manifest, diff stat, diff and new-file previews. */
|
|
4441
|
+
function buildPlanText(input, maxChars = MAX_MANIFEST_CHARS) {
|
|
555
4442
|
const sections = [];
|
|
556
4443
|
sections.push("Scope: all current workspace changes of the repository.");
|
|
557
|
-
if (input.recentSubjects.length > 0) sections.push(["Recent commit subjects (style reference):", ...input.recentSubjects.map((s) => `- ${s}`)].join("\n"));
|
|
558
4444
|
const manifest = input.files.map((file) => `- ${statusLetter(file.status)} ${file.origin ? `${file.origin} ${file.status === "copied" ? "=>" : "->"} ` : ""}${file.path}`).join("\n");
|
|
559
4445
|
sections.push(`Changed files:\n${manifest}`);
|
|
560
4446
|
if (input.stat.trim()) sections.push(`Diff stat:\n${input.stat.trim()}`);
|
|
561
4447
|
if (input.diff.trim()) sections.push(`Diff against HEAD:\n${input.diff}`);
|
|
562
4448
|
for (const untracked of input.untracked) sections.push(untracked.binary ? `New binary file: ${untracked.path}` : `New file ${untracked.path}:\n${untracked.preview}`);
|
|
563
|
-
return truncateMiddle(sections.join("\n\n"),
|
|
4449
|
+
return truncateMiddle(sections.join("\n\n"), maxChars);
|
|
4450
|
+
}
|
|
4451
|
+
/**
|
|
4452
|
+
* The complete single-message input kept for callers that cannot split
|
|
4453
|
+
* stable context from the live request; identical material to
|
|
4454
|
+
* buildPlanContext + buildPlanText laid out in the same order as before.
|
|
4455
|
+
*/
|
|
4456
|
+
function buildPlanInput(input) {
|
|
4457
|
+
const context = buildPlanContext(input);
|
|
4458
|
+
const text = buildPlanText(input, MAX_MANIFEST_CHARS - (context.length ? context.join("\n").length + 2 : 0));
|
|
4459
|
+
return context.length ? `${context.join("\n")}\n\n${text}` : text;
|
|
564
4460
|
}
|
|
565
4461
|
function sanitizeMessage(raw) {
|
|
566
4462
|
if (typeof raw !== "string") return void 0;
|
|
@@ -736,9 +4632,15 @@ var GitCommitService = class {
|
|
|
736
4632
|
}
|
|
737
4633
|
selectedModel(sessionId) {
|
|
738
4634
|
const page = modelRouteOverride(this.settings.model);
|
|
4635
|
+
if (page) return {
|
|
4636
|
+
route: page,
|
|
4637
|
+
source: "page"
|
|
4638
|
+
};
|
|
4639
|
+
const resolved = resolveFeatureModel(this.deps.host, void 0, sessionId);
|
|
4640
|
+
const auxiliary = getAuxiliaryModel(this.deps.host);
|
|
739
4641
|
return {
|
|
740
|
-
route:
|
|
741
|
-
source:
|
|
4642
|
+
route: resolved,
|
|
4643
|
+
source: auxiliary && resolved && resolved.provider === auxiliary.provider && resolved.model === auxiliary.model && (resolved.reasoningEffort ?? "") === (auxiliary.reasoningEffort ?? "") ? "aux" : "default"
|
|
742
4644
|
};
|
|
743
4645
|
}
|
|
744
4646
|
async updateSettings(patch, expectedRevision) {
|
|
@@ -864,17 +4766,26 @@ var GitCommitService = class {
|
|
|
864
4766
|
let model;
|
|
865
4767
|
const planned = this.selectedModel(sessionId);
|
|
866
4768
|
if (planned.route) try {
|
|
4769
|
+
const planInput = {
|
|
4770
|
+
files: selected,
|
|
4771
|
+
recentSubjects: subjects,
|
|
4772
|
+
stat,
|
|
4773
|
+
diff,
|
|
4774
|
+
untracked
|
|
4775
|
+
};
|
|
4776
|
+
const context = buildPlanContext(planInput);
|
|
4777
|
+
const envelopeBase = buildAuxiliaryUserText({
|
|
4778
|
+
feature: COMMIT_PLAN_FEATURE,
|
|
4779
|
+
context,
|
|
4780
|
+
text: ""
|
|
4781
|
+
});
|
|
867
4782
|
const result = await callLlmText(this.deps.llm, {
|
|
868
4783
|
plugin: this.deps.plugin,
|
|
4784
|
+
feature: COMMIT_PLAN_FEATURE,
|
|
4785
|
+
context,
|
|
4786
|
+
text: buildPlanText(planInput, Math.max(1024, MAX_MANIFEST_CHARS - envelopeBase.length)),
|
|
869
4787
|
route: planned.route,
|
|
870
4788
|
system: planSystemPrompt(),
|
|
871
|
-
text: buildPlanInput({
|
|
872
|
-
files: selected,
|
|
873
|
-
recentSubjects: subjects,
|
|
874
|
-
stat,
|
|
875
|
-
diff,
|
|
876
|
-
untracked
|
|
877
|
-
}),
|
|
878
4789
|
maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
879
4790
|
signal,
|
|
880
4791
|
sessionId
|
|
@@ -1094,8 +5005,10 @@ async function handleRpc(service, endpoint, payload, signal) {
|
|
|
1094
5005
|
return fail(error);
|
|
1095
5006
|
}
|
|
1096
5007
|
}
|
|
1097
|
-
function apply(ctx) {
|
|
5008
|
+
async function apply(ctx) {
|
|
1098
5009
|
createModelSettingsChanges(ctx, modelSettingsChangesIdentity(PLUGIN_NAME), PLUGIN_NAME);
|
|
5010
|
+
const release = await retainAuxiliaryModel(ctx);
|
|
5011
|
+
ctx.effect(() => release, "git-commit.auxiliary-model");
|
|
1099
5012
|
const host = ctx;
|
|
1100
5013
|
const domainPromise = host.storageDomain.open(gitCommitDomain);
|
|
1101
5014
|
const tablePromise = domainPromise.then((domain) => domain.table("settings"));
|
|
@@ -1141,8 +5054,8 @@ function apply(ctx) {
|
|
|
1141
5054
|
},
|
|
1142
5055
|
current: settings.model,
|
|
1143
5056
|
followLabel: {
|
|
1144
|
-
zh: "
|
|
1145
|
-
en: "
|
|
5057
|
+
zh: "默认辅助模型",
|
|
5058
|
+
en: "Default auxiliary model"
|
|
1146
5059
|
},
|
|
1147
5060
|
allowFollow: true
|
|
1148
5061
|
}]
|