@huaweidevtools/aishell-dsh-llm 0.0.1-beta.2 → 0.0.1-beta.4
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/LICENSE +1 -1
- package/README.md +45 -9
- package/lib/client.js +234 -0
- package/lib/index.js +1344 -47
- package/lib/types/adapter.d.ts +100 -4
- package/lib/types/client/index.d.ts +32 -0
- package/lib/types/client/locales.d.ts +30 -0
- package/lib/types/client/provider-row-shadow.d.ts +80 -0
- package/lib/types/images.d.ts +120 -0
- package/lib/types/index.d.ts +39 -5
- package/lib/types/provider-name.d.ts +17 -0
- package/lib/types/serialize.d.ts +21 -4
- package/lib/types/types.d.ts +47 -13
- package/package.json +29 -7
package/lib/index.js
CHANGED
|
@@ -1,25 +1,1053 @@
|
|
|
1
1
|
import { createCredentialsStore } from "@huaweidevtools/aishell-dsh-credentials-store";
|
|
2
|
-
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, contentHasImage, offloadedImageText, projectOffloadedImages, requestImageHandleText, requiredImageOffload, resolveImageAttachmentAccess } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
|
|
3
4
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
4
5
|
import { EventSourceParserStream } from "eventsource-parser/stream";
|
|
5
6
|
import { sign } from "@huaweidevtools/aishell-dsh-credentials-store/signer";
|
|
7
|
+
import { readStoredPair } from "@huaweidevtools/aishell-dsh-credentials-store/accounts";
|
|
6
8
|
|
|
9
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.5/node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
10
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
11
|
+
function isNullable(value) {
|
|
12
|
+
return value === null || value === void 0;
|
|
13
|
+
}
|
|
14
|
+
/** Return true for non-array object values. */
|
|
15
|
+
function isPlainObject(data) {
|
|
16
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
17
|
+
}
|
|
18
|
+
/** Filter object entries and return a new object. */
|
|
19
|
+
function filterKeys(object, filter) {
|
|
20
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
21
|
+
}
|
|
22
|
+
/** Map object values while preserving the original key set. */
|
|
23
|
+
function mapValues(object, transform) {
|
|
24
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
25
|
+
}
|
|
26
|
+
/** Pick selected keys from an object, optionally including `undefined` values. */
|
|
27
|
+
function pick(source, keys, forced) {
|
|
28
|
+
if (!keys) return { ...source };
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/** Shared config references used by schema validators and plugin runtimes. */
|
|
34
|
+
const write = Symbol.for("cosmokit.volatile.write");
|
|
35
|
+
function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
36
|
+
if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
|
|
37
|
+
if (value === null || typeof value !== "object") return value;
|
|
38
|
+
if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
|
|
39
|
+
ancestors.add(value);
|
|
40
|
+
try {
|
|
41
|
+
if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
|
|
42
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
|
|
43
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
|
|
44
|
+
} finally {
|
|
45
|
+
ancestors.delete(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Create a detached reference containing an immutable copy of the supplied data.
|
|
50
|
+
* @param value - validated config data; class instances and functions are unsupported.
|
|
51
|
+
* @returns a reference whose value is updated only by its owning runtime.
|
|
52
|
+
*/
|
|
53
|
+
function createVolatile(value) {
|
|
54
|
+
let current = snapshot(value);
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
get: () => current,
|
|
57
|
+
[write]: (value$1) => {
|
|
58
|
+
current = value$1;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Identify references across ESM/CJS copies of the shared library.
|
|
64
|
+
* @param value - a parsed config value.
|
|
65
|
+
* @returns whether the value implements the shared reference protocol.
|
|
66
|
+
*/
|
|
67
|
+
function isVolatile(value) {
|
|
68
|
+
return typeof value === "object" && value !== null && write in value;
|
|
69
|
+
}
|
|
70
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
71
|
+
function is(type, value) {
|
|
72
|
+
if (arguments.length === 1) return (value$1) => is(type, value$1);
|
|
73
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
74
|
+
}
|
|
75
|
+
function isArrayBufferLike(value) {
|
|
76
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
77
|
+
}
|
|
78
|
+
function isArrayBufferSource(value) {
|
|
79
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
80
|
+
}
|
|
81
|
+
/** Binary source detection and base64/hex conversion helpers. */
|
|
82
|
+
var Binary;
|
|
83
|
+
(function(Binary$1) {
|
|
84
|
+
Binary$1.is = isArrayBufferLike;
|
|
85
|
+
Binary$1.isSource = isArrayBufferSource;
|
|
86
|
+
function fromSource(source) {
|
|
87
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
88
|
+
else return source;
|
|
89
|
+
}
|
|
90
|
+
Binary$1.fromSource = fromSource;
|
|
91
|
+
function toBase64(source) {
|
|
92
|
+
source = fromSource(source);
|
|
93
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
94
|
+
let binary = "";
|
|
95
|
+
const bytes = new Uint8Array(source);
|
|
96
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
97
|
+
return btoa(binary);
|
|
98
|
+
}
|
|
99
|
+
Binary$1.toBase64 = toBase64;
|
|
100
|
+
function fromBase64(source) {
|
|
101
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
102
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
103
|
+
}
|
|
104
|
+
Binary$1.fromBase64 = fromBase64;
|
|
105
|
+
function toHex(source) {
|
|
106
|
+
source = fromSource(source);
|
|
107
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
108
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
109
|
+
}
|
|
110
|
+
Binary$1.toHex = toHex;
|
|
111
|
+
function fromHex(source) {
|
|
112
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
113
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
114
|
+
const buffer = [];
|
|
115
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
116
|
+
return Uint8Array.from(buffer).buffer;
|
|
117
|
+
}
|
|
118
|
+
Binary$1.fromHex = fromHex;
|
|
119
|
+
})(Binary || (Binary = {}));
|
|
120
|
+
/** Decode a base64 string into binary data. */
|
|
121
|
+
const base64ToArrayBuffer = Binary.fromBase64;
|
|
122
|
+
/** Encode binary data as base64. */
|
|
123
|
+
const arrayBufferToBase64 = Binary.toBase64;
|
|
124
|
+
/** Decode a hex string into binary data. */
|
|
125
|
+
const hexToArrayBuffer = Binary.fromHex;
|
|
126
|
+
/** Encode binary data as hex. */
|
|
127
|
+
const arrayBufferToHex = Binary.toHex;
|
|
128
|
+
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
|
129
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
130
|
+
if (!source || typeof source !== "object") return source;
|
|
131
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
132
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
133
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
134
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
135
|
+
const cached = refs.get(source);
|
|
136
|
+
if (cached) return cached;
|
|
137
|
+
if (Array.isArray(source)) {
|
|
138
|
+
const result$1 = [];
|
|
139
|
+
refs.set(source, result$1);
|
|
140
|
+
source.forEach((value, index) => {
|
|
141
|
+
result$1[index] = Reflect.apply(clone, null, [value, refs]);
|
|
142
|
+
});
|
|
143
|
+
return result$1;
|
|
144
|
+
}
|
|
145
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
146
|
+
refs.set(source, result);
|
|
147
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
148
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
149
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
150
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Compare values recursively, treating two volatile references as equal regardless of value.
|
|
156
|
+
* Strict comparison distinguishes null/undefined, treats opaque objects by identity,
|
|
157
|
+
* compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
|
|
158
|
+
* @param a - first value.
|
|
159
|
+
* @param b - second value.
|
|
160
|
+
* @param strict - whether to require strict data equality outside volatile references.
|
|
161
|
+
* @returns whether the values compare equal.
|
|
162
|
+
*/
|
|
163
|
+
function deepEqual(a, b, strict) {
|
|
164
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
165
|
+
function compare(a$1, b$1) {
|
|
166
|
+
if (a$1 === b$1) return true;
|
|
167
|
+
if (isVolatile(a$1) || isVolatile(b$1)) return isVolatile(a$1) && isVolatile(b$1);
|
|
168
|
+
if (!strict && isNullable(a$1) && isNullable(b$1)) return true;
|
|
169
|
+
if (typeof a$1 !== typeof b$1 || typeof a$1 !== "object" || !a$1 || !b$1) return false;
|
|
170
|
+
if (ancestors.has(a$1)) return false;
|
|
171
|
+
function check(test, then) {
|
|
172
|
+
return test(a$1) ? test(b$1) ? then(a$1, b$1) : false : test(b$1) ? false : void 0;
|
|
173
|
+
}
|
|
174
|
+
ancestors.add(a$1);
|
|
175
|
+
try {
|
|
176
|
+
return check(Array.isArray, (a$2, b$2) => {
|
|
177
|
+
if (a$2.length !== b$2.length) return false;
|
|
178
|
+
for (let index = 0; index < a$2.length; index++) if (!compare(a$2[index], b$2[index])) return false;
|
|
179
|
+
return true;
|
|
180
|
+
}) ?? check(is("Date"), (a$2, b$2) => a$2.valueOf() === b$2.valueOf()) ?? check(is("URL"), (a$2, b$2) => a$2.href === b$2.href) ?? check(is("RegExp"), (a$2, b$2) => a$2.source === b$2.source && a$2.flags === b$2.flags) ?? check(isArrayBufferLike, (a$2, b$2) => {
|
|
181
|
+
if (a$2.byteLength !== b$2.byteLength) return false;
|
|
182
|
+
const viewA = new Uint8Array(a$2);
|
|
183
|
+
const viewB = new Uint8Array(b$2);
|
|
184
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
185
|
+
return true;
|
|
186
|
+
}) ?? ((!strict || [a$1, b$1].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
|
|
187
|
+
...a$1,
|
|
188
|
+
...b$1
|
|
189
|
+
}).every((key) => compare(a$1[key], b$1[key])));
|
|
190
|
+
} finally {
|
|
191
|
+
ancestors.delete(a$1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return compare(a, b);
|
|
195
|
+
}
|
|
196
|
+
/** Time constants plus parsing and formatting helpers. */
|
|
197
|
+
var Time;
|
|
198
|
+
(function(Time$1) {
|
|
199
|
+
Time$1.millisecond = 1;
|
|
200
|
+
Time$1.second = 1e3;
|
|
201
|
+
Time$1.minute = Time$1.second * 60;
|
|
202
|
+
Time$1.hour = Time$1.minute * 60;
|
|
203
|
+
Time$1.day = Time$1.hour * 24;
|
|
204
|
+
Time$1.week = Time$1.day * 7;
|
|
205
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
206
|
+
function setTimezoneOffset(offset) {
|
|
207
|
+
timezoneOffset = offset;
|
|
208
|
+
}
|
|
209
|
+
Time$1.setTimezoneOffset = setTimezoneOffset;
|
|
210
|
+
function getTimezoneOffset() {
|
|
211
|
+
return timezoneOffset;
|
|
212
|
+
}
|
|
213
|
+
Time$1.getTimezoneOffset = getTimezoneOffset;
|
|
214
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
215
|
+
if (typeof date === "number") date = new Date(date);
|
|
216
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
217
|
+
return Math.floor((date.valueOf() / Time$1.minute - offset) / 1440);
|
|
218
|
+
}
|
|
219
|
+
Time$1.getDateNumber = getDateNumber;
|
|
220
|
+
function fromDateNumber(value, offset) {
|
|
221
|
+
const date = new Date(value * Time$1.day);
|
|
222
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
223
|
+
return new Date(+date + offset * Time$1.minute);
|
|
224
|
+
}
|
|
225
|
+
Time$1.fromDateNumber = fromDateNumber;
|
|
226
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
227
|
+
const timeRegExp = new RegExp(`^${[
|
|
228
|
+
"w(?:eek(?:s)?)?",
|
|
229
|
+
"d(?:ay(?:s)?)?",
|
|
230
|
+
"h(?:our(?:s)?)?",
|
|
231
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
232
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
233
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
234
|
+
function parseTime(source) {
|
|
235
|
+
const capture = timeRegExp.exec(source);
|
|
236
|
+
if (!capture) return 0;
|
|
237
|
+
return (parseFloat(capture[1]) * Time$1.week || 0) + (parseFloat(capture[2]) * Time$1.day || 0) + (parseFloat(capture[3]) * Time$1.hour || 0) + (parseFloat(capture[4]) * Time$1.minute || 0) + (parseFloat(capture[5]) * Time$1.second || 0);
|
|
238
|
+
}
|
|
239
|
+
Time$1.parseTime = parseTime;
|
|
240
|
+
function parseDate(date) {
|
|
241
|
+
const parsed = parseTime(date);
|
|
242
|
+
if (parsed) date = Date.now() + parsed;
|
|
243
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
244
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
245
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
246
|
+
}
|
|
247
|
+
Time$1.parseDate = parseDate;
|
|
248
|
+
function format(ms) {
|
|
249
|
+
const abs = Math.abs(ms);
|
|
250
|
+
if (abs >= Time$1.day - Time$1.hour / 2) return Math.round(ms / Time$1.day) + "d";
|
|
251
|
+
else if (abs >= Time$1.hour - Time$1.minute / 2) return Math.round(ms / Time$1.hour) + "h";
|
|
252
|
+
else if (abs >= Time$1.minute - Time$1.second / 2) return Math.round(ms / Time$1.minute) + "m";
|
|
253
|
+
else if (abs >= Time$1.second) return Math.round(ms / Time$1.second) + "s";
|
|
254
|
+
return ms + "ms";
|
|
255
|
+
}
|
|
256
|
+
Time$1.format = format;
|
|
257
|
+
function toDigits(source, length = 2) {
|
|
258
|
+
return source.toString().padStart(length, "0");
|
|
259
|
+
}
|
|
260
|
+
Time$1.toDigits = toDigits;
|
|
261
|
+
function template(template$1, time = /* @__PURE__ */ new Date()) {
|
|
262
|
+
return template$1.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));
|
|
263
|
+
}
|
|
264
|
+
Time$1.template = template;
|
|
265
|
+
})(Time || (Time = {}));
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region ../../node_modules/.pnpm/@deepseek-ai+schemastery@3.18.4/node_modules/@deepseek-ai/schemastery/lib/index.mjs
|
|
269
|
+
const kSchema = Symbol.for("schemastery");
|
|
270
|
+
const kValidationError = Symbol.for("ValidationError");
|
|
271
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
272
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
273
|
+
var ValidationError = class extends TypeError {
|
|
274
|
+
options;
|
|
275
|
+
name = "ValidationError";
|
|
276
|
+
constructor(message, options) {
|
|
277
|
+
let prefix = "$";
|
|
278
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
279
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
280
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
281
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
282
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
283
|
+
this.options = options;
|
|
284
|
+
}
|
|
285
|
+
static is(error) {
|
|
286
|
+
return !!error?.[kValidationError];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
290
|
+
const Schema = function(options) {
|
|
291
|
+
const schema = function(data, options$1 = {}) {
|
|
292
|
+
return Schema.resolve(data, schema, options$1)[0];
|
|
293
|
+
};
|
|
294
|
+
if (options.refs) {
|
|
295
|
+
const refs = mapValues(options.refs, (options$1) => new Schema(options$1));
|
|
296
|
+
const getRef = (uid) => refs[uid];
|
|
297
|
+
for (const key in refs) {
|
|
298
|
+
const options$1 = refs[key];
|
|
299
|
+
options$1.sKey = getRef(options$1.sKey);
|
|
300
|
+
options$1.inner = getRef(options$1.inner);
|
|
301
|
+
options$1.list = options$1.list && options$1.list.map(getRef);
|
|
302
|
+
options$1.dict = options$1.dict && mapValues(options$1.dict, getRef);
|
|
303
|
+
}
|
|
304
|
+
return refs[options.uid];
|
|
305
|
+
}
|
|
306
|
+
Object.assign(schema, options);
|
|
307
|
+
if (typeof schema.callback === "string") try {
|
|
308
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
309
|
+
} catch {}
|
|
310
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
311
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
312
|
+
schema.meta ||= {};
|
|
313
|
+
schema.toString = schema.toString.bind(schema);
|
|
314
|
+
return schema;
|
|
315
|
+
};
|
|
316
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
317
|
+
Schema.prototype[kSchema] = true;
|
|
318
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
319
|
+
return {
|
|
320
|
+
version: 1,
|
|
321
|
+
vendor: "schemastery",
|
|
322
|
+
validate: (value) => {
|
|
323
|
+
try {
|
|
324
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
327
|
+
message: error.message,
|
|
328
|
+
path: error.options.path
|
|
329
|
+
}] };
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
} });
|
|
335
|
+
Schema.ValidationError = ValidationError;
|
|
336
|
+
Schema.prototype.toJSON = function toJSON() {
|
|
337
|
+
if (globalThis.__schemastery_refs__) {
|
|
338
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
339
|
+
return this.uid;
|
|
340
|
+
}
|
|
341
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
342
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
343
|
+
const result = {
|
|
344
|
+
uid: this.uid,
|
|
345
|
+
refs: globalThis.__schemastery_refs__
|
|
346
|
+
};
|
|
347
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
348
|
+
return result;
|
|
349
|
+
};
|
|
350
|
+
Schema.prototype.set = function set(key, value) {
|
|
351
|
+
this.dict[key] = value;
|
|
352
|
+
return this;
|
|
353
|
+
};
|
|
354
|
+
Schema.prototype.push = function push(value) {
|
|
355
|
+
this.list.push(value);
|
|
356
|
+
return this;
|
|
357
|
+
};
|
|
358
|
+
function mergeDesc(original, messages) {
|
|
359
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
360
|
+
for (const locale in messages) {
|
|
361
|
+
const value = messages[locale];
|
|
362
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
363
|
+
else if (typeof value === "string") result[locale] = value;
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
function getInner(value) {
|
|
368
|
+
return value?.$value ?? value?.$inner;
|
|
369
|
+
}
|
|
370
|
+
function extractKeys(data) {
|
|
371
|
+
return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
|
|
372
|
+
}
|
|
373
|
+
Schema.prototype.i18n = function i18n(messages) {
|
|
374
|
+
const schema = Schema(this);
|
|
375
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
376
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
377
|
+
if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
|
|
378
|
+
return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
379
|
+
});
|
|
380
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
381
|
+
return inner.i18n(mapValues(messages, (data = {}) => {
|
|
382
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
383
|
+
if (Array.isArray(data)) return data[index];
|
|
384
|
+
return extractKeys(data);
|
|
385
|
+
}));
|
|
386
|
+
});
|
|
387
|
+
if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
|
|
388
|
+
if (getInner(data)) return getInner(data);
|
|
389
|
+
return extractKeys(data);
|
|
390
|
+
}));
|
|
391
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
|
|
392
|
+
return schema;
|
|
393
|
+
};
|
|
394
|
+
Schema.prototype.extra = function extra(key, value) {
|
|
395
|
+
const schema = Schema(this);
|
|
396
|
+
schema.meta = {
|
|
397
|
+
...schema.meta,
|
|
398
|
+
[key]: value
|
|
399
|
+
};
|
|
400
|
+
return schema;
|
|
401
|
+
};
|
|
402
|
+
for (const key of [
|
|
403
|
+
"required",
|
|
404
|
+
"disabled",
|
|
405
|
+
"collapse",
|
|
406
|
+
"hidden",
|
|
407
|
+
"loose"
|
|
408
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
409
|
+
const schema = Schema(this);
|
|
410
|
+
schema.meta = {
|
|
411
|
+
...schema.meta,
|
|
412
|
+
[key]: value
|
|
413
|
+
};
|
|
414
|
+
return schema;
|
|
415
|
+
} });
|
|
416
|
+
Schema.prototype.deprecated = function deprecated() {
|
|
417
|
+
const schema = Schema(this);
|
|
418
|
+
schema.meta.badges ||= [];
|
|
419
|
+
schema.meta.badges.push({
|
|
420
|
+
text: "deprecated",
|
|
421
|
+
type: "danger"
|
|
422
|
+
});
|
|
423
|
+
return schema;
|
|
424
|
+
};
|
|
425
|
+
Schema.prototype.experimental = function experimental() {
|
|
426
|
+
const schema = Schema(this);
|
|
427
|
+
schema.meta.badges ||= [];
|
|
428
|
+
schema.meta.badges.push({
|
|
429
|
+
text: "experimental",
|
|
430
|
+
type: "warning"
|
|
431
|
+
});
|
|
432
|
+
return schema;
|
|
433
|
+
};
|
|
434
|
+
Schema.prototype.pattern = function pattern(regexp) {
|
|
435
|
+
const schema = Schema(this);
|
|
436
|
+
const pattern$1 = pick(regexp, ["source", "flags"]);
|
|
437
|
+
schema.meta = {
|
|
438
|
+
...schema.meta,
|
|
439
|
+
pattern: pattern$1
|
|
440
|
+
};
|
|
441
|
+
return schema;
|
|
442
|
+
};
|
|
443
|
+
Schema.prototype.simplify = function simplify(value) {
|
|
444
|
+
if (isVolatile(value)) value = value.get();
|
|
445
|
+
if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
|
|
446
|
+
if (isNullable(value)) return value;
|
|
447
|
+
if (this.type === "object" || this.type === "dict") {
|
|
448
|
+
const result = {};
|
|
449
|
+
for (const key in value) {
|
|
450
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
451
|
+
if (this.type === "dict" || !isNullable(item)) result[key] = item;
|
|
452
|
+
}
|
|
453
|
+
if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
|
|
454
|
+
return result;
|
|
455
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
456
|
+
const result = [];
|
|
457
|
+
value.forEach((value$1, index) => {
|
|
458
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
459
|
+
const item = schema ? schema.simplify(value$1) : value$1;
|
|
460
|
+
result.push(item);
|
|
461
|
+
});
|
|
462
|
+
return result;
|
|
463
|
+
} else if (this.type === "intersect") {
|
|
464
|
+
const result = {};
|
|
465
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
466
|
+
return result;
|
|
467
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
468
|
+
Schema.resolve(value, schema, {});
|
|
469
|
+
return schema.simplify(value);
|
|
470
|
+
} catch {}
|
|
471
|
+
return value;
|
|
472
|
+
};
|
|
473
|
+
Schema.prototype.toString = function toString(inline) {
|
|
474
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
475
|
+
};
|
|
476
|
+
Schema.prototype.role = function role(role$1, extra) {
|
|
477
|
+
const schema = Schema(this);
|
|
478
|
+
schema.meta = {
|
|
479
|
+
...schema.meta,
|
|
480
|
+
role: role$1,
|
|
481
|
+
extra
|
|
482
|
+
};
|
|
483
|
+
return schema;
|
|
484
|
+
};
|
|
485
|
+
for (const key of [
|
|
486
|
+
"default",
|
|
487
|
+
"link",
|
|
488
|
+
"comment",
|
|
489
|
+
"description",
|
|
490
|
+
"max",
|
|
491
|
+
"min",
|
|
492
|
+
"step"
|
|
493
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
494
|
+
const schema = Schema(this);
|
|
495
|
+
schema.meta = {
|
|
496
|
+
...schema.meta,
|
|
497
|
+
[key]: value
|
|
498
|
+
};
|
|
499
|
+
return schema;
|
|
500
|
+
} });
|
|
501
|
+
Schema.prototype.volatile = function volatile() {
|
|
502
|
+
if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
|
|
503
|
+
return this.extra("volatile", true);
|
|
504
|
+
};
|
|
505
|
+
const resolvers = {};
|
|
506
|
+
const checkedVolatile = Symbol("checked-volatile-schema");
|
|
507
|
+
function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
|
|
508
|
+
const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
|
|
509
|
+
if (states.has(blocked)) return;
|
|
510
|
+
states.add(blocked);
|
|
511
|
+
seen.set(schema, states);
|
|
512
|
+
if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
|
|
513
|
+
const nested = blocked || !!schema.meta?.volatile;
|
|
514
|
+
if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
|
|
515
|
+
if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
|
|
516
|
+
if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
|
|
517
|
+
if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
|
|
518
|
+
}
|
|
519
|
+
Schema.extend = function extend(type, resolve) {
|
|
520
|
+
resolvers[type] = resolve;
|
|
521
|
+
};
|
|
522
|
+
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
|
523
|
+
if (!schema) return [data];
|
|
524
|
+
if (!options[checkedVolatile]) {
|
|
525
|
+
validateVolatileSchema(schema, options.path);
|
|
526
|
+
options = {
|
|
527
|
+
...options,
|
|
528
|
+
[checkedVolatile]: true
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (schema.meta?.volatile) {
|
|
532
|
+
const inner = Schema(schema);
|
|
533
|
+
inner.meta = {
|
|
534
|
+
...schema.meta,
|
|
535
|
+
volatile: false
|
|
536
|
+
};
|
|
537
|
+
const [value, adapted] = Schema.resolve(data, inner, options, strict);
|
|
538
|
+
try {
|
|
539
|
+
return [createVolatile(value), adapted];
|
|
540
|
+
} catch (error) {
|
|
541
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error), options);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
545
|
+
if (isNullable(data) && schema.type !== "lazy") {
|
|
546
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
547
|
+
let current = schema;
|
|
548
|
+
let fallback = schema.meta.default;
|
|
549
|
+
while (current?.type === "intersect" && isNullable(fallback)) {
|
|
550
|
+
current = current.list[0];
|
|
551
|
+
fallback = current?.meta.default;
|
|
552
|
+
}
|
|
553
|
+
if (isNullable(fallback)) return [data];
|
|
554
|
+
data = clone(fallback);
|
|
555
|
+
}
|
|
556
|
+
const callback = resolvers[schema.type];
|
|
557
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
558
|
+
try {
|
|
559
|
+
return callback(data, schema, options, strict);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if (!schema.meta.loose) throw error;
|
|
562
|
+
return [schema.meta.default];
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
Schema.from = function from(source) {
|
|
566
|
+
if (isNullable(source)) return Schema.any();
|
|
567
|
+
else if ([
|
|
568
|
+
"string",
|
|
569
|
+
"number",
|
|
570
|
+
"boolean"
|
|
571
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
572
|
+
else if (source[kSchema]) return source;
|
|
573
|
+
else if (typeof source === "function") switch (source) {
|
|
574
|
+
case String: return Schema.string().required();
|
|
575
|
+
case Number: return Schema.number().required();
|
|
576
|
+
case Boolean: return Schema.boolean().required();
|
|
577
|
+
case Function: return Schema.function().required();
|
|
578
|
+
default: return Schema.is(source).required();
|
|
579
|
+
}
|
|
580
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
581
|
+
};
|
|
582
|
+
Schema.lazy = function lazy(builder) {
|
|
583
|
+
const toJSON = () => {
|
|
584
|
+
if (!schema.inner[kSchema]) {
|
|
585
|
+
schema.inner = schema.builder();
|
|
586
|
+
schema.inner.meta = {
|
|
587
|
+
...schema.meta,
|
|
588
|
+
...schema.inner.meta
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return schema.inner.toJSON();
|
|
592
|
+
};
|
|
593
|
+
const schema = new Schema({
|
|
594
|
+
type: "lazy",
|
|
595
|
+
builder,
|
|
596
|
+
inner: { toJSON }
|
|
597
|
+
});
|
|
598
|
+
return schema;
|
|
599
|
+
};
|
|
600
|
+
Schema.natural = function natural() {
|
|
601
|
+
return Schema.number().step(1).min(0);
|
|
602
|
+
};
|
|
603
|
+
Schema.percent = function percent() {
|
|
604
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
605
|
+
};
|
|
606
|
+
Schema.date = function date() {
|
|
607
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
608
|
+
const date$1 = new Date(value);
|
|
609
|
+
if (isNaN(+date$1)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
610
|
+
return date$1;
|
|
611
|
+
}, true)]);
|
|
612
|
+
};
|
|
613
|
+
Schema.regExp = function regExp(flag = "") {
|
|
614
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
615
|
+
try {
|
|
616
|
+
return new RegExp(value, flag);
|
|
617
|
+
} catch (e) {
|
|
618
|
+
throw new ValidationError(e.message, options);
|
|
619
|
+
}
|
|
620
|
+
}, true)]);
|
|
621
|
+
};
|
|
622
|
+
Schema.arrayBuffer = function arrayBuffer(encoding) {
|
|
623
|
+
return Schema.union([
|
|
624
|
+
Schema.is(ArrayBuffer),
|
|
625
|
+
Schema.is(SharedArrayBuffer),
|
|
626
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
627
|
+
if (Binary.isSource(value)) return Binary.fromSource(value);
|
|
628
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
629
|
+
}, true),
|
|
630
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
631
|
+
try {
|
|
632
|
+
return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
throw new ValidationError(e.message, options);
|
|
635
|
+
}
|
|
636
|
+
}, true)] : []
|
|
637
|
+
]);
|
|
638
|
+
};
|
|
639
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
640
|
+
if (!schema.inner[kSchema]) {
|
|
641
|
+
schema.inner = schema.builder();
|
|
642
|
+
schema.inner.meta = {
|
|
643
|
+
...schema.meta,
|
|
644
|
+
...schema.inner.meta
|
|
645
|
+
};
|
|
646
|
+
validateVolatileSchema(schema.inner, options.path, true);
|
|
647
|
+
}
|
|
648
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
649
|
+
});
|
|
650
|
+
Schema.extend("any", (data) => {
|
|
651
|
+
return [data];
|
|
652
|
+
});
|
|
653
|
+
Schema.extend("never", (data, _, options) => {
|
|
654
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
655
|
+
});
|
|
656
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
657
|
+
if (deepEqual(data, value)) return [value];
|
|
658
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
659
|
+
});
|
|
660
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
661
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
662
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
663
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
664
|
+
}
|
|
665
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
666
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
667
|
+
if (meta.pattern) {
|
|
668
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
669
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
670
|
+
}
|
|
671
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
672
|
+
return [data];
|
|
673
|
+
});
|
|
674
|
+
function decimalShift(data, digits) {
|
|
675
|
+
const str = data.toString();
|
|
676
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
677
|
+
const index = str.indexOf(".");
|
|
678
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
679
|
+
const frac = str.slice(index + 1);
|
|
680
|
+
const integer = str.slice(0, index);
|
|
681
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
682
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
683
|
+
}
|
|
684
|
+
function isMultipleOf(data, min, step) {
|
|
685
|
+
step = Math.abs(step);
|
|
686
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
687
|
+
const index = step.toString().indexOf(".");
|
|
688
|
+
const digits = step.toString().slice(index + 1).length;
|
|
689
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
690
|
+
}
|
|
691
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
692
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
693
|
+
checkWithinRange(data, meta, "number", options);
|
|
694
|
+
const { step } = meta;
|
|
695
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
696
|
+
return [data];
|
|
697
|
+
});
|
|
698
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
699
|
+
if (typeof data === "boolean") return [data];
|
|
700
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
701
|
+
});
|
|
702
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
703
|
+
let value = 0, keys = [];
|
|
704
|
+
if (typeof data === "number") {
|
|
705
|
+
value = data;
|
|
706
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
707
|
+
} else if (Array.isArray(data)) {
|
|
708
|
+
keys = data;
|
|
709
|
+
for (const key of keys) {
|
|
710
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
711
|
+
if (key in bits) value |= bits[key];
|
|
712
|
+
}
|
|
713
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
714
|
+
if (value === meta.default) return [value];
|
|
715
|
+
return [value, keys];
|
|
716
|
+
});
|
|
717
|
+
Schema.extend("function", (data, _, options) => {
|
|
718
|
+
if (typeof data === "function") return [data];
|
|
719
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
720
|
+
});
|
|
721
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
722
|
+
if (typeof constructor === "function") {
|
|
723
|
+
if (data instanceof constructor) return [data];
|
|
724
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
725
|
+
} else {
|
|
726
|
+
if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
727
|
+
let prototype = Object.getPrototypeOf(data);
|
|
728
|
+
while (prototype) {
|
|
729
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
730
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
731
|
+
}
|
|
732
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
function property(data, key, schema, options) {
|
|
736
|
+
try {
|
|
737
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
738
|
+
...options,
|
|
739
|
+
path: [...options.path || [], key]
|
|
740
|
+
});
|
|
741
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
742
|
+
return value;
|
|
743
|
+
} catch (e) {
|
|
744
|
+
if (!options?.autofix) throw e;
|
|
745
|
+
delete data[key];
|
|
746
|
+
return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
750
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
751
|
+
checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
|
|
752
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
753
|
+
});
|
|
754
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
755
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
756
|
+
const result = {};
|
|
757
|
+
for (const key in data) {
|
|
758
|
+
let rKey;
|
|
759
|
+
try {
|
|
760
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (strict) continue;
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
result[rKey] = property(data, key, inner, options);
|
|
766
|
+
data[rKey] = data[key];
|
|
767
|
+
if (key !== rKey) delete data[key];
|
|
768
|
+
}
|
|
769
|
+
return [result];
|
|
770
|
+
});
|
|
771
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
772
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
773
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
774
|
+
if (strict) return [result];
|
|
775
|
+
result.push(...data.slice(list.length));
|
|
776
|
+
return [result];
|
|
777
|
+
});
|
|
778
|
+
function merge(result, data) {
|
|
779
|
+
for (const key in data) {
|
|
780
|
+
if (key in result) continue;
|
|
781
|
+
result[key] = data[key];
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
785
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
786
|
+
const result = {};
|
|
787
|
+
for (const key in dict) {
|
|
788
|
+
const value = property(data, key, dict[key], options);
|
|
789
|
+
if (!isNullable(value) || key in data) result[key] = value;
|
|
790
|
+
}
|
|
791
|
+
if (!strict) merge(result, data);
|
|
792
|
+
return [result];
|
|
793
|
+
});
|
|
794
|
+
Schema.extend("union", (data, { list, toString }, options, strict) => {
|
|
795
|
+
const messages = [];
|
|
796
|
+
for (const inner of list) try {
|
|
797
|
+
return Schema.resolve(data, inner, options, strict);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
messages.push(error);
|
|
800
|
+
}
|
|
801
|
+
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
802
|
+
});
|
|
803
|
+
Schema.extend("intersect", (data, { list, toString }, options, strict) => {
|
|
804
|
+
if (!list.length) return [data];
|
|
805
|
+
let result;
|
|
806
|
+
for (const inner of list) {
|
|
807
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
808
|
+
if (isNullable(value)) continue;
|
|
809
|
+
if (isNullable(result)) result = value;
|
|
810
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
811
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
812
|
+
else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
813
|
+
}
|
|
814
|
+
if (!strict && isPlainObject(data)) merge(result, data);
|
|
815
|
+
return [result];
|
|
816
|
+
});
|
|
817
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
818
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
819
|
+
if (preserve) return [callback(result)];
|
|
820
|
+
else return [callback(result), callback(adapted)];
|
|
821
|
+
});
|
|
822
|
+
const formatters = {};
|
|
823
|
+
function defineMethod(name$1, keys, format) {
|
|
824
|
+
formatters[name$1] = format;
|
|
825
|
+
Object.assign(Schema, { [name$1](...args) {
|
|
826
|
+
const schema = new Schema({ type: name$1 });
|
|
827
|
+
keys.forEach((key, index) => {
|
|
828
|
+
switch (key) {
|
|
829
|
+
case "sKey":
|
|
830
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
831
|
+
break;
|
|
832
|
+
case "inner":
|
|
833
|
+
schema.inner = Schema.from(args[index]);
|
|
834
|
+
break;
|
|
835
|
+
case "list":
|
|
836
|
+
schema.list = args[index].map(Schema.from);
|
|
837
|
+
break;
|
|
838
|
+
case "dict":
|
|
839
|
+
schema.dict = mapValues(args[index], Schema.from);
|
|
840
|
+
break;
|
|
841
|
+
case "bits":
|
|
842
|
+
schema.bits = {};
|
|
843
|
+
for (const key$1 in args[index]) {
|
|
844
|
+
if (typeof args[index][key$1] !== "number") continue;
|
|
845
|
+
schema.bits[key$1] = args[index][key$1];
|
|
846
|
+
}
|
|
847
|
+
break;
|
|
848
|
+
case "callback": {
|
|
849
|
+
const callback = schema.callback = args[index];
|
|
850
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case "constructor": {
|
|
854
|
+
const constructor = schema.constructor = args[index];
|
|
855
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
default: schema[key] = args[index];
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
if (name$1 === "object" || name$1 === "dict") schema.meta.default = {};
|
|
862
|
+
else if (name$1 === "array" || name$1 === "tuple") schema.meta.default = [];
|
|
863
|
+
else if (name$1 === "bitset") schema.meta.default = 0;
|
|
864
|
+
return schema;
|
|
865
|
+
} });
|
|
866
|
+
}
|
|
867
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
868
|
+
if (typeof constructor === "function") return constructor.name;
|
|
869
|
+
else return constructor;
|
|
870
|
+
});
|
|
871
|
+
defineMethod("any", [], () => "any");
|
|
872
|
+
defineMethod("never", [], () => "never");
|
|
873
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
874
|
+
defineMethod("string", [], () => "string");
|
|
875
|
+
defineMethod("number", [], () => "number");
|
|
876
|
+
defineMethod("boolean", [], () => "boolean");
|
|
877
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
878
|
+
defineMethod("function", [], () => "function");
|
|
879
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
880
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
881
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
882
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
883
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
884
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
885
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
886
|
+
}).join(", ")} }`;
|
|
887
|
+
});
|
|
888
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
889
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
890
|
+
return inline ? `(${result})` : result;
|
|
891
|
+
});
|
|
892
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
893
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
894
|
+
});
|
|
895
|
+
defineMethod("transform", [
|
|
896
|
+
"inner",
|
|
897
|
+
"callback",
|
|
898
|
+
"preserve"
|
|
899
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
900
|
+
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/images.ts
|
|
903
|
+
/**
|
|
904
|
+
* True when any message carries an image block — the guard that decides
|
|
905
|
+
* whether this adapter needs the attachment service at all.
|
|
906
|
+
*
|
|
907
|
+
* @param messages - complete request history.
|
|
908
|
+
* @returns whether any message content holds an image.
|
|
909
|
+
*/
|
|
910
|
+
function historyHasImage(messages) {
|
|
911
|
+
return messages.some((message) => contentHasImage(message.content));
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Reject image blocks in roles the wire format cannot carry them in.
|
|
915
|
+
*
|
|
916
|
+
* A user or tool-result message maps onto content parts, so an image there is
|
|
917
|
+
* representable. Every other role serializes to a plain string (assistant
|
|
918
|
+
* text plus tool calls, system/developer prose), where an image would be
|
|
919
|
+
* silently dropped — the exact failure this module exists to remove, so it
|
|
920
|
+
* fails loud instead.
|
|
921
|
+
*
|
|
922
|
+
* @param messages - complete request history.
|
|
923
|
+
* @throws LlmError `UNSUPPORTED_CONTENT` naming the offending role.
|
|
924
|
+
*/
|
|
925
|
+
function assertImageRoles(messages) {
|
|
926
|
+
for (const message of messages) {
|
|
927
|
+
if (message.role === "user" || message.role === "tool") continue;
|
|
928
|
+
if (contentHasImage(message.content)) throw new LlmError(`aishell-llm cannot represent an image in a ${message.role} message`, "UNSUPPORTED_CONTENT");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Resolve one request version per retained image, keyed by durable attachment
|
|
933
|
+
* id so a reference shared by several occurrences is read once.
|
|
934
|
+
*
|
|
935
|
+
* Occurrences already marked `offloaded` are skipped: their bytes are not sent
|
|
936
|
+
* (serialization replaces them with placeholder text), so reading them would
|
|
937
|
+
* be work whose result nothing consumes.
|
|
938
|
+
*
|
|
939
|
+
* @param messages - complete request history.
|
|
940
|
+
* @param attachments - mounted attachment service.
|
|
941
|
+
* @param budget - projection policy for this route.
|
|
942
|
+
* @param signal - request cancellation, honoured by the attachment read.
|
|
943
|
+
* @returns request versions by durable attachment id.
|
|
944
|
+
*/
|
|
945
|
+
async function prepareRequestImages(messages, attachments, budget, signal) {
|
|
946
|
+
const refs = new Map();
|
|
947
|
+
for (const message of messages) for (const block of message.content) {
|
|
948
|
+
if (block.type !== "image" || block.offloaded === true) continue;
|
|
949
|
+
refs.set(block.attachment.attachmentId, block.attachment);
|
|
950
|
+
}
|
|
951
|
+
const ordered = [...refs.values()];
|
|
952
|
+
const versions = await Promise.all(ordered.map((ref) => attachments.readImageRequest(ref, {
|
|
953
|
+
...requestImageDimensions(ref.width, ref.height, budget.maxPixels),
|
|
954
|
+
maxBytes: budget.maxBytes
|
|
955
|
+
}, signal)));
|
|
956
|
+
return new Map(ordered.map((ref, index) => [ref.attachmentId, versions[index]]));
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Request version for one retained occurrence, or a loud failure.
|
|
960
|
+
*
|
|
961
|
+
* Every image block reaching serialization must have been prepared, so a
|
|
962
|
+
* miss is a bug in this adapter rather than bad input — but the alternative
|
|
963
|
+
* (letting the lookup produce `undefined` and reading `.bytes` off it) turns
|
|
964
|
+
* it into a `TypeError` from deep inside the request body, so it is named.
|
|
965
|
+
*
|
|
966
|
+
* @param images - prepared request versions.
|
|
967
|
+
* @param ref - durable reference of one retained occurrence.
|
|
968
|
+
* @returns the request version carrying that image's bytes.
|
|
969
|
+
* @throws LlmError `INVALID_REQUEST` when the reference was never prepared.
|
|
970
|
+
*/
|
|
971
|
+
function requireRequestImage(images, ref) {
|
|
972
|
+
const version = images.get(ref.attachmentId);
|
|
973
|
+
if (version === void 0) throw new LlmError(`aishell-llm request image ${String(ref.attachmentId)} was not prepared`, "INVALID_REQUEST");
|
|
974
|
+
return version;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Replace every offloaded occurrence with its placeholder text.
|
|
978
|
+
*
|
|
979
|
+
* The durable `offloaded` mark is a surface fact shared by all routes; only
|
|
980
|
+
* the placeholder wording is route-owned. Request assembly does not do this
|
|
981
|
+
* for adapters, so an adapter that skips it would send an image whose bytes
|
|
982
|
+
* it deliberately never loaded.
|
|
983
|
+
*
|
|
984
|
+
* @param messages - complete request history.
|
|
985
|
+
* @param resolveAccess - execution-world path resolver, when one is available.
|
|
986
|
+
* @returns the history with offloaded occurrences projected to text.
|
|
987
|
+
*/
|
|
988
|
+
function projectOffloaded(messages, resolveAccess) {
|
|
989
|
+
return projectOffloadedImages(messages, (ref) => offloadedImageText(ref, resolveAccess?.(ref)));
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Fail the request when its retained images exceed the route's byte budget.
|
|
993
|
+
*
|
|
994
|
+
* This adapter does not offload on its own: the durable decision belongs to
|
|
995
|
+
* the surface, and `dsh-compaction-image-offload` (mounted by `dsh-base`) is
|
|
996
|
+
* the component that records it and retries the step. Throwing the canonical
|
|
997
|
+
* code with the required count is what hands it that decision.
|
|
998
|
+
*
|
|
999
|
+
* @param messages - complete request history, carrying the surface's offload marks.
|
|
1000
|
+
* @param images - prepared request versions.
|
|
1001
|
+
* @param maxBytes - accumulated represented base64 bytes this route accepts.
|
|
1002
|
+
* @throws LlmError `IMAGE_OFFLOAD_REQUIRED` carrying how many more occurrences must be offloaded.
|
|
1003
|
+
*/
|
|
1004
|
+
function assertRequestImageBudget(messages, images, maxBytes) {
|
|
1005
|
+
const offloadImages = requiredImageOffload(messages, {
|
|
1006
|
+
representation: "base64",
|
|
1007
|
+
maxBytes
|
|
1008
|
+
}, (block) => requireRequestImage(images, block.attachment).bytes);
|
|
1009
|
+
if (offloadImages > 0) throw new LlmError(`aishell-llm request images exceed the ${maxBytes}-byte base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages });
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
//#endregion
|
|
7
1013
|
//#region src/serialize.ts
|
|
8
|
-
/**
|
|
9
|
-
|
|
1014
|
+
/**
|
|
1015
|
+
* Build the POST body for `/chat/completions`.
|
|
1016
|
+
*
|
|
1017
|
+
* @param options - provider-neutral request.
|
|
1018
|
+
* @param images - prepared request versions, required only when the history carries a retained image.
|
|
1019
|
+
* @param resolveAccess - execution-world path resolver for image handle text, when a provider exposes one.
|
|
1020
|
+
* @returns the OpenAI-compatible request body.
|
|
1021
|
+
*/
|
|
1022
|
+
function serializeRequest(options, images, resolveAccess) {
|
|
10
1023
|
const messages = [];
|
|
11
|
-
|
|
12
|
-
|
|
1024
|
+
if (options.system !== void 0) messages.push({
|
|
1025
|
+
role: "system",
|
|
1026
|
+
content: options.system
|
|
1027
|
+
});
|
|
1028
|
+
let pendingToolImages = [];
|
|
1029
|
+
for (const [index, message] of options.messages.entries()) if (message.role === "user") messages.push({
|
|
1030
|
+
role: "user",
|
|
1031
|
+
content: contentFor(message.content, images, resolveAccess) ?? blocksToText(message.content)
|
|
1032
|
+
});
|
|
1033
|
+
else if (message.role === "tool") {
|
|
1034
|
+
const toolText = blocksToText(message.content);
|
|
1035
|
+
const hasImages = message.content.some((block) => block.type === "image");
|
|
13
1036
|
messages.push({
|
|
14
|
-
role: "
|
|
15
|
-
|
|
1037
|
+
role: "tool",
|
|
1038
|
+
tool_call_id: String(message.toolCallId),
|
|
1039
|
+
content: toolText.length > 0 || !hasImages ? toolText : TOOL_IMAGES_ONLY
|
|
16
1040
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1041
|
+
if (hasImages) pendingToolImages.push(message.content);
|
|
1042
|
+
if (isLastToolOfRun(options.messages, index)) {
|
|
1043
|
+
const hoisted = hoistedToolImages(pendingToolImages, images, resolveAccess);
|
|
1044
|
+
pendingToolImages = [];
|
|
1045
|
+
if (hoisted !== void 0) messages.push({
|
|
1046
|
+
role: "user",
|
|
1047
|
+
content: hoisted
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
} else if (message.role === "developer") {
|
|
23
1051
|
const text = blocksToText(message.content);
|
|
24
1052
|
if (text) messages.push({
|
|
25
1053
|
role: "system",
|
|
@@ -49,6 +1077,122 @@ function serializeRequest(options) {
|
|
|
49
1077
|
...options.stop !== void 0 ? { stop: options.stop } : {}
|
|
50
1078
|
};
|
|
51
1079
|
}
|
|
1080
|
+
/** Intro line naming what the hoisted parts are, matching the ecosystem's wording. */
|
|
1081
|
+
const TOOL_IMAGES_INTRO = "Attached image(s) from tool result:";
|
|
1082
|
+
/** Stand-in for a tool result whose only content was an image, which cannot ride the tool message itself. */
|
|
1083
|
+
const TOOL_IMAGES_ONLY = "(see attached image)";
|
|
1084
|
+
/**
|
|
1085
|
+
* Whether the tool-role message at `index` is the last of its consecutive run.
|
|
1086
|
+
*
|
|
1087
|
+
* Tool results arrive as a run, one per call the assistant issued in a single
|
|
1088
|
+
* turn, and they must stay adjacent to that assistant turn on the wire. The
|
|
1089
|
+
* hoisted user turn therefore waits for the run's end rather than following
|
|
1090
|
+
* whichever result happened to carry the image.
|
|
1091
|
+
*
|
|
1092
|
+
* @param messages - the complete request history.
|
|
1093
|
+
* @param index - position of the tool-role message being serialized.
|
|
1094
|
+
* @returns whether no further tool-role message follows.
|
|
1095
|
+
*/
|
|
1096
|
+
function isLastToolOfRun(messages, index) {
|
|
1097
|
+
return messages[index + 1]?.role !== "tool";
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Parts for one tool-result run's images, hoisted into a user turn.
|
|
1101
|
+
*
|
|
1102
|
+
* An OpenAI tool message carries text only, so an image a tool returned has
|
|
1103
|
+
* nowhere to go in its own message; sending it as a following `user` turn is
|
|
1104
|
+
* how the one OpenAI-path serializer in the ecosystem handles the same
|
|
1105
|
+
* situation. The tool messages keep their text (or a placeholder), so the
|
|
1106
|
+
* model is not left with an empty result to explain away.
|
|
1107
|
+
*
|
|
1108
|
+
* @param run - content blocks of every image-bearing tool result in one run.
|
|
1109
|
+
* @param images - prepared request versions.
|
|
1110
|
+
* @param resolveAccess - execution-world path resolver, when available.
|
|
1111
|
+
* @returns the user-turn part list, or `undefined` when the run carried no image.
|
|
1112
|
+
*/
|
|
1113
|
+
function hoistedToolImages(run, images, resolveAccess) {
|
|
1114
|
+
if (run.length === 0) return void 0;
|
|
1115
|
+
if (images === void 0) throw new LlmError("aishell-llm request images were not prepared", "INVALID_REQUEST");
|
|
1116
|
+
const parts = [{
|
|
1117
|
+
type: "text",
|
|
1118
|
+
text: TOOL_IMAGES_INTRO
|
|
1119
|
+
}];
|
|
1120
|
+
for (const blocks of run) for (const block of blocks) {
|
|
1121
|
+
if (block.type !== "image") continue;
|
|
1122
|
+
const version = requireRequestImage(images, block.attachment);
|
|
1123
|
+
parts.push({
|
|
1124
|
+
type: "text",
|
|
1125
|
+
text: requestImageHandleText(block.attachment, version, resolveAccess?.(block.attachment))
|
|
1126
|
+
});
|
|
1127
|
+
parts.push({
|
|
1128
|
+
type: "image_url",
|
|
1129
|
+
image_url: { url: dataUrl(version) }
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
return parts;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Content for one user message: a parts list when it carries an image,
|
|
1136
|
+
* otherwise `undefined` so the caller falls back to plain text.
|
|
1137
|
+
*
|
|
1138
|
+
* Each image is preceded by a text part carrying its stable model-facing
|
|
1139
|
+
* handle, which names the image and the dimensions it was projected to — so a
|
|
1140
|
+
* model that cannot use the bytes can still refer to the occurrence, and one
|
|
1141
|
+
* that can reads what was actually sent rather than what was uploaded.
|
|
1142
|
+
*
|
|
1143
|
+
* @param blocks - content blocks of one user message.
|
|
1144
|
+
* @param images - prepared request versions.
|
|
1145
|
+
* @param resolveAccess - execution-world path resolver, when available.
|
|
1146
|
+
* @returns the part list, or `undefined` when the message has no image.
|
|
1147
|
+
*/
|
|
1148
|
+
function contentFor(blocks, images, resolveAccess) {
|
|
1149
|
+
if (!blocks.some((block) => block.type === "image")) return void 0;
|
|
1150
|
+
if (images === void 0) throw new LlmError("aishell-llm request images were not prepared", "INVALID_REQUEST");
|
|
1151
|
+
const parts = [];
|
|
1152
|
+
for (const block of blocks) {
|
|
1153
|
+
if (block.type === "image") {
|
|
1154
|
+
const version = requireRequestImage(images, block.attachment);
|
|
1155
|
+
parts.push({
|
|
1156
|
+
type: "text",
|
|
1157
|
+
text: requestImageHandleText(block.attachment, version, resolveAccess?.(block.attachment))
|
|
1158
|
+
});
|
|
1159
|
+
parts.push({
|
|
1160
|
+
type: "image_url",
|
|
1161
|
+
image_url: { url: dataUrl(version) }
|
|
1162
|
+
});
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (block.type === "text" || block.type === "reasoning") {
|
|
1166
|
+
if (block.text.length > 0) parts.push({
|
|
1167
|
+
type: "text",
|
|
1168
|
+
text: block.text
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return parts;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Inline data URL for one prepared image.
|
|
1176
|
+
*
|
|
1177
|
+
* Media type comes from the request *version*, not the source reference: the
|
|
1178
|
+
* attachment service may re-encode during projection (that is what the byte
|
|
1179
|
+
* target is for), so the version is the only description of the bytes actually
|
|
1180
|
+
* being sent.
|
|
1181
|
+
*
|
|
1182
|
+
* Encoded through `btoa` over the byte string rather than `Buffer`: this
|
|
1183
|
+
* package compiles host and browser halves in one project under `types: []`
|
|
1184
|
+
* with the DOM lib, so the Node global is neither declared nor reachable here.
|
|
1185
|
+
* `btoa` is a global in both Node 22+ and every browser, which is why it is
|
|
1186
|
+
* the one that typechecks in both halves.
|
|
1187
|
+
*
|
|
1188
|
+
* @param version - prepared request version.
|
|
1189
|
+
* @returns a base64 data URL.
|
|
1190
|
+
*/
|
|
1191
|
+
function dataUrl(version) {
|
|
1192
|
+
let binary = "";
|
|
1193
|
+
for (const byte of version.data) binary += String.fromCharCode(byte);
|
|
1194
|
+
return `data:${version.mediaType};base64,${btoa(binary)}`;
|
|
1195
|
+
}
|
|
52
1196
|
/** Type guard for tool-call blocks. */
|
|
53
1197
|
function isToolCallBlock(block) {
|
|
54
1198
|
return block.type === "tool-call";
|
|
@@ -81,7 +1225,8 @@ function serializeTool(tool) {
|
|
|
81
1225
|
//#region src/sse.ts
|
|
82
1226
|
const DONE = "[DONE]";
|
|
83
1227
|
async function* parseSse(stream) {
|
|
84
|
-
const
|
|
1228
|
+
const decoder = new TextDecoderStream();
|
|
1229
|
+
const events = stream.pipeThrough(decoder).pipeThrough(new EventSourceParserStream());
|
|
85
1230
|
for await (const { data } of events) {
|
|
86
1231
|
yield data;
|
|
87
1232
|
if (data === DONE) return;
|
|
@@ -260,12 +1405,50 @@ async function* translate(payloads) {
|
|
|
260
1405
|
};
|
|
261
1406
|
}
|
|
262
1407
|
|
|
1408
|
+
//#endregion
|
|
1409
|
+
//#region src/provider-name.ts
|
|
1410
|
+
/**
|
|
1411
|
+
* The aishell provider's display name, shared by both plugin halves.
|
|
1412
|
+
*
|
|
1413
|
+
* The Host half registers it on the configurable-provider directory (the
|
|
1414
|
+
* Models page renders it as the provider row's name), and the browser half's
|
|
1415
|
+
* row shadow finds that row by matching the rendered name against this
|
|
1416
|
+
* literal (see `client/provider-row-shadow.ts`). It must stay a single
|
|
1417
|
+
* literal string — the shadow compares `textContent` with it.
|
|
1418
|
+
*
|
|
1419
|
+
* It is also the name `providerInfo()` reports, so the picker and the Models
|
|
1420
|
+
* row call the route the same thing: this package's whole surface is one
|
|
1421
|
+
* product, and qualifying it by cloud ("AI Shell (Huawei Cloud)") added a
|
|
1422
|
+
* distinction no reader needed — there is no second AI Shell to tell it
|
|
1423
|
+
* apart from.
|
|
1424
|
+
*/
|
|
1425
|
+
const PROVIDER_DISPLAY_NAME = "AI Shell";
|
|
1426
|
+
|
|
263
1427
|
//#endregion
|
|
264
1428
|
//#region src/adapter.ts
|
|
265
|
-
/**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
1429
|
+
/**
|
|
1430
|
+
* Whether a gateway error body reports an id the gateway does not carry.
|
|
1431
|
+
*
|
|
1432
|
+
* Both fields are accepted because the gateway emits them from one definition
|
|
1433
|
+
* and either proves the same fact; a proxy that strips one should not disable
|
|
1434
|
+
* the diagnosis. A body that is not the gateway's JSON is not a match.
|
|
1435
|
+
*
|
|
1436
|
+
* @param text - the error response body, as received.
|
|
1437
|
+
* @returns whether this is the "model not configured on this gateway" answer.
|
|
1438
|
+
*/
|
|
1439
|
+
function isModelNotFound(text) {
|
|
1440
|
+
let body;
|
|
1441
|
+
try {
|
|
1442
|
+
body = JSON.parse(text);
|
|
1443
|
+
} catch {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
if (typeof body !== "object" || body === null) return false;
|
|
1447
|
+
const envelope = body;
|
|
1448
|
+
if (envelope.error_code === "AISHELL.1004") return true;
|
|
1449
|
+
const inner = envelope.error;
|
|
1450
|
+
return typeof inner === "object" && inner !== null && inner.code === "model_not_found";
|
|
1451
|
+
}
|
|
269
1452
|
/**
|
|
270
1453
|
* Map the gateway's `modalities` array (e.g. `["text","image"]`) to DSH's
|
|
271
1454
|
* `ModelModality` vocabulary. DSH currently defines only `'text' | 'image'`;
|
|
@@ -281,11 +1464,16 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
281
1464
|
constructor(opts) {
|
|
282
1465
|
super();
|
|
283
1466
|
this.opts = opts;
|
|
1467
|
+
this.imageAccess = (ref) => {
|
|
1468
|
+
const attachments = this.opts.resolveAttachments?.();
|
|
1469
|
+
if (attachments === void 0) return void 0;
|
|
1470
|
+
return resolveImageAttachmentAccess(attachments, (hostPath) => this.opts.resolveHostPath?.(hostPath), ref);
|
|
1471
|
+
};
|
|
284
1472
|
}
|
|
285
1473
|
providerInfo(provider) {
|
|
286
1474
|
return {
|
|
287
1475
|
id: provider,
|
|
288
|
-
name:
|
|
1476
|
+
name: PROVIDER_DISPLAY_NAME
|
|
289
1477
|
};
|
|
290
1478
|
}
|
|
291
1479
|
/**
|
|
@@ -297,8 +1485,7 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
297
1485
|
async fetchWireModels() {
|
|
298
1486
|
const now = Date.now();
|
|
299
1487
|
if (this.modelsCache !== void 0 && this.modelsCache.expires > now) return this.modelsCache.promise;
|
|
300
|
-
|
|
301
|
-
if (!creds?.ak) return [];
|
|
1488
|
+
if (!this.hasCredentials()) return [];
|
|
302
1489
|
const promise = (async () => {
|
|
303
1490
|
const response = await this.signedFetch("/models", "GET", void 0, void 0);
|
|
304
1491
|
if (!response.ok) return [];
|
|
@@ -316,7 +1503,7 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
316
1503
|
return wireModels.map((m) => ({
|
|
317
1504
|
provider,
|
|
318
1505
|
id: m.id,
|
|
319
|
-
name: m.id,
|
|
1506
|
+
name: m.name ?? m.id,
|
|
320
1507
|
...mapModalities(m.modalities)
|
|
321
1508
|
}));
|
|
322
1509
|
}
|
|
@@ -326,7 +1513,7 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
326
1513
|
const base = {
|
|
327
1514
|
provider,
|
|
328
1515
|
id: model,
|
|
329
|
-
name: model,
|
|
1516
|
+
name: wire?.name ?? model,
|
|
330
1517
|
...mapModalities(wire?.modalities)
|
|
331
1518
|
};
|
|
332
1519
|
if (wire === void 0) return base;
|
|
@@ -343,22 +1530,40 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
343
1530
|
})) } } : {}
|
|
344
1531
|
};
|
|
345
1532
|
}
|
|
346
|
-
/**
|
|
1533
|
+
/**
|
|
1534
|
+
* Whether a usable AK/SK pair is stored.
|
|
1535
|
+
*
|
|
1536
|
+
* The one predicate behind every "can this route do anything" question:
|
|
1537
|
+
* `fetchWireModels` returns an empty catalog without it, and the Models-page
|
|
1538
|
+
* card reports readiness from it. Kept in one place so the catalog and the
|
|
1539
|
+
* card cannot disagree about whether this route is usable.
|
|
1540
|
+
*
|
|
1541
|
+
* @returns whether both halves of the signing credential are present.
|
|
1542
|
+
*/
|
|
1543
|
+
hasCredentials() {
|
|
1544
|
+
return this.readCredentials() !== void 0;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Read the AK/SK pair from the credential store.
|
|
1548
|
+
*
|
|
1549
|
+
* Delegates to the shared reader: this adapter, `aishell-e2b`,
|
|
1550
|
+
* `aishell-credentials` and `aishell-deploy` all read the same record, and a
|
|
1551
|
+
* local copy of that rule is how two of them end up disagreeing about what
|
|
1552
|
+
* "credentials are configured" means.
|
|
1553
|
+
*
|
|
1554
|
+
* Read per request, never captured — that is what makes a rotation in the
|
|
1555
|
+
* credential form apply to the very next call.
|
|
1556
|
+
*/
|
|
347
1557
|
readCredentials() {
|
|
348
|
-
|
|
349
|
-
const sk = this.opts.keyring.get(SK_ACCOUNT);
|
|
350
|
-
if (!ak || !sk) return void 0;
|
|
351
|
-
const securityToken = this.opts.keyring.get(TOKEN_ACCOUNT);
|
|
352
|
-
return {
|
|
353
|
-
ak,
|
|
354
|
-
sk,
|
|
355
|
-
...securityToken ? { securityToken } : {}
|
|
356
|
-
};
|
|
1558
|
+
return readStoredPair(this.opts.keyring);
|
|
357
1559
|
}
|
|
358
|
-
/**
|
|
1560
|
+
/**
|
|
1561
|
+
* Read credentials or throw `MISSING_CREDENTIAL` — every signing code path
|
|
1562
|
+
* goes through here.
|
|
1563
|
+
*/
|
|
359
1564
|
requireCredentials() {
|
|
360
1565
|
const creds = this.readCredentials();
|
|
361
|
-
if (!creds) throw new LlmError("aishell-llm:
|
|
1566
|
+
if (!creds) throw new LlmError("aishell-llm: not authenticated with Huawei Cloud IAM", "MISSING_CREDENTIAL");
|
|
362
1567
|
return creds;
|
|
363
1568
|
}
|
|
364
1569
|
/**
|
|
@@ -367,9 +1572,9 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
367
1572
|
*/
|
|
368
1573
|
async signedFetch(path, method, body, signal) {
|
|
369
1574
|
const creds = this.requireCredentials();
|
|
370
|
-
const
|
|
371
|
-
const headers = sign(method,
|
|
372
|
-
return fetch(
|
|
1575
|
+
const requestUrl = new URL(`${this.opts.resolveBaseUrl()}${path}`);
|
|
1576
|
+
const headers = sign(method, requestUrl.host, requestUrl.pathname, {}, body ?? "", creds);
|
|
1577
|
+
return fetch(requestUrl, {
|
|
373
1578
|
method,
|
|
374
1579
|
headers: {
|
|
375
1580
|
host: headers.host,
|
|
@@ -382,18 +1587,66 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
|
|
|
382
1587
|
...signal ? { signal } : {}
|
|
383
1588
|
});
|
|
384
1589
|
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Diagnose an "unsupported model" rejection and append what to do about it.
|
|
1592
|
+
*
|
|
1593
|
+
* The gateway answers a model id it does not carry with `AISHELL.1004`
|
|
1594
|
+
* (`model_not_found`, HTTP 404 — see {@link isModelNotFound}). The default
|
|
1595
|
+
* selection is a pinned literal (it must be readable before any credential or
|
|
1596
|
+
* network call could enumerate the catalog), so the day the catalog drops
|
|
1597
|
+
* that id, every fresh session fails here. Rather than leave the user to
|
|
1598
|
+
* reverse-engineer the supported set, the failure itself asks the gateway
|
|
1599
|
+
* (the catalog cache usually makes this a second signed GET already in
|
|
1600
|
+
* flight) and names the models it currently accepts.
|
|
1601
|
+
*
|
|
1602
|
+
* Returns '' for any other failure, so the message stays untouched there.
|
|
1603
|
+
*/
|
|
1604
|
+
async unsupportedModelHint(text, options) {
|
|
1605
|
+
if (!isModelNotFound(text)) return "";
|
|
1606
|
+
const ids = (await this.fetchWireModels().catch(() => [])).map((m) => m.id).filter((id) => id.length > 0);
|
|
1607
|
+
if (ids.length === 0) return "";
|
|
1608
|
+
return ` (this gateway currently supports: ${ids.join(", ")}; switch the model in the picker — the switch also becomes your saved default. Requested: ${options.model})`;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Turn the harness history into the wire body, loading image bytes on the
|
|
1612
|
+
* way.
|
|
1613
|
+
*
|
|
1614
|
+
* Deliberately the whole image pipeline and nothing else, so `stream` stays
|
|
1615
|
+
* about transport. Ordering matters and mirrors the official adapters:
|
|
1616
|
+
* resolve the attachment service, prepare one request version per retained
|
|
1617
|
+
* image, fail the request if the budget is exceeded (handing
|
|
1618
|
+
* `dsh-compaction-image-offload` its decision), and only then project the
|
|
1619
|
+
* offloaded occurrences to placeholder text — a mark set by an earlier
|
|
1620
|
+
* request's offload decision, which serialization must replace rather than
|
|
1621
|
+
* look up bytes for.
|
|
1622
|
+
*/
|
|
1623
|
+
async buildBody(options) {
|
|
1624
|
+
if (!historyHasImage(options.messages)) return JSON.stringify(serializeRequest(options, void 0, this.imageAccess));
|
|
1625
|
+
assertImageRoles(options.messages);
|
|
1626
|
+
const attachments = this.opts.resolveAttachments?.();
|
|
1627
|
+
if (attachments === void 0) throw new LlmError("aishell-llm image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1628
|
+
const messages = options.messages;
|
|
1629
|
+
const images = await prepareRequestImages(messages, attachments, this.opts.resolveImageBudget(), options.signal);
|
|
1630
|
+
assertRequestImageBudget(messages, images, this.opts.resolveMaxRequestImageBytes());
|
|
1631
|
+
const projected = [...projectOffloaded(messages, this.imageAccess)];
|
|
1632
|
+
return JSON.stringify(serializeRequest({
|
|
1633
|
+
...options,
|
|
1634
|
+
messages: projected
|
|
1635
|
+
}, images, this.imageAccess));
|
|
1636
|
+
}
|
|
385
1637
|
async *stream(options) {
|
|
386
|
-
const body =
|
|
1638
|
+
const body = await this.buildBody(options);
|
|
387
1639
|
let response;
|
|
388
1640
|
try {
|
|
389
1641
|
response = await this.signedFetch("/chat/completions", "POST", body, options.signal);
|
|
390
1642
|
} catch (error) {
|
|
391
1643
|
if (options.signal?.aborted) throw error;
|
|
1644
|
+
if (error instanceof LlmError) throw error;
|
|
392
1645
|
throw new LlmError(`aishell-llm: request failed: ${error instanceof Error ? error.message : String(error)}`, "NETWORK_ERROR");
|
|
393
1646
|
}
|
|
394
1647
|
if (!response.ok) {
|
|
395
1648
|
const text = await response.text().catch(() => "");
|
|
396
|
-
throw new LlmError(`aishell-llm: gateway returned ${response.status}: ${text}
|
|
1649
|
+
throw new LlmError(`aishell-llm: gateway returned ${response.status}: ${text}` + await this.unsupportedModelHint(text, options), "PROVIDER_ERROR", { status: response.status });
|
|
397
1650
|
}
|
|
398
1651
|
if (!response.body) throw new LlmError("aishell-llm: gateway returned no body", "EMPTY_RESPONSE");
|
|
399
1652
|
yield* translate(parseSse(response.body));
|
|
@@ -403,21 +1656,65 @@ GatewayAdapter.MODELS_TTL_MS = 6e4;
|
|
|
403
1656
|
|
|
404
1657
|
//#endregion
|
|
405
1658
|
//#region src/index.ts
|
|
406
|
-
/** Default chat API base URL. */
|
|
407
|
-
const DEFAULT_BASE_URL = "https://
|
|
1659
|
+
/** Default chat API base URL. Both `/chat/completions` and `/models` hang below `/v1`. */
|
|
1660
|
+
const DEFAULT_BASE_URL = "https://openai.aishell.openbuildlab.com/v1";
|
|
1661
|
+
/**
|
|
1662
|
+
* Default total-pixel cap one request image is scaled into, and the encoded
|
|
1663
|
+
* byte target the encoder aims for. Both match the pi-ai defaults, for the
|
|
1664
|
+
* same reason: the gateway publishes no image budget, so the route needs a
|
|
1665
|
+
* bound that keeps one large upload from dominating a request.
|
|
1666
|
+
*/
|
|
1667
|
+
const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 4194304;
|
|
1668
|
+
const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1048576;
|
|
1669
|
+
/**
|
|
1670
|
+
* Default accumulated base64 bytes one request may carry before its oldest
|
|
1671
|
+
* images must be offloaded. Above this the request fails with
|
|
1672
|
+
* `IMAGE_OFFLOAD_REQUIRED` and `dsh-compaction-image-offload` (mounted by
|
|
1673
|
+
* `dsh-base`) replaces the oldest occurrences with placeholders. Again the
|
|
1674
|
+
* pi-ai default.
|
|
1675
|
+
*/
|
|
1676
|
+
const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20971520;
|
|
1677
|
+
/**
|
|
1678
|
+
* Settings schema for this entry.
|
|
1679
|
+
*
|
|
1680
|
+
* The schema exists for two reasons beyond configurability: the settings
|
|
1681
|
+
* mirror only lists namespaces whose plugin declares a Config, and the Models
|
|
1682
|
+
* page derives a provider row's existence from that listing — without a
|
|
1683
|
+
* schema, the `aishell` route registers its adapter but never shows up as a
|
|
1684
|
+
* configurable provider.
|
|
1685
|
+
*/
|
|
1686
|
+
const Config = Schema.object({
|
|
1687
|
+
baseUrl: Schema.string().default(DEFAULT_BASE_URL).volatile(),
|
|
1688
|
+
requestImagePixelBudget: Schema.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET).volatile(),
|
|
1689
|
+
requestImageMaxBytes: Schema.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES).volatile(),
|
|
1690
|
+
maxRequestImageBytes: Schema.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES).volatile()
|
|
1691
|
+
});
|
|
408
1692
|
const name = "aishell-llm";
|
|
409
1693
|
const inject = ["llm"];
|
|
410
1694
|
/** The provider route this plugin owns. */
|
|
411
1695
|
const PROVIDER = "aishell";
|
|
412
|
-
function apply(ctx, config
|
|
413
|
-
const store = createCredentialsStore(
|
|
1696
|
+
function apply(ctx, config) {
|
|
1697
|
+
const store = createCredentialsStore();
|
|
414
1698
|
const adapter = new GatewayAdapter({
|
|
415
|
-
|
|
416
|
-
keyring: store
|
|
1699
|
+
resolveBaseUrl: () => config.baseUrl.get(),
|
|
1700
|
+
keyring: store,
|
|
1701
|
+
resolveAttachments: () => ctx.get("attachments"),
|
|
1702
|
+
resolveHostPath: (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath),
|
|
1703
|
+
resolveImageBudget: () => ({
|
|
1704
|
+
maxPixels: config.requestImagePixelBudget.get(),
|
|
1705
|
+
maxBytes: config.requestImageMaxBytes.get()
|
|
1706
|
+
}),
|
|
1707
|
+
resolveMaxRequestImageBytes: () => config.maxRequestImageBytes.get()
|
|
417
1708
|
});
|
|
418
1709
|
ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
1710
|
+
ctx.llm.registerConfigurableProviders([{
|
|
1711
|
+
provider: PROVIDER,
|
|
1712
|
+
displayName: PROVIDER_DISPLAY_NAME,
|
|
1713
|
+
settingsNs: ctx.fiber.entry?.options.id ?? name,
|
|
1714
|
+
settingsPath: []
|
|
1715
|
+
}]);
|
|
419
1716
|
}
|
|
420
1717
|
|
|
421
1718
|
//#endregion
|
|
422
|
-
export { PROVIDER, apply, inject, name };
|
|
1719
|
+
export { Config, PROVIDER, PROVIDER_DISPLAY_NAME, apply, inject, name };
|
|
423
1720
|
//# sourceMappingURL=index.js.map
|