arcane-os 0.5.8 → 0.5.10
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/CHANGELOG.md +25 -0
- package/README.md +32 -27
- package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
- package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +95 -363
- package/browser-runtime/ai/speech-worker-client.mjs +51 -14
- package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
- package/browser-runtime/dependencies/strong-type/package.json +22 -23
- package/browser-runtime/event-manager.mjs +27 -41
- package/package.json +2 -3
- package/runtime/arcane/components/chat.html +1 -1
- package/runtime/arcane/components/speech.html +8 -38
- package/runtime/arcane/components/voice-transcription.html +1 -1
- package/runtime/arcane/css/theme.css +1 -1
- package/runtime/arcane/entities/Chat.js +12 -7
- package/runtime/arcane/modules/AI.js +506 -391
- package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
- package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
- package/runtime/arcane/modules/AIRuntimeState.js +2 -18
- package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
- package/runtime/arcane/modules/CalculatorEngine.js +0 -4
- package/runtime/arcane/modules/ChatRecords.js +169 -1
- package/runtime/arcane/modules/CommunicationHub.js +0 -19
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
- package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
- package/runtime/arcane/modules/Errors.js +6 -19
- package/runtime/arcane/modules/Mail.js +1 -2
- package/runtime/arcane/modules/MailTransport.mjs +1 -3
- package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
- package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
- package/runtime/arcane/modules/ScreenCapture.js +2 -4
- package/runtime/arcane/modules/SpeechPlayback.js +0 -27
- package/runtime/arcane/modules/WaitForComponent.js +0 -1
- package/src/app-descriptor.mjs +1 -1
- package/src/doctor.mjs +1 -3
- package/src/event-manager.mjs +27 -41
- package/src/import-map.mjs +1 -1
- package/src/index.mjs +2 -9
- package/src/installed-sdk-runtime.mjs +1 -11
- package/src/mail-api.mjs +0 -1
- package/src/native-provider-loader.mjs +0 -9
- package/src/scaffold.mjs +9 -21
- package/src/toolchain.mjs +5 -28
- package/src/workspace-runtime.mjs +0 -4
- package/src/workspace.mjs +2 -23
|
@@ -10,9 +10,6 @@ import { arcaneEvents } from "../event-manager.mjs";
|
|
|
10
10
|
|
|
11
11
|
const completeValue = (value) => value;
|
|
12
12
|
|
|
13
|
-
// Compatibility export only. Ordinary model caching no longer creates or
|
|
14
|
-
// requires completion manifests, receipts, or byte identities.
|
|
15
|
-
const MODEL_MANIFEST_SCHEMA = "arcane.ai.browser-wasm.model.v4";
|
|
16
13
|
const BROWSER_MODEL_SOURCES = new WeakSet();
|
|
17
14
|
const BROWSER_MODEL_SOURCE_METADATA = new WeakMap();
|
|
18
15
|
const BROWSER_MODEL_SOURCE_TRANSPORTS = new WeakMap();
|
|
@@ -23,17 +20,12 @@ const AI_PROVIDER_PROTOCOL = "arcane-ai-provider/2";
|
|
|
23
20
|
const AI_MODEL_AUTHORITY_PROTOCOL = "arcane-ai-model-authority/1";
|
|
24
21
|
const WEBGPU_ADAPTER_SELECTED_EVENT = "arcane.ai.browser-wasm.webgpu.adapter.selected";
|
|
25
22
|
const WEBGPU_ADAPTER_SELECTION_PROTOCOL = "arcane-ai-webgpu-adapter-selection/1";
|
|
26
|
-
const CHROME_HIGH_PERFORMANCE_GPU_FLAG_URL =
|
|
27
|
-
"chrome://flags/#force-high-performance-gpu";
|
|
28
23
|
const MODEL_LOAD_HEARTBEAT_MS = 5_000;
|
|
29
24
|
const DEFAULT_MODEL_DOWNLOAD_CONCURRENCY = 4;
|
|
30
25
|
const MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 250;
|
|
31
26
|
const MODEL_DOWNLOAD_SPEED_WINDOW_MS = 5_000;
|
|
32
27
|
const MODEL_DOWNLOAD_MAX_RANGE_PARTS = 4_096;
|
|
33
28
|
const MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 4_000_000;
|
|
34
|
-
const LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS = 16;
|
|
35
|
-
const LEGACY_MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 128_000_000;
|
|
36
|
-
const INTEL_VENDOR_ID = 0x8086;
|
|
37
29
|
const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
|
|
38
30
|
let highPerformanceGpuNoticeShown = false;
|
|
39
31
|
|
|
@@ -132,39 +124,6 @@ function modelHttpRanges(total) {
|
|
|
132
124
|
);
|
|
133
125
|
}
|
|
134
126
|
|
|
135
|
-
function modelHttpSubranges(range) {
|
|
136
|
-
const length = range.end - range.start + 1;
|
|
137
|
-
const maximumParts = Math.floor(
|
|
138
|
-
MODEL_DOWNLOAD_MAX_RANGE_PARTS / LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
139
|
-
);
|
|
140
|
-
return completeValue(modelHttpRangesFor(
|
|
141
|
-
length,
|
|
142
|
-
maximumParts,
|
|
143
|
-
MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
144
|
-
).map((part) => completeValue({
|
|
145
|
-
start: range.start + part.start,
|
|
146
|
-
end: range.start + part.end,
|
|
147
|
-
total: range.total,
|
|
148
|
-
})));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function modelHttpRangePlans(total) {
|
|
152
|
-
const current = modelHttpRanges(total);
|
|
153
|
-
const legacy = modelHttpRangesFor(
|
|
154
|
-
total,
|
|
155
|
-
LEGACY_MODEL_DOWNLOAD_MAX_RANGE_PARTS,
|
|
156
|
-
LEGACY_MODEL_DOWNLOAD_TARGET_RANGE_BYTES,
|
|
157
|
-
);
|
|
158
|
-
if (
|
|
159
|
-
current.length === legacy.length
|
|
160
|
-
&& current.every((range, index) => (
|
|
161
|
-
range.start === legacy[index].start
|
|
162
|
-
&& range.end === legacy[index].end
|
|
163
|
-
))
|
|
164
|
-
) return completeValue([current]);
|
|
165
|
-
return completeValue([current, legacy]);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
127
|
function progressClock() {
|
|
169
128
|
return typeof globalThis.performance?.now === "function"
|
|
170
129
|
? globalThis.performance.now()
|
|
@@ -208,7 +167,7 @@ function modelIdText(value) {
|
|
|
208
167
|
return id;
|
|
209
168
|
}
|
|
210
169
|
|
|
211
|
-
function descriptorFileName(value, url
|
|
170
|
+
function descriptorFileName(value, url) {
|
|
212
171
|
const suppliedName = value.name;
|
|
213
172
|
let name = suppliedName;
|
|
214
173
|
if (name === undefined) {
|
|
@@ -218,7 +177,6 @@ function descriptorFileName(value, url, fallbackName = null) {
|
|
|
218
177
|
} catch {
|
|
219
178
|
name = "";
|
|
220
179
|
}
|
|
221
|
-
if (!name && fallbackName) name = fallbackName;
|
|
222
180
|
}
|
|
223
181
|
name = requiredText(name, "file name");
|
|
224
182
|
if (
|
|
@@ -236,23 +194,19 @@ function descriptorFileName(value, url, fallbackName = null) {
|
|
|
236
194
|
return name;
|
|
237
195
|
}
|
|
238
196
|
|
|
239
|
-
function descriptorFile(value
|
|
197
|
+
function descriptorFile(value) {
|
|
240
198
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
241
199
|
throw new TypeError("Each browser model file descriptor must be an object.");
|
|
242
200
|
}
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
&& value.immutableUrl !== undefined
|
|
246
|
-
&& value.url !== value.immutableUrl
|
|
247
|
-
) {
|
|
248
|
-
throw new TypeError("Browser model file url and legacy immutableUrl must match when both are provided.");
|
|
201
|
+
if (Object.hasOwn(value, "immutableUrl")) {
|
|
202
|
+
throw new TypeError("Browser model file descriptors must use url.");
|
|
249
203
|
}
|
|
250
|
-
const url = modelSourceUrl(value.url
|
|
204
|
+
const url = modelSourceUrl(value.url);
|
|
251
205
|
if (!url) {
|
|
252
206
|
throw new TypeError("Browser model file url must be a valid absolute URL.");
|
|
253
207
|
}
|
|
254
208
|
const file = {
|
|
255
|
-
name: descriptorFileName(value, url
|
|
209
|
+
name: descriptorFileName(value, url),
|
|
256
210
|
url: url.href,
|
|
257
211
|
};
|
|
258
212
|
if (Number.isSafeInteger(value.bytes) && value.bytes > 0) {
|
|
@@ -266,24 +220,14 @@ function modelDescriptor(value) {
|
|
|
266
220
|
throw new TypeError("A browser model descriptor is required.");
|
|
267
221
|
}
|
|
268
222
|
const id = modelIdText(value.id);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let files;
|
|
276
|
-
let legacy = false;
|
|
277
|
-
if (hasFiles) {
|
|
278
|
-
if (!Array.isArray(value.files) || value.files.length === 0) {
|
|
279
|
-
throw new TypeError("Browser model files must be a nonempty ordered array.");
|
|
280
|
-
}
|
|
281
|
-
files = value.files.map((file) => descriptorFile(file));
|
|
282
|
-
} else {
|
|
283
|
-
legacy = true;
|
|
284
|
-
const safeId = id.replace(/[^a-z0-9._-]+/giu, "_");
|
|
285
|
-
files = [descriptorFile(value, { fallbackName: `${safeId}.gguf` })];
|
|
223
|
+
if (["url", "immutableUrl", "name", "bytes", "sha256", "licenseSpdx", "sourceRevision"]
|
|
224
|
+
.some((field) => Object.hasOwn(value, field))) {
|
|
225
|
+
throw new TypeError("Browser model descriptor files must be declared in files[].");
|
|
226
|
+
}
|
|
227
|
+
if (!Array.isArray(value.files) || value.files.length === 0) {
|
|
228
|
+
throw new TypeError("Browser model files must be a nonempty ordered array.");
|
|
286
229
|
}
|
|
230
|
+
let files = value.files.map((file) => descriptorFile(file));
|
|
287
231
|
const names = new Set();
|
|
288
232
|
const urls = new Set();
|
|
289
233
|
for (const file of files) {
|
|
@@ -303,16 +247,8 @@ function modelDescriptor(value) {
|
|
|
303
247
|
url: file.url,
|
|
304
248
|
...(file.bytes === undefined ? {} : { bytes: file.bytes }),
|
|
305
249
|
})));
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const [file] = files;
|
|
309
|
-
descriptor = { id, url: file.url };
|
|
310
|
-
if (file.bytes !== undefined) descriptor.bytes = file.bytes;
|
|
311
|
-
} else {
|
|
312
|
-
descriptor = { id, files: publicFiles };
|
|
313
|
-
}
|
|
314
|
-
descriptor = completeValue(descriptor);
|
|
315
|
-
MODEL_DESCRIPTOR_METADATA.set(descriptor, completeValue({ files, legacy }));
|
|
250
|
+
const descriptor = completeValue({ id, files: publicFiles });
|
|
251
|
+
MODEL_DESCRIPTOR_METADATA.set(descriptor, completeValue({ files }));
|
|
316
252
|
return descriptor;
|
|
317
253
|
}
|
|
318
254
|
|
|
@@ -324,44 +260,71 @@ function publicDescriptor(source) {
|
|
|
324
260
|
return modelDescriptor(source);
|
|
325
261
|
}
|
|
326
262
|
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
263
|
+
function highPerformanceGpuBrowser() {
|
|
264
|
+
const navigatorObject = globalThis.navigator;
|
|
265
|
+
const userAgent = String(navigatorObject?.userAgent ?? "");
|
|
266
|
+
const clientHints = navigatorObject?.userAgentData;
|
|
267
|
+
const platform = String(clientHints?.platform || navigatorObject?.platform || userAgent);
|
|
268
|
+
if (
|
|
269
|
+
!/\b(?:Windows|Win32|Win64)\b/iu.test(platform)
|
|
270
|
+
|| clientHints?.mobile === true
|
|
271
|
+
|| /\b(?:Android|iPhone|iPad|iPod|Mobile)\b/iu.test(userAgent)
|
|
272
|
+
) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
332
275
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
276
|
+
const brands = new Set();
|
|
277
|
+
for (const entry of clientHints?.brands ?? []) {
|
|
278
|
+
brands.add(entry.brand);
|
|
279
|
+
}
|
|
280
|
+
// Vivaldi can append its identity while also identifying as Edge or Chrome.
|
|
281
|
+
if (brands.has("Vivaldi") || /\bVivaldi\//u.test(userAgent)) {
|
|
282
|
+
return { name: "Vivaldi", url: "vivaldi://flags/#force-high-performance-gpu" };
|
|
283
|
+
}
|
|
284
|
+
if (brands.has("Brave") || typeof navigatorObject?.brave?.isBrave === "function") {
|
|
285
|
+
return { name: "Brave", url: "brave://flags/#force-high-performance-gpu" };
|
|
286
|
+
}
|
|
287
|
+
if (brands.has("Opera") || /\bOPR\//u.test(userAgent)) {
|
|
288
|
+
return { name: "Opera", url: "opera://flags/#force-high-performance-gpu" };
|
|
289
|
+
}
|
|
290
|
+
if (brands.has("Microsoft Edge") || /\bEdg\//u.test(userAgent)) {
|
|
291
|
+
return { name: "Microsoft Edge", url: "edge://flags/#force-high-performance-gpu" };
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
brands.has("Chromium")
|
|
295
|
+
|| brands.has("Google Chrome")
|
|
296
|
+
|| /\b(?:Chrome|Chromium)\//u.test(userAgent)
|
|
297
|
+
) {
|
|
298
|
+
// Some Chromium browsers mask their brand. about:// uses their own flags page.
|
|
299
|
+
return { name: "your browser", url: "about://flags/#force-high-performance-gpu" };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
342
302
|
}
|
|
343
303
|
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
304
|
+
function notifyHighPerformanceGpu(adapter) {
|
|
305
|
+
if (highPerformanceGpuNoticeShown) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const browser = highPerformanceGpuBrowser();
|
|
309
|
+
if (!browser) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
highPerformanceGpuNoticeShown = true;
|
|
313
|
+
try {
|
|
314
|
+
globalThis.open?.(browser.url, "_blank", "noopener,noreferrer");
|
|
315
|
+
} catch {
|
|
316
|
+
// Browsers may reject internal-page navigation from web content.
|
|
317
|
+
}
|
|
318
|
+
const adapterName = adapter.description || adapter.name
|
|
319
|
+
|| [adapter.vendor, adapter.architecture].filter(Boolean).join(" ")
|
|
320
|
+
|| "the available WebGPU adapter";
|
|
321
|
+
globalThis.alert?.(
|
|
322
|
+
`Selected WebGPU adapter: ${adapterName}.\n\n`
|
|
323
|
+
+ `If this computer has multiple GPUs, enable “Force High Performance GPU” in ${browser.name} `
|
|
324
|
+
+ "to request the high-performance GPU when available. "
|
|
325
|
+
+ `Then completely close and reopen ${browser.name} before loading the model again.\n\n`
|
|
326
|
+
+ `If the flags page did not open, paste ${browser.url} into the address bar.`,
|
|
327
|
+
);
|
|
365
328
|
}
|
|
366
329
|
|
|
367
330
|
function emitWebgpuAdapterSelection(source, runtime) {
|
|
@@ -383,7 +346,7 @@ function emitWebgpuAdapterSelection(source, runtime) {
|
|
|
383
346
|
category: "capability",
|
|
384
347
|
}));
|
|
385
348
|
} finally {
|
|
386
|
-
|
|
349
|
+
notifyHighPerformanceGpu(webgpu.adapter);
|
|
387
350
|
}
|
|
388
351
|
}
|
|
389
352
|
|
|
@@ -394,7 +357,6 @@ function sourceMetadata(source) {
|
|
|
394
357
|
|
|
395
358
|
/**
|
|
396
359
|
* Creates a browser download authority for one caller-supplied model file set.
|
|
397
|
-
* Legacy one-file descriptors normalize to one ordered member.
|
|
398
360
|
* Security is an intent-only seam and does not change ordinary downloads.
|
|
399
361
|
*/
|
|
400
362
|
export function createBrowserModelSource(descriptor, {
|
|
@@ -470,15 +432,7 @@ export function createBrowserModelSource(descriptor, {
|
|
|
470
432
|
});
|
|
471
433
|
}
|
|
472
434
|
|
|
473
|
-
async function open(
|
|
474
|
-
let memberIndex = memberOrOptions;
|
|
475
|
-
if (!Number.isSafeInteger(memberOrOptions)) {
|
|
476
|
-
if (metadata.files.length !== 1) {
|
|
477
|
-
throw new TypeError("A browser model file index is required for a multi-file source.");
|
|
478
|
-
}
|
|
479
|
-
memberIndex = 0;
|
|
480
|
-
options = memberOrOptions ?? {};
|
|
481
|
-
}
|
|
435
|
+
async function open(memberIndex = 0, options = {}) {
|
|
482
436
|
const { signal } = options;
|
|
483
437
|
const download = await request(memberIndex, { signal });
|
|
484
438
|
if (!download.response?.ok) await rejectHttpResponse(download);
|
|
@@ -587,10 +541,6 @@ export function createBrowserModelSource(descriptor, {
|
|
|
587
541
|
descriptor: model,
|
|
588
542
|
open,
|
|
589
543
|
};
|
|
590
|
-
if (metadata.legacy) {
|
|
591
|
-
sourceRecord.name = metadata.files[0].name;
|
|
592
|
-
sourceRecord.immutableUrl = metadata.files[0].url;
|
|
593
|
-
}
|
|
594
544
|
const source = completeValue(sourceRecord);
|
|
595
545
|
BROWSER_MODEL_SOURCES.add(source);
|
|
596
546
|
BROWSER_MODEL_SOURCE_METADATA.set(source, metadata);
|
|
@@ -654,18 +604,14 @@ function injectiveStorageId(id) {
|
|
|
654
604
|
).join("");
|
|
655
605
|
}
|
|
656
606
|
|
|
657
|
-
function storageName(source
|
|
658
|
-
const safeId =
|
|
659
|
-
? source.id.replace(/[^a-z0-9._-]+/giu, "_")
|
|
660
|
-
: `id-${injectiveStorageId(source.id)}`;
|
|
607
|
+
function storageName(source) {
|
|
608
|
+
const safeId = `id-${injectiveStorageId(source.id)}`;
|
|
661
609
|
const models = sourceMetadata(source).files.map((file) => completeValue({
|
|
662
610
|
file,
|
|
663
611
|
name: `${safeId}--${file.name}`,
|
|
664
612
|
}));
|
|
665
613
|
return completeValue({
|
|
666
614
|
models: completeValue(models),
|
|
667
|
-
model: models[0].name,
|
|
668
|
-
manifest: `${safeId}.complete.json`,
|
|
669
615
|
});
|
|
670
616
|
}
|
|
671
617
|
|
|
@@ -973,7 +919,7 @@ export function createDbopfsModelStore({
|
|
|
973
919
|
} = {},
|
|
974
920
|
) {
|
|
975
921
|
const transport = BROWSER_MODEL_SOURCE_TRANSPORTS.get(source);
|
|
976
|
-
const ranges =
|
|
922
|
+
const ranges = rangePlanForInstall(total, signal);
|
|
977
923
|
const partFiles = new Array(ranges.length);
|
|
978
924
|
const pendingRangeIndexes = [];
|
|
979
925
|
for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
|
|
@@ -1161,7 +1107,6 @@ export function createDbopfsModelStore({
|
|
|
1161
1107
|
async function removeNames(names) {
|
|
1162
1108
|
const removed = [];
|
|
1163
1109
|
for (const entry of names.models) removed.push(await removeEntry(entry.name));
|
|
1164
|
-
removed.push(await removeEntry(names.manifest));
|
|
1165
1110
|
return removed.some(Boolean);
|
|
1166
1111
|
}
|
|
1167
1112
|
|
|
@@ -1176,74 +1121,9 @@ export function createDbopfsModelStore({
|
|
|
1176
1121
|
return names;
|
|
1177
1122
|
}
|
|
1178
1123
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
total
|
|
1182
|
-
signal,
|
|
1183
|
-
knownNames = undefined,
|
|
1184
|
-
) {
|
|
1185
|
-
const plans = modelHttpRangePlans(total);
|
|
1186
|
-
const existingNames = knownNames === undefined
|
|
1187
|
-
? await memberRangePartNames(modelName)
|
|
1188
|
-
: knownNames;
|
|
1189
|
-
const available = existingNames === null
|
|
1190
|
-
? null
|
|
1191
|
-
: existingNames instanceof Set
|
|
1192
|
-
? existingNames
|
|
1193
|
-
: new Set(existingNames);
|
|
1194
|
-
if (available?.size === 0) return plans[0];
|
|
1195
|
-
if (plans.length === 1) return plans[0];
|
|
1196
|
-
const completedParts = new Map();
|
|
1197
|
-
|
|
1198
|
-
async function completedPart(range) {
|
|
1199
|
-
throwIfAborted(signal, "install");
|
|
1200
|
-
const partName = rangePartName(modelName, range);
|
|
1201
|
-
if (available && !available.has(partName)) return null;
|
|
1202
|
-
if (completedParts.has(partName)) return completedParts.get(partName);
|
|
1203
|
-
const partFile = await file(partName);
|
|
1204
|
-
const expected = range.end - range.start + 1;
|
|
1205
|
-
const completed = partFile?.size === expected ? partFile : null;
|
|
1206
|
-
completedParts.set(partName, completed);
|
|
1207
|
-
return completed;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
const completedLegacyRanges = new Set();
|
|
1211
|
-
for (const range of plans[1]) {
|
|
1212
|
-
if (await completedPart(range)) completedLegacyRanges.add(range);
|
|
1213
|
-
}
|
|
1214
|
-
const hybrid = completeValue(plans[1].flatMap((range) => (
|
|
1215
|
-
completedLegacyRanges.has(range)
|
|
1216
|
-
? [range]
|
|
1217
|
-
: modelHttpSubranges(range)
|
|
1218
|
-
)));
|
|
1219
|
-
let selected = plans[0];
|
|
1220
|
-
let selectedBytes = 0;
|
|
1221
|
-
let selectedLastModified = 0;
|
|
1222
|
-
for (const ranges of [plans[0], hybrid]) {
|
|
1223
|
-
let completedBytes = 0;
|
|
1224
|
-
let lastModified = 0;
|
|
1225
|
-
for (const range of ranges) {
|
|
1226
|
-
const partFile = await completedPart(range);
|
|
1227
|
-
if (!partFile) continue;
|
|
1228
|
-
completedBytes += range.end - range.start + 1;
|
|
1229
|
-
if (Number.isFinite(partFile.lastModified)) {
|
|
1230
|
-
lastModified = Math.max(lastModified, partFile.lastModified);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
if (
|
|
1234
|
-
completedBytes > selectedBytes
|
|
1235
|
-
|| (
|
|
1236
|
-
completedBytes === selectedBytes
|
|
1237
|
-
&& completedBytes > 0
|
|
1238
|
-
&& lastModified > selectedLastModified
|
|
1239
|
-
)
|
|
1240
|
-
) {
|
|
1241
|
-
selected = ranges;
|
|
1242
|
-
selectedBytes = completedBytes;
|
|
1243
|
-
selectedLastModified = lastModified;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return selected;
|
|
1124
|
+
function rangePlanForInstall(total, signal) {
|
|
1125
|
+
throwIfAborted(signal, "install");
|
|
1126
|
+
return modelHttpRanges(total);
|
|
1247
1127
|
}
|
|
1248
1128
|
|
|
1249
1129
|
async function removeMemberRangeParts(modelName, {
|
|
@@ -1253,14 +1133,7 @@ export function createDbopfsModelStore({
|
|
|
1253
1133
|
const existingNames = await memberRangePartNames(modelName);
|
|
1254
1134
|
let names = existingNames;
|
|
1255
1135
|
if (names === null && Number.isSafeInteger(total) && total > 0) {
|
|
1256
|
-
|
|
1257
|
-
const hybridRanges = plans.length === 1
|
|
1258
|
-
? []
|
|
1259
|
-
: plans[1].flatMap((range) => modelHttpSubranges(range));
|
|
1260
|
-
names = [...new Set([
|
|
1261
|
-
...plans.flat(),
|
|
1262
|
-
...hybridRanges,
|
|
1263
|
-
].map((range) => rangePartName(modelName, range)))];
|
|
1136
|
+
names = modelHttpRanges(total).map((range) => rangePartName(modelName, range));
|
|
1264
1137
|
}
|
|
1265
1138
|
names ??= [];
|
|
1266
1139
|
const removed = [];
|
|
@@ -1356,120 +1229,11 @@ export function createDbopfsModelStore({
|
|
|
1356
1229
|
|
|
1357
1230
|
async function remove(source) {
|
|
1358
1231
|
const names = storageName(source);
|
|
1359
|
-
const legacyNames = storageName(source, { legacy: true });
|
|
1360
|
-
let legacyManifest = null;
|
|
1361
|
-
let removeLegacy = sourceMetadata(source).legacy;
|
|
1362
|
-
try {
|
|
1363
|
-
legacyManifest = await legacyManifestForSource(source, legacyNames);
|
|
1364
|
-
removeLegacy ||= Boolean(legacyManifest);
|
|
1365
|
-
} catch (error) {
|
|
1366
|
-
globalThis.console?.warn?.(
|
|
1367
|
-
"Arcane could not inspect the legacy browser model cache during removal.",
|
|
1368
|
-
error,
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
1232
|
let removed = await removeNames(names);
|
|
1372
1233
|
removed = await removeRangeParts(source, names) || removed;
|
|
1373
|
-
if (removeLegacy) {
|
|
1374
|
-
removed = await removeNames(
|
|
1375
|
-
legacyStorageNames(source, legacyNames, legacyManifest),
|
|
1376
|
-
) || removed;
|
|
1377
|
-
}
|
|
1378
1234
|
return removed;
|
|
1379
1235
|
}
|
|
1380
1236
|
|
|
1381
|
-
async function legacyManifestForSource(source, legacyNames) {
|
|
1382
|
-
const manifestFile = await file(legacyNames.manifest);
|
|
1383
|
-
if (!manifestFile) return null;
|
|
1384
|
-
let manifest;
|
|
1385
|
-
try {
|
|
1386
|
-
manifest = JSON.parse(await manifestFile.text());
|
|
1387
|
-
} catch {
|
|
1388
|
-
return null;
|
|
1389
|
-
}
|
|
1390
|
-
const model = manifest?.model;
|
|
1391
|
-
return manifest?.complete === true && model?.id === source.id ? manifest : null;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
function legacyStorageNames(source, names, manifest) {
|
|
1395
|
-
if (!manifest) return names;
|
|
1396
|
-
const safeId = source.id.replace(/[^a-z0-9._-]+/giu, "_");
|
|
1397
|
-
const storedNames = new Set(names.models.map((entry) => entry.name));
|
|
1398
|
-
const candidates = [];
|
|
1399
|
-
if (Array.isArray(manifest.model?.files)) candidates.push(...manifest.model.files);
|
|
1400
|
-
if (manifest.model && typeof manifest.model === "object") candidates.push(manifest.model);
|
|
1401
|
-
if (Array.isArray(manifest.files)) candidates.push(...manifest.files);
|
|
1402
|
-
for (const candidate of candidates) {
|
|
1403
|
-
if (!candidate || typeof candidate !== "object") continue;
|
|
1404
|
-
if (
|
|
1405
|
-
candidate.name === undefined
|
|
1406
|
-
&& candidate.url === undefined
|
|
1407
|
-
&& candidate.immutableUrl === undefined
|
|
1408
|
-
) continue;
|
|
1409
|
-
try {
|
|
1410
|
-
const url = modelSourceUrl(candidate.url ?? candidate.immutableUrl)
|
|
1411
|
-
?? new URL("https://arcane.invalid/model.gguf");
|
|
1412
|
-
const name = descriptorFileName(candidate, url);
|
|
1413
|
-
storedNames.add(`${safeId}--${name}`);
|
|
1414
|
-
} catch {
|
|
1415
|
-
// Keep cleanup limited to valid filenames attributable to this manifest.
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
return completeValue({
|
|
1419
|
-
...names,
|
|
1420
|
-
models: completeValue([...storedNames].map((name) => completeValue({ name }))),
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
async function legacyNamespaceMatchesSource(source, legacyNames) {
|
|
1425
|
-
const metadata = sourceMetadata(source);
|
|
1426
|
-
if (metadata.legacy) return true;
|
|
1427
|
-
if (metadata.files.length !== 1) return false;
|
|
1428
|
-
const manifest = await legacyManifestForSource(source, legacyNames);
|
|
1429
|
-
if (!manifest) return false;
|
|
1430
|
-
const model = manifest.model;
|
|
1431
|
-
const [member] = metadata.files;
|
|
1432
|
-
if (model.url === member.url) return true;
|
|
1433
|
-
if (model.name === member.name && model.immutableUrl === member.url) return true;
|
|
1434
|
-
return Array.isArray(model.files)
|
|
1435
|
-
&& model.files.length === 1
|
|
1436
|
-
&& model.files[0]?.name === member.name
|
|
1437
|
-
&& model.files[0]?.url === member.url;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
async function legacyCacheForSource(source, signal) {
|
|
1441
|
-
const names = storageName(source, { legacy: true });
|
|
1442
|
-
if (!await legacyNamespaceMatchesSource(source, names)) return null;
|
|
1443
|
-
const files = await storedModelFiles(names, signal);
|
|
1444
|
-
return files ? completeValue({ files, names }) : null;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
async function removeLegacyCacheAfterReplacement(source) {
|
|
1448
|
-
try {
|
|
1449
|
-
const names = storageName(source, { legacy: true });
|
|
1450
|
-
const manifest = await legacyManifestForSource(source, names);
|
|
1451
|
-
if (!sourceMetadata(source).legacy && !manifest) return;
|
|
1452
|
-
await removeNames(legacyStorageNames(source, names, manifest));
|
|
1453
|
-
} catch (error) {
|
|
1454
|
-
globalThis.console?.warn?.(
|
|
1455
|
-
"Arcane could not remove the superseded legacy browser model cache.",
|
|
1456
|
-
error,
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
async function removeIncompleteReplacement(source, names) {
|
|
1462
|
-
try {
|
|
1463
|
-
await removeNames(names);
|
|
1464
|
-
await removeRangeParts(source, names);
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
globalThis.console?.warn?.(
|
|
1467
|
-
"Arcane could not remove an incomplete duplicate browser model cache.",
|
|
1468
|
-
error,
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
1237
|
async function storagePolicy({ cached = false } = {}) {
|
|
1474
1238
|
return completeValue({
|
|
1475
1239
|
compatibility: "compatible",
|
|
@@ -1495,11 +1259,6 @@ export function createDbopfsModelStore({
|
|
|
1495
1259
|
return modelFiles;
|
|
1496
1260
|
}
|
|
1497
1261
|
|
|
1498
|
-
async function storedModelFiles(names, signal) {
|
|
1499
|
-
const modelFiles = await availableModelFiles(names, signal);
|
|
1500
|
-
return modelFiles.every(Boolean) ? modelFiles : null;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
1262
|
async function storedRangeCandidateForPlan(
|
|
1504
1263
|
modelName,
|
|
1505
1264
|
ranges,
|
|
@@ -1538,12 +1297,7 @@ export function createDbopfsModelStore({
|
|
|
1538
1297
|
signal,
|
|
1539
1298
|
availableNames = null,
|
|
1540
1299
|
) {
|
|
1541
|
-
const ranges =
|
|
1542
|
-
modelName,
|
|
1543
|
-
total,
|
|
1544
|
-
signal,
|
|
1545
|
-
availableNames,
|
|
1546
|
-
);
|
|
1300
|
+
const ranges = rangePlanForInstall(total, signal);
|
|
1547
1301
|
return storedRangeCandidateForPlan(
|
|
1548
1302
|
modelName,
|
|
1549
1303
|
ranges,
|
|
@@ -1624,20 +1378,10 @@ export function createDbopfsModelStore({
|
|
|
1624
1378
|
signal,
|
|
1625
1379
|
} = {}) {
|
|
1626
1380
|
const names = storageName(source);
|
|
1627
|
-
|
|
1628
|
-
if (modelFiles.every(Boolean)) {
|
|
1629
|
-
await removeLegacyCacheAfterReplacement(source);
|
|
1630
|
-
} else {
|
|
1631
|
-
const legacyCache = await legacyCacheForSource(source, signal);
|
|
1632
|
-
if (legacyCache) {
|
|
1633
|
-
await removeIncompleteReplacement(source, names);
|
|
1634
|
-
modelFiles = legacyCache.files;
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1381
|
+
const modelFiles = await availableResumableModelFiles(source, names, signal);
|
|
1637
1382
|
if (!modelFiles.every(Boolean)) return null;
|
|
1638
1383
|
return completeValue({
|
|
1639
1384
|
files: completeValue(modelFiles),
|
|
1640
|
-
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
1641
1385
|
});
|
|
1642
1386
|
}
|
|
1643
1387
|
|
|
@@ -1744,13 +1488,11 @@ export function createDbopfsModelStore({
|
|
|
1744
1488
|
}
|
|
1745
1489
|
}
|
|
1746
1490
|
if (failure !== null) throw failure;
|
|
1747
|
-
await removeLegacyCacheAfterReplacement(source);
|
|
1748
1491
|
progress.finish();
|
|
1749
1492
|
if (failure !== null) throw failure;
|
|
1750
1493
|
throwIfAborted(downloadSignal, "install");
|
|
1751
1494
|
return completeValue({
|
|
1752
1495
|
files: completeValue(modelFiles),
|
|
1753
|
-
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
1754
1496
|
});
|
|
1755
1497
|
} catch (error) {
|
|
1756
1498
|
retainFailure(error);
|
|
@@ -3011,13 +2753,12 @@ function capabilityPolicy(
|
|
|
3011
2753
|
});
|
|
3012
2754
|
}
|
|
3013
2755
|
|
|
3014
|
-
function providerModelSources(
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
throw new TypeError("createBrowserWasmLlmProvider requires a nonempty sources array or legacy source.");
|
|
2756
|
+
function providerModelSources(sources) {
|
|
2757
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
2758
|
+
throw new TypeError("createBrowserWasmLlmProvider requires a nonempty sources array.");
|
|
3018
2759
|
}
|
|
3019
2760
|
const ids = new Set();
|
|
3020
|
-
for (const candidate of
|
|
2761
|
+
for (const candidate of sources) {
|
|
3021
2762
|
if (!BROWSER_MODEL_SOURCES.has(candidate)) {
|
|
3022
2763
|
throw new TypeError("Every browser-WASM model source must come from createBrowserModelSource().");
|
|
3023
2764
|
}
|
|
@@ -3026,27 +2767,20 @@ function providerModelSources(source, sources) {
|
|
|
3026
2767
|
}
|
|
3027
2768
|
ids.add(candidate.id);
|
|
3028
2769
|
}
|
|
3029
|
-
if (source !== undefined && !BROWSER_MODEL_SOURCES.has(source)) {
|
|
3030
|
-
throw new TypeError("The legacy default source must come from createBrowserModelSource().");
|
|
3031
|
-
}
|
|
3032
|
-
if (source !== undefined && !list.includes(source)) {
|
|
3033
|
-
throw new TypeError("The legacy default source must be one member of sources.");
|
|
3034
|
-
}
|
|
3035
2770
|
return completeValue({
|
|
3036
|
-
sources: completeValue(
|
|
3037
|
-
defaultSource:
|
|
2771
|
+
sources: completeValue(sources.slice()),
|
|
2772
|
+
defaultSource: sources[0],
|
|
3038
2773
|
});
|
|
3039
2774
|
}
|
|
3040
2775
|
|
|
3041
2776
|
export function createBrowserWasmLlmProvider({
|
|
3042
|
-
source,
|
|
3043
2777
|
sources,
|
|
3044
2778
|
store,
|
|
3045
2779
|
loadDefaults = {},
|
|
3046
2780
|
security,
|
|
3047
2781
|
logger = console,
|
|
3048
2782
|
} = {}) {
|
|
3049
|
-
const configuredModels = providerModelSources(
|
|
2783
|
+
const configuredModels = providerModelSources(sources);
|
|
3050
2784
|
const modelSources = configuredModels.sources;
|
|
3051
2785
|
const defaultSource = configuredModels.defaultSource;
|
|
3052
2786
|
if (!DBOPFS_MODEL_STORES.has(store)) {
|
|
@@ -3751,5 +3485,3 @@ export function adaptV1LlmProvider(provider) {
|
|
|
3751
3485
|
V1_LLM_PROVIDER_ADAPTERS.set(provider, adapted);
|
|
3752
3486
|
return adapted;
|
|
3753
3487
|
}
|
|
3754
|
-
|
|
3755
|
-
export { MODEL_MANIFEST_SCHEMA };
|