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