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.
Files changed (46) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +32 -27
  3. package/browser-runtime/ai/browser-speech-artifacts.mjs +23 -1683
  4. package/browser-runtime/ai/browser-speech-providers.mjs +353 -143
  5. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +95 -363
  6. package/browser-runtime/ai/speech-worker-client.mjs +51 -14
  7. package/browser-runtime/ai/speech-worker-runtime.mjs +45 -1072
  8. package/browser-runtime/dependencies/strong-type/package.json +22 -23
  9. package/browser-runtime/event-manager.mjs +27 -41
  10. package/package.json +2 -3
  11. package/runtime/arcane/components/chat.html +1 -1
  12. package/runtime/arcane/components/speech.html +8 -38
  13. package/runtime/arcane/components/voice-transcription.html +1 -1
  14. package/runtime/arcane/css/theme.css +1 -1
  15. package/runtime/arcane/entities/Chat.js +12 -7
  16. package/runtime/arcane/modules/AI.js +506 -391
  17. package/runtime/arcane/modules/AIPreferenceTuple.js +1 -1
  18. package/runtime/arcane/modules/AIProviderRuntime.js +129 -68
  19. package/runtime/arcane/modules/AIRuntimeState.js +2 -18
  20. package/runtime/arcane/modules/BrowserTestSuite.js +2 -5
  21. package/runtime/arcane/modules/CalculatorEngine.js +0 -4
  22. package/runtime/arcane/modules/ChatRecords.js +169 -1
  23. package/runtime/arcane/modules/CommunicationHub.js +0 -19
  24. package/runtime/arcane/modules/ConfiguredAIChatSession.js +28 -25
  25. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +4 -5
  26. package/runtime/arcane/modules/DocumentLexicalSearch.js +1 -1
  27. package/runtime/arcane/modules/Errors.js +6 -19
  28. package/runtime/arcane/modules/Mail.js +1 -2
  29. package/runtime/arcane/modules/MailTransport.mjs +1 -3
  30. package/runtime/arcane/modules/OllamaModelIdentifier.js +1 -1
  31. package/runtime/arcane/modules/PersistentAIChatSession.js +5 -5
  32. package/runtime/arcane/modules/ScreenCapture.js +2 -4
  33. package/runtime/arcane/modules/SpeechPlayback.js +0 -27
  34. package/runtime/arcane/modules/WaitForComponent.js +0 -1
  35. package/src/app-descriptor.mjs +1 -1
  36. package/src/doctor.mjs +1 -3
  37. package/src/event-manager.mjs +27 -41
  38. package/src/import-map.mjs +1 -1
  39. package/src/index.mjs +2 -9
  40. package/src/installed-sdk-runtime.mjs +1 -11
  41. package/src/mail-api.mjs +0 -1
  42. package/src/native-provider-loader.mjs +0 -9
  43. package/src/scaffold.mjs +9 -21
  44. package/src/toolchain.mjs +5 -28
  45. package/src/workspace-runtime.mjs +0 -4
  46. 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, fallbackName = null) {
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, { fallbackName = null } = {}) {
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
- value.url !== undefined
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 ?? value.immutableUrl);
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, fallbackName),
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
- const hasFiles = value.files !== undefined;
270
- const hasLegacyFile = ["url", "immutableUrl", "name"]
271
- .some((field) => value[field] !== undefined);
272
- if (hasFiles && hasLegacyFile) {
273
- throw new TypeError("Browser model files[] is mutually exclusive with legacy one-file fields.");
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
- let descriptor;
307
- if (legacy) {
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 isChromeBrowser() {
328
- const userAgent = String(globalThis.navigator?.userAgent ?? "");
329
- return /\b(?:Chrome|Chromium)\//u.test(userAgent)
330
- && !/\b(?:Edg|OPR)\//u.test(userAgent);
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
- function isLowerPowerIntelAdapter(adapter) {
334
- const identity = [
335
- adapter.vendor,
336
- adapter.architecture,
337
- adapter.name,
338
- adapter.description,
339
- ].filter(Boolean).join(" ");
340
- if (adapter?.vendorId !== INTEL_VENDOR_ID && !/\bintel\b/iu.test(identity)) return false;
341
- return /(?:intel|integrated|xe-lp)/iu.test(identity);
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 notifyChromeHighPerformanceGpu(adapter) {
345
- if (
346
- highPerformanceGpuNoticeShown
347
- || !isChromeBrowser()
348
- || !isLowerPowerIntelAdapter(adapter)
349
- ) return;
350
- highPerformanceGpuNoticeShown = true;
351
- try {
352
- globalThis.open?.(CHROME_HIGH_PERFORMANCE_GPU_FLAG_URL, "_blank", "noopener,noreferrer");
353
- } catch {
354
- // Chrome may reject internal-page navigation from web content.
355
- }
356
- const adapterName = adapter.description || adapter.name
357
- || [adapter.vendor, adapter.architecture].filter(Boolean).join(" ")
358
- || "a lower-power Intel adapter";
359
- globalThis.alert?.(
360
- `Arcane selected the lower-power WebGPU adapter: ${adapterName}.\n\n`
361
- + "Enable “Force High Performance GPU” in the Chrome flags window. "
362
- + "Then completely close every Chrome window and reopen Chrome before loading the model again.\n\n"
363
- + `If the flags window did not open, paste ${CHROME_HIGH_PERFORMANCE_GPU_FLAG_URL} into Chrome.`,
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
- notifyChromeHighPerformanceGpu(webgpu.adapter);
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(memberOrOptions = 0, options = {}) {
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, { legacy = false } = {}) {
658
- const safeId = legacy
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 = await rangePlanForInstall(name, total, signal);
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
- async function rangePlanForInstall(
1180
- modelName,
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
- const plans = modelHttpRangePlans(total);
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 = await rangePlanForInstall(
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
- let modelFiles = await availableResumableModelFiles(source, names, signal);
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(source, sources) {
3015
- const list = sources === undefined ? [source] : sources;
3016
- if (!Array.isArray(list) || list.length === 0) {
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 list) {
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(list.slice()),
3037
- defaultSource: source ?? list[0],
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(source, sources);
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 };