arcane-os 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +27 -67
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +109 -1558
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -4,17 +4,15 @@ import {
|
|
|
4
4
|
normalizeModelSecurity,
|
|
5
5
|
normalizeArcaneAIError,
|
|
6
6
|
resolveModelSecurity,
|
|
7
|
-
sameModelSecurity,
|
|
8
7
|
} from "./model-controller.mjs";
|
|
9
8
|
import { createPackagedWllamaRuntime } from "./browser-wllama-runtime.mjs";
|
|
10
|
-
import { createStreamingSha256 } from "./internal/sha256.mjs";
|
|
11
9
|
import { arcaneEvents } from "../event-manager.mjs";
|
|
12
10
|
|
|
11
|
+
const completeValue = (value) => value;
|
|
12
|
+
|
|
13
|
+
// Compatibility export only. Ordinary model caching no longer creates or
|
|
14
|
+
// requires completion manifests, receipts, or byte identities.
|
|
13
15
|
const MODEL_MANIFEST_SCHEMA = "arcane.ai.browser-wasm.model.v4";
|
|
14
|
-
const SINGLE_MODEL_MANIFEST_SCHEMA = "arcane.ai.browser-wasm.model.v3";
|
|
15
|
-
const LEGACY_MODEL_MANIFEST_SCHEMA = "arcane.ai.browser-wasm.model.v2";
|
|
16
|
-
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
17
|
-
const MUTABLE_PATH_PATTERN = /\/(?:resolve\/)?(?:main|master|latest)(?:\/|$)/iu;
|
|
18
16
|
const BROWSER_MODEL_SOURCES = new WeakSet();
|
|
19
17
|
const BROWSER_MODEL_SOURCE_METADATA = new WeakMap();
|
|
20
18
|
const MODEL_DESCRIPTOR_METADATA = new WeakMap();
|
|
@@ -28,7 +26,6 @@ const CHROME_HIGH_PERFORMANCE_GPU_FLAG_URL =
|
|
|
28
26
|
"chrome://flags/#force-high-performance-gpu";
|
|
29
27
|
const INTEL_VENDOR_ID = 0x8086;
|
|
30
28
|
const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
|
|
31
|
-
const WLLAMA_MAX_FILE_BYTES = 2_000_000_000;
|
|
32
29
|
let highPerformanceGpuNoticeShown = false;
|
|
33
30
|
|
|
34
31
|
function fail(code, message, cause) {
|
|
@@ -52,20 +49,13 @@ function normalizationSignal(error, signal) {
|
|
|
52
49
|
return error?.code === "ARCANE_AI_WORKER_TERMINATION_UNCONFIRMED" ? null : signal;
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
function
|
|
52
|
+
function modelSourceUrl(value) {
|
|
56
53
|
let url;
|
|
57
54
|
try {
|
|
58
55
|
url = new URL(value);
|
|
59
56
|
} catch {
|
|
60
57
|
return null;
|
|
61
58
|
}
|
|
62
|
-
if (
|
|
63
|
-
url.protocol !== "https:"
|
|
64
|
-
|| url.username
|
|
65
|
-
|| url.password
|
|
66
|
-
|| url.hash
|
|
67
|
-
|| MUTABLE_PATH_PATTERN.test(url.pathname)
|
|
68
|
-
) return null;
|
|
69
59
|
return url;
|
|
70
60
|
}
|
|
71
61
|
|
|
@@ -135,28 +125,15 @@ function descriptorFile(value, { fallbackName = null } = {}) {
|
|
|
135
125
|
) {
|
|
136
126
|
throw new TypeError("Browser model file url and legacy immutableUrl must match when both are provided.");
|
|
137
127
|
}
|
|
138
|
-
const url =
|
|
128
|
+
const url = modelSourceUrl(value.url ?? value.immutableUrl);
|
|
139
129
|
if (!url) {
|
|
140
|
-
throw new TypeError("Browser model file url must be
|
|
130
|
+
throw new TypeError("Browser model file url must be a valid absolute URL.");
|
|
141
131
|
}
|
|
142
132
|
const file = {
|
|
143
133
|
name: descriptorFileName(value, url, fallbackName),
|
|
144
134
|
url: url.href,
|
|
145
135
|
};
|
|
146
|
-
|
|
147
|
-
if (!Number.isSafeInteger(value.bytes) || value.bytes < 1) {
|
|
148
|
-
throw new TypeError("Browser model file bytes must be a positive safe integer when provided.");
|
|
149
|
-
}
|
|
150
|
-
file.bytes = value.bytes;
|
|
151
|
-
}
|
|
152
|
-
if (value.sha256 !== undefined) {
|
|
153
|
-
const sha256 = requiredText(value.sha256, "file sha256").toLowerCase();
|
|
154
|
-
if (!SHA256_PATTERN.test(sha256)) {
|
|
155
|
-
throw new TypeError("Browser model file sha256 must be exactly 64 hexadecimal characters when provided.");
|
|
156
|
-
}
|
|
157
|
-
file.sha256 = sha256;
|
|
158
|
-
}
|
|
159
|
-
return Object.freeze(file);
|
|
136
|
+
return completeValue(file);
|
|
160
137
|
}
|
|
161
138
|
|
|
162
139
|
function modelDescriptor(value) {
|
|
@@ -165,7 +142,7 @@ function modelDescriptor(value) {
|
|
|
165
142
|
}
|
|
166
143
|
const id = modelIdText(value.id);
|
|
167
144
|
const hasFiles = value.files !== undefined;
|
|
168
|
-
const hasLegacyFile = ["url", "immutableUrl", "name"
|
|
145
|
+
const hasLegacyFile = ["url", "immutableUrl", "name"]
|
|
169
146
|
.some((field) => value[field] !== undefined);
|
|
170
147
|
if (hasFiles && hasLegacyFile) {
|
|
171
148
|
throw new TypeError("Browser model files[] is mutually exclusive with legacy one-file fields.");
|
|
@@ -195,18 +172,20 @@ function modelDescriptor(value) {
|
|
|
195
172
|
names.add(nameKey);
|
|
196
173
|
urls.add(file.url);
|
|
197
174
|
}
|
|
198
|
-
files =
|
|
175
|
+
files = completeValue(files);
|
|
176
|
+
const publicFiles = completeValue(files.map((file) => completeValue({
|
|
177
|
+
name: file.name,
|
|
178
|
+
url: file.url,
|
|
179
|
+
})));
|
|
199
180
|
let descriptor;
|
|
200
181
|
if (legacy) {
|
|
201
182
|
const [file] = files;
|
|
202
183
|
descriptor = { id, url: file.url };
|
|
203
|
-
if (file.bytes !== undefined) descriptor.bytes = file.bytes;
|
|
204
|
-
if (file.sha256 !== undefined) descriptor.sha256 = file.sha256;
|
|
205
184
|
} else {
|
|
206
|
-
descriptor = { id, files };
|
|
185
|
+
descriptor = { id, files: publicFiles };
|
|
207
186
|
}
|
|
208
|
-
descriptor =
|
|
209
|
-
MODEL_DESCRIPTOR_METADATA.set(descriptor,
|
|
187
|
+
descriptor = completeValue(descriptor);
|
|
188
|
+
MODEL_DESCRIPTOR_METADATA.set(descriptor, completeValue({ files, legacy }));
|
|
210
189
|
return descriptor;
|
|
211
190
|
}
|
|
212
191
|
|
|
@@ -263,7 +242,7 @@ function emitWebgpuAdapterSelection(source, runtime) {
|
|
|
263
242
|
const webgpu = evidence?.webgpu;
|
|
264
243
|
if (webgpu?.observed !== true || !webgpu.adapter) return;
|
|
265
244
|
try {
|
|
266
|
-
arcaneEvents.instrument(WEBGPU_ADAPTER_SELECTED_EVENT,
|
|
245
|
+
arcaneEvents.instrument(WEBGPU_ADAPTER_SELECTED_EVENT, completeValue({
|
|
267
246
|
protocol: WEBGPU_ADAPTER_SELECTION_PROTOCOL,
|
|
268
247
|
providerId: "arcane-browser-wasm-wllama",
|
|
269
248
|
modelId: source.id,
|
|
@@ -272,7 +251,7 @@ function emitWebgpuAdapterSelection(source, runtime) {
|
|
|
272
251
|
offload: webgpu.offload ?? null,
|
|
273
252
|
buffers: webgpu.buffers ?? null,
|
|
274
253
|
queue: webgpu.queue ?? null,
|
|
275
|
-
}),
|
|
254
|
+
}), completeValue({
|
|
276
255
|
source: "sdk:ai/browser-wasm",
|
|
277
256
|
category: "capability",
|
|
278
257
|
}));
|
|
@@ -286,20 +265,10 @@ function sourceMetadata(source) {
|
|
|
286
265
|
?? MODEL_DESCRIPTOR_METADATA.get(publicDescriptor(source));
|
|
287
266
|
}
|
|
288
267
|
|
|
289
|
-
function manifestModelIdentity(source) {
|
|
290
|
-
return Object.freeze({
|
|
291
|
-
id: source.id,
|
|
292
|
-
files: Object.freeze(sourceMetadata(source).files.map((file) => Object.freeze({
|
|
293
|
-
name: file.name,
|
|
294
|
-
url: file.url,
|
|
295
|
-
}))),
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
268
|
/**
|
|
300
|
-
* Creates a browser download authority for one caller-supplied
|
|
301
|
-
*
|
|
302
|
-
*
|
|
269
|
+
* Creates a browser download authority for one caller-supplied model file set.
|
|
270
|
+
* Legacy one-file descriptors normalize to one ordered member.
|
|
271
|
+
* Security is an intent-only seam and does not change ordinary downloads.
|
|
303
272
|
*/
|
|
304
273
|
export function createBrowserModelSource(descriptor, {
|
|
305
274
|
fetchImpl = null,
|
|
@@ -331,10 +300,7 @@ export function createBrowserModelSource(descriptor, {
|
|
|
331
300
|
try {
|
|
332
301
|
response = await fetchFunction(member.url, {
|
|
333
302
|
cache: "no-store",
|
|
334
|
-
credentials: "omit",
|
|
335
|
-
mode: "cors",
|
|
336
303
|
redirect: "follow",
|
|
337
|
-
referrerPolicy: "no-referrer",
|
|
338
304
|
signal,
|
|
339
305
|
});
|
|
340
306
|
} catch (error) {
|
|
@@ -354,22 +320,14 @@ export function createBrowserModelSource(descriptor, {
|
|
|
354
320
|
} catch {
|
|
355
321
|
finalUrl = null;
|
|
356
322
|
}
|
|
357
|
-
|
|
358
|
-
await response.body?.cancel?.().catch(() => undefined);
|
|
359
|
-
throw fail("ARCANE_AI_MODEL_REDIRECT_BLOCKED", "The model response left HTTPS.");
|
|
360
|
-
}
|
|
323
|
+
finalUrl ??= new URL(member.url);
|
|
361
324
|
if (!response.body || typeof response.body.getReader !== "function") {
|
|
362
325
|
throw fail("ARCANE_AI_MODEL_SOURCE_INVALID", "The model response did not provide a byte stream.");
|
|
363
326
|
}
|
|
364
|
-
|
|
365
|
-
const reported = header === null || header === undefined || header === ""
|
|
366
|
-
? null
|
|
367
|
-
: Number(header);
|
|
368
|
-
return Object.freeze({
|
|
327
|
+
return completeValue({
|
|
369
328
|
body: response.body,
|
|
370
329
|
requestedUrl: member.url,
|
|
371
330
|
finalUrl: finalUrl.href,
|
|
372
|
-
reportedBytes: Number.isSafeInteger(reported) && reported >= 0 ? reported : null,
|
|
373
331
|
cancel: (reason) => response.body.cancel(reason),
|
|
374
332
|
});
|
|
375
333
|
}
|
|
@@ -386,7 +344,7 @@ export function createBrowserModelSource(descriptor, {
|
|
|
386
344
|
immutableUrl: { value: metadata.files[0].url, enumerable: false },
|
|
387
345
|
});
|
|
388
346
|
}
|
|
389
|
-
const source =
|
|
347
|
+
const source = completeValue(sourceRecord);
|
|
390
348
|
BROWSER_MODEL_SOURCES.add(source);
|
|
391
349
|
BROWSER_MODEL_SOURCE_METADATA.set(source, metadata);
|
|
392
350
|
return source;
|
|
@@ -452,213 +410,25 @@ function storageName(source, { legacy = false } = {}) {
|
|
|
452
410
|
const safeId = legacy
|
|
453
411
|
? source.id.replace(/[^a-z0-9._-]+/giu, "_")
|
|
454
412
|
: `id-${injectiveStorageId(source.id)}`;
|
|
455
|
-
const models = sourceMetadata(source).files.map((file) =>
|
|
413
|
+
const models = sourceMetadata(source).files.map((file) => completeValue({
|
|
456
414
|
file,
|
|
457
415
|
name: `${safeId}--${file.name}`,
|
|
458
416
|
}));
|
|
459
|
-
return
|
|
460
|
-
models:
|
|
417
|
+
return completeValue({
|
|
418
|
+
models: completeValue(models),
|
|
461
419
|
model: models[0].name,
|
|
462
420
|
manifest: `${safeId}.complete.json`,
|
|
463
421
|
});
|
|
464
422
|
}
|
|
465
423
|
|
|
466
|
-
function manifestFor(source, files) {
|
|
467
|
-
const observedBytes = files.reduce((total, file) => total + file.observedBytes, 0);
|
|
468
|
-
return Object.freeze({
|
|
469
|
-
schema: MODEL_MANIFEST_SCHEMA,
|
|
470
|
-
complete: true,
|
|
471
|
-
model: manifestModelIdentity(source),
|
|
472
|
-
files: Object.freeze(files.map((file) => Object.freeze({
|
|
473
|
-
name: file.name,
|
|
474
|
-
finalUrl: file.finalUrl,
|
|
475
|
-
observedBytes: file.observedBytes,
|
|
476
|
-
}))),
|
|
477
|
-
observedBytes,
|
|
478
|
-
completedAt: new Date().toISOString(),
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function manifestByteLength(manifest) {
|
|
483
|
-
return new TextEncoder().encode(`${JSON.stringify(manifest)}\n`).byteLength;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function projectedManifestByteLength(source) {
|
|
487
|
-
return manifestByteLength(manifestFor(
|
|
488
|
-
source,
|
|
489
|
-
sourceMetadata(source).files.map((file) => ({
|
|
490
|
-
name: file.name,
|
|
491
|
-
finalUrl: file.url,
|
|
492
|
-
observedBytes: file.bytes,
|
|
493
|
-
})),
|
|
494
|
-
));
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function manifestKind(manifest, source) {
|
|
498
|
-
const model = manifest?.model;
|
|
499
|
-
const members = sourceMetadata(source).files;
|
|
500
|
-
if (
|
|
501
|
-
manifest?.schema === MODEL_MANIFEST_SCHEMA
|
|
502
|
-
&& manifest?.complete === true
|
|
503
|
-
&& model?.id === source.id
|
|
504
|
-
&& Array.isArray(model?.files)
|
|
505
|
-
&& model.files.length === members.length
|
|
506
|
-
&& model.files.every((file, index) => (
|
|
507
|
-
file?.name === members[index].name
|
|
508
|
-
&& file?.url === members[index].url
|
|
509
|
-
))
|
|
510
|
-
&& Array.isArray(manifest.files)
|
|
511
|
-
&& manifest.files.length === members.length
|
|
512
|
-
&& manifest.files.every((file, index) => (
|
|
513
|
-
file?.name === members[index].name
|
|
514
|
-
&& Number.isSafeInteger(file?.observedBytes)
|
|
515
|
-
&& file.observedBytes >= 0
|
|
516
|
-
&& immutableHttpsUrl(file?.finalUrl)
|
|
517
|
-
))
|
|
518
|
-
&& Number.isSafeInteger(manifest.observedBytes)
|
|
519
|
-
&& manifest.observedBytes >= 0
|
|
520
|
-
&& manifest.files.reduce((total, file) => total + file.observedBytes, 0)
|
|
521
|
-
=== manifest.observedBytes
|
|
522
|
-
) return "set";
|
|
523
|
-
if (!sourceMetadata(source).legacy || members.length !== 1) return null;
|
|
524
|
-
const [member] = members;
|
|
525
|
-
if (
|
|
526
|
-
manifest?.schema === SINGLE_MODEL_MANIFEST_SCHEMA
|
|
527
|
-
&& manifest?.complete === true
|
|
528
|
-
&& model?.id === source.id
|
|
529
|
-
&& model?.url === member.url
|
|
530
|
-
&& Number.isSafeInteger(manifest.observedBytes)
|
|
531
|
-
&& manifest.observedBytes >= 0
|
|
532
|
-
) return "single";
|
|
533
|
-
if (
|
|
534
|
-
manifest?.schema === LEGACY_MODEL_MANIFEST_SCHEMA
|
|
535
|
-
&& manifest?.complete === true
|
|
536
|
-
&& model?.id === source.id
|
|
537
|
-
&& model?.name === member.name
|
|
538
|
-
&& model?.immutableUrl === member.url
|
|
539
|
-
) return "legacy";
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function progress(source, phase, loaded, total = null, memberIndex = 0, memberLoaded = loaded) {
|
|
544
|
-
const members = sourceMetadata(source).files;
|
|
545
|
-
const member = members[memberIndex] ?? members[0];
|
|
546
|
-
return Object.freeze({
|
|
547
|
-
modelId: source.id,
|
|
548
|
-
phase,
|
|
549
|
-
loaded,
|
|
550
|
-
total,
|
|
551
|
-
percent: Number.isSafeInteger(total) && total > 0 ? (loaded / total) * 100 : null,
|
|
552
|
-
file: Object.freeze({
|
|
553
|
-
index: memberIndex,
|
|
554
|
-
count: members.length,
|
|
555
|
-
name: member.name,
|
|
556
|
-
loaded: memberLoaded,
|
|
557
|
-
total: member.bytes ?? null,
|
|
558
|
-
}),
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
|
|
562
424
|
function securitySnapshot(security) {
|
|
563
|
-
return
|
|
564
|
-
secure: security.secure,
|
|
565
|
-
checks: Object.freeze({
|
|
566
|
-
byteLength: security.checks.byteLength,
|
|
567
|
-
sha256: security.checks.sha256,
|
|
568
|
-
}),
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
function assertDescriptorChecks(source, security) {
|
|
573
|
-
const files = sourceMetadata(source).files;
|
|
574
|
-
if (security.checks.byteLength && files.some((file) => file.bytes === undefined)) {
|
|
575
|
-
throw fail(
|
|
576
|
-
"ARCANE_AI_MODEL_SOURCE_INVALID",
|
|
577
|
-
"Browser model bytes is required for every file when the byteLength check is enabled.",
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
if (security.checks.sha256 && files.some((file) => file.sha256 === undefined)) {
|
|
581
|
-
throw fail(
|
|
582
|
-
"ARCANE_AI_MODEL_SOURCE_INVALID",
|
|
583
|
-
"Browser model sha256 is required for every file when the sha256 check is enabled.",
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function knownModelBytes(source) {
|
|
589
|
-
const files = sourceMetadata(source).files;
|
|
590
|
-
if (files.some((file) => file.bytes === undefined)) return null;
|
|
591
|
-
const total = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
592
|
-
return Number.isSafeInteger(total) ? total : null;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function oversizedModelFile(source) {
|
|
596
|
-
return sourceMetadata(source).files.find(
|
|
597
|
-
(file) => file.bytes !== undefined && file.bytes > WLLAMA_MAX_FILE_BYTES,
|
|
598
|
-
) ?? null;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function integritySnapshot(security, source, {
|
|
602
|
-
observedBytes = null,
|
|
603
|
-
byteLength = security.checks.byteLength ? "pending" : "unchecked",
|
|
604
|
-
sha256 = security.checks.sha256 ? "pending" : "unchecked",
|
|
605
|
-
actualSha256 = null,
|
|
606
|
-
files = null,
|
|
607
|
-
} = {}) {
|
|
608
|
-
const enabledStates = [];
|
|
609
|
-
if (security.checks.byteLength) enabledStates.push(byteLength);
|
|
610
|
-
if (security.checks.sha256) enabledStates.push(sha256);
|
|
611
|
-
const state = enabledStates.length === 0
|
|
612
|
-
? "unchecked"
|
|
613
|
-
: enabledStates.every((value) => value === "verified")
|
|
614
|
-
? "verified"
|
|
615
|
-
: enabledStates.some((value) => value === "failed")
|
|
616
|
-
? "failed"
|
|
617
|
-
: "pending";
|
|
618
|
-
const members = sourceMetadata(source).files;
|
|
619
|
-
const fileStates = members.map((member, index) => {
|
|
620
|
-
const evidence = files?.[index] ?? {};
|
|
621
|
-
return Object.freeze({
|
|
622
|
-
name: member.name,
|
|
623
|
-
observedBytes: evidence.observedBytes ?? null,
|
|
624
|
-
byteLength: Object.freeze({
|
|
625
|
-
enabled: security.checks.byteLength,
|
|
626
|
-
state: evidence.byteLength ?? byteLength,
|
|
627
|
-
expected: member.bytes ?? null,
|
|
628
|
-
observed: evidence.observedBytes ?? null,
|
|
629
|
-
}),
|
|
630
|
-
sha256: Object.freeze({
|
|
631
|
-
enabled: security.checks.sha256,
|
|
632
|
-
state: evidence.sha256 ?? sha256,
|
|
633
|
-
expected: member.sha256 ?? null,
|
|
634
|
-
actual: evidence.actualSha256 ?? (members.length === 1 ? actualSha256 : null),
|
|
635
|
-
}),
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
return Object.freeze({
|
|
639
|
-
state,
|
|
640
|
-
observedBytes,
|
|
641
|
-
byteLength: Object.freeze({
|
|
642
|
-
enabled: security.checks.byteLength,
|
|
643
|
-
state: byteLength,
|
|
644
|
-
expected: knownModelBytes(source),
|
|
645
|
-
observed: observedBytes,
|
|
646
|
-
}),
|
|
647
|
-
sha256: Object.freeze({
|
|
648
|
-
enabled: security.checks.sha256,
|
|
649
|
-
state: sha256,
|
|
650
|
-
expected: members.length === 1 ? members[0].sha256 ?? null : null,
|
|
651
|
-
actual: members.length === 1 ? actualSha256 : null,
|
|
652
|
-
}),
|
|
653
|
-
files: Object.freeze(fileStates),
|
|
654
|
-
});
|
|
425
|
+
return security?.secure === true ? { secure: true } : undefined;
|
|
655
426
|
}
|
|
656
427
|
|
|
657
428
|
/**
|
|
658
429
|
* Adapts an existing Arcane DBOPFS singleton without rebinding or changing any
|
|
659
|
-
* of its public methods.
|
|
660
|
-
*
|
|
661
|
-
* effective check is enabled.
|
|
430
|
+
* of its public methods. Ordinary cache reuse is based on the selected model's
|
|
431
|
+
* expected files being present; no receipt or byte identity is created.
|
|
662
432
|
*/
|
|
663
433
|
export function createDbopfsModelStore({
|
|
664
434
|
dbopfs,
|
|
@@ -709,20 +479,17 @@ export function createDbopfsModelStore({
|
|
|
709
479
|
}
|
|
710
480
|
}
|
|
711
481
|
|
|
712
|
-
async function write(name, body, { signal
|
|
482
|
+
async function write(name, body, { signal } = {}) {
|
|
713
483
|
const directory = await table();
|
|
714
484
|
const handle = await directory.getFileHandle(name, { create: true });
|
|
715
485
|
const writable = await handle.createWritable();
|
|
716
|
-
let written = 0;
|
|
717
486
|
try {
|
|
718
487
|
for await (const chunk of byteChunks(body, signal)) {
|
|
719
488
|
await writable.write(chunk);
|
|
720
|
-
written += chunk.byteLength;
|
|
721
|
-
await onChunk?.(chunk, written);
|
|
722
489
|
}
|
|
723
490
|
throwIfAborted(signal, "install");
|
|
724
491
|
await writable.close();
|
|
725
|
-
return
|
|
492
|
+
return undefined;
|
|
726
493
|
} catch (error) {
|
|
727
494
|
await writable.abort?.(error).catch(() => undefined);
|
|
728
495
|
await directory.removeEntry(name).catch(() => undefined);
|
|
@@ -730,17 +497,6 @@ export function createDbopfsModelStore({
|
|
|
730
497
|
}
|
|
731
498
|
}
|
|
732
499
|
|
|
733
|
-
async function readManifest(name, { removeInvalid = true } = {}) {
|
|
734
|
-
const manifestFile = await file(name);
|
|
735
|
-
if (!manifestFile) return null;
|
|
736
|
-
try {
|
|
737
|
-
return JSON.parse(await manifestFile.text());
|
|
738
|
-
} catch {
|
|
739
|
-
if (removeInvalid) await removeEntry(name);
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
500
|
async function removeNames(names) {
|
|
745
501
|
const removed = [await removeEntry(names.manifest)];
|
|
746
502
|
for (const entry of names.models) removed.push(await removeEntry(entry.name));
|
|
@@ -750,272 +506,56 @@ export function createDbopfsModelStore({
|
|
|
750
506
|
async function remove(source) {
|
|
751
507
|
let removed = await removeNames(storageName(source));
|
|
752
508
|
if (sourceMetadata(source).legacy) {
|
|
753
|
-
|
|
754
|
-
const legacyManifest = await readManifest(legacyNames.manifest, { removeInvalid: false });
|
|
755
|
-
if (manifestKind(legacyManifest, source)) {
|
|
756
|
-
removed = await removeNames(legacyNames) || removed;
|
|
757
|
-
}
|
|
509
|
+
removed = await removeNames(storageName(source, { legacy: true })) || removed;
|
|
758
510
|
}
|
|
759
511
|
return removed;
|
|
760
512
|
}
|
|
761
513
|
|
|
762
|
-
async function
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
phase,
|
|
771
|
-
completedBytes = 0,
|
|
772
|
-
totalBytes = null,
|
|
773
|
-
}) {
|
|
774
|
-
const digest = createStreamingSha256();
|
|
775
|
-
let hashed = 0;
|
|
776
|
-
for await (const chunk of byteChunks(modelFile, signal)) {
|
|
777
|
-
digest.update(chunk);
|
|
778
|
-
hashed += chunk.byteLength;
|
|
779
|
-
onProgress?.(progress(
|
|
780
|
-
source,
|
|
781
|
-
phase,
|
|
782
|
-
completedBytes + hashed,
|
|
783
|
-
totalBytes,
|
|
784
|
-
memberIndex,
|
|
785
|
-
hashed,
|
|
786
|
-
));
|
|
787
|
-
}
|
|
788
|
-
return Object.freeze({ hashed, sha256: digest.digestHex() });
|
|
514
|
+
async function storagePolicy({ cached = false } = {}) {
|
|
515
|
+
return completeValue({
|
|
516
|
+
compatibility: "compatible",
|
|
517
|
+
code: cached
|
|
518
|
+
? "ARCANE_AI_MODEL_CACHE_AVAILABLE"
|
|
519
|
+
: "ARCANE_AI_MODEL_STORAGE_AVAILABLE",
|
|
520
|
+
measured: false,
|
|
521
|
+
});
|
|
789
522
|
}
|
|
790
523
|
|
|
791
|
-
async function
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
return
|
|
797
|
-
|
|
798
|
-
code: "ARCANE_AI_MODEL_CACHE_COMPLETE",
|
|
799
|
-
requiredBytes,
|
|
800
|
-
payloadBytes,
|
|
801
|
-
manifestBytes,
|
|
802
|
-
quotaBytes: null,
|
|
803
|
-
usageBytes: null,
|
|
804
|
-
availableBytes: null,
|
|
805
|
-
measured: false,
|
|
806
|
-
admitted: true,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
if (security?.checks?.byteLength !== true) {
|
|
810
|
-
return Object.freeze({
|
|
811
|
-
compatibility: "unknown",
|
|
812
|
-
code: "ARCANE_AI_MODEL_STORAGE_REQUIREMENT_UNBOUNDED",
|
|
813
|
-
requiredBytes: null,
|
|
814
|
-
payloadBytes: null,
|
|
815
|
-
manifestBytes: null,
|
|
816
|
-
quotaBytes: null,
|
|
817
|
-
usageBytes: null,
|
|
818
|
-
availableBytes: null,
|
|
819
|
-
measured: false,
|
|
820
|
-
admitted: false,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
const payloadBytes = knownModelBytes(source);
|
|
824
|
-
const manifestBytes = payloadBytes === null ? null : projectedManifestByteLength(source);
|
|
825
|
-
const requiredBytes = payloadBytes === null || !Number.isSafeInteger(payloadBytes + manifestBytes)
|
|
826
|
-
? null
|
|
827
|
-
: payloadBytes + manifestBytes;
|
|
828
|
-
if (requiredBytes === null) {
|
|
829
|
-
return Object.freeze({
|
|
830
|
-
compatibility: "unknown",
|
|
831
|
-
code: "ARCANE_AI_MODEL_STORAGE_REQUIREMENT_UNKNOWN",
|
|
832
|
-
requiredBytes: null,
|
|
833
|
-
payloadBytes,
|
|
834
|
-
manifestBytes,
|
|
835
|
-
quotaBytes: null,
|
|
836
|
-
usageBytes: null,
|
|
837
|
-
availableBytes: null,
|
|
838
|
-
measured: false,
|
|
839
|
-
admitted: false,
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
const estimator = estimateStorage
|
|
843
|
-
?? globalThis.navigator?.storage?.estimate?.bind(globalThis.navigator.storage);
|
|
844
|
-
if (typeof estimator !== "function") {
|
|
845
|
-
return Object.freeze({
|
|
846
|
-
compatibility: "unknown",
|
|
847
|
-
code: "ARCANE_AI_STORAGE_ESTIMATE_UNAVAILABLE",
|
|
848
|
-
requiredBytes,
|
|
849
|
-
payloadBytes,
|
|
850
|
-
manifestBytes,
|
|
851
|
-
quotaBytes: null,
|
|
852
|
-
usageBytes: null,
|
|
853
|
-
availableBytes: null,
|
|
854
|
-
measured: false,
|
|
855
|
-
admitted: false,
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
let estimate;
|
|
859
|
-
try {
|
|
860
|
-
estimate = await estimator();
|
|
861
|
-
} catch {
|
|
862
|
-
return Object.freeze({
|
|
863
|
-
compatibility: "unknown",
|
|
864
|
-
code: "ARCANE_AI_STORAGE_ESTIMATE_FAILED",
|
|
865
|
-
requiredBytes,
|
|
866
|
-
payloadBytes,
|
|
867
|
-
manifestBytes,
|
|
868
|
-
quotaBytes: null,
|
|
869
|
-
usageBytes: null,
|
|
870
|
-
availableBytes: null,
|
|
871
|
-
measured: true,
|
|
872
|
-
admitted: false,
|
|
873
|
-
});
|
|
524
|
+
async function storedModelFiles(names, signal) {
|
|
525
|
+
const modelFiles = [];
|
|
526
|
+
for (const entry of names.models) {
|
|
527
|
+
throwIfAborted(signal, "install");
|
|
528
|
+
const modelFile = await file(entry.name);
|
|
529
|
+
if (!modelFile) return null;
|
|
530
|
+
modelFiles.push(modelFile);
|
|
874
531
|
}
|
|
875
|
-
|
|
876
|
-
? estimate.quota
|
|
877
|
-
: null;
|
|
878
|
-
const usageBytes = Number.isSafeInteger(estimate?.usage) && estimate.usage >= 0
|
|
879
|
-
? estimate.usage
|
|
880
|
-
: null;
|
|
881
|
-
const availableBytes = quotaBytes !== null && usageBytes !== null && quotaBytes >= usageBytes
|
|
882
|
-
? quotaBytes - usageBytes
|
|
883
|
-
: null;
|
|
884
|
-
const incompatible = availableBytes !== null && requiredBytes > availableBytes;
|
|
885
|
-
return Object.freeze({
|
|
886
|
-
compatibility: incompatible ? "incompatible" : availableBytes === null ? "unknown" : "compatible",
|
|
887
|
-
code: incompatible
|
|
888
|
-
? "ARCANE_AI_STORAGE_CAPACITY_INSUFFICIENT"
|
|
889
|
-
: availableBytes === null
|
|
890
|
-
? "ARCANE_AI_STORAGE_ESTIMATE_INVALID"
|
|
891
|
-
: "ARCANE_AI_STORAGE_CAPACITY_AVAILABLE",
|
|
892
|
-
requiredBytes,
|
|
893
|
-
payloadBytes,
|
|
894
|
-
manifestBytes,
|
|
895
|
-
quotaBytes,
|
|
896
|
-
usageBytes,
|
|
897
|
-
availableBytes,
|
|
898
|
-
measured: true,
|
|
899
|
-
admitted: false,
|
|
900
|
-
});
|
|
532
|
+
return modelFiles;
|
|
901
533
|
}
|
|
902
534
|
|
|
903
535
|
async function openCached(source, {
|
|
904
536
|
signal,
|
|
905
|
-
onProgress,
|
|
906
|
-
security = resolveModelSecurity(),
|
|
907
537
|
} = {}) {
|
|
908
|
-
assertDescriptorChecks(source, security);
|
|
909
538
|
let names = storageName(source);
|
|
910
|
-
let
|
|
911
|
-
|
|
912
|
-
|
|
539
|
+
let modelFiles = await storedModelFiles(names, signal);
|
|
540
|
+
if (!modelFiles && sourceMetadata(source).legacy) {
|
|
541
|
+
await removeNames(names);
|
|
913
542
|
const legacyNames = storageName(source, { legacy: true });
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
if (legacyKind) {
|
|
543
|
+
const legacyFiles = await storedModelFiles(legacyNames, signal);
|
|
544
|
+
if (legacyFiles) {
|
|
917
545
|
names = legacyNames;
|
|
918
|
-
|
|
919
|
-
|
|
546
|
+
modelFiles = legacyFiles;
|
|
547
|
+
} else {
|
|
548
|
+
await removeNames(legacyNames);
|
|
920
549
|
}
|
|
921
550
|
}
|
|
922
|
-
if (!
|
|
923
|
-
|
|
924
|
-
await remove(source);
|
|
551
|
+
if (!modelFiles) {
|
|
552
|
+
await removeNames(names);
|
|
925
553
|
return null;
|
|
926
554
|
}
|
|
927
|
-
const members = sourceMetadata(source).files;
|
|
928
|
-
const modelFiles = [];
|
|
929
|
-
const fileEvidence = [];
|
|
930
|
-
let observedBytes = 0;
|
|
931
555
|
try {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const modelFile = await file(names.models[index].name);
|
|
935
|
-
if (!modelFile) {
|
|
936
|
-
await removeNames(names);
|
|
937
|
-
return null;
|
|
938
|
-
}
|
|
939
|
-
if (modelFile.size > WLLAMA_MAX_FILE_BYTES) {
|
|
940
|
-
await removeNames(names);
|
|
941
|
-
throw fail(
|
|
942
|
-
"ARCANE_AI_MODEL_SHARD_TOO_LARGE",
|
|
943
|
-
`Cached model file ${member.name} exceeds Wllama's ${WLLAMA_MAX_FILE_BYTES}-byte boundary.`,
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
if (kind === "set" && manifest.files[index].observedBytes !== modelFile.size) {
|
|
947
|
-
await removeNames(names);
|
|
948
|
-
return null;
|
|
949
|
-
}
|
|
950
|
-
if (security.checks.byteLength && modelFile.size !== member.bytes) {
|
|
951
|
-
await removeNames(names);
|
|
952
|
-
return null;
|
|
953
|
-
}
|
|
954
|
-
modelFiles.push(modelFile);
|
|
955
|
-
fileEvidence.push({
|
|
956
|
-
observedBytes: modelFile.size,
|
|
957
|
-
byteLength: security.checks.byteLength ? "verified" : "unchecked",
|
|
958
|
-
sha256: security.checks.sha256 ? "pending" : "unchecked",
|
|
959
|
-
actualSha256: null,
|
|
960
|
-
});
|
|
961
|
-
observedBytes += modelFile.size;
|
|
962
|
-
}
|
|
963
|
-
if ((kind === "set" || kind === "single") && manifest.observedBytes !== observedBytes) {
|
|
964
|
-
await removeNames(names);
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
let completedBytes = 0;
|
|
968
|
-
for (let index = 0; index < modelFiles.length; index += 1) {
|
|
969
|
-
const modelFile = modelFiles[index];
|
|
970
|
-
if (security.checks.sha256) {
|
|
971
|
-
const verification = await verifySha256(source, index, modelFile, {
|
|
972
|
-
signal,
|
|
973
|
-
onProgress,
|
|
974
|
-
phase: "verify-cache",
|
|
975
|
-
completedBytes,
|
|
976
|
-
totalBytes: observedBytes,
|
|
977
|
-
});
|
|
978
|
-
fileEvidence[index].actualSha256 = verification.sha256;
|
|
979
|
-
if (
|
|
980
|
-
verification.hashed !== modelFile.size
|
|
981
|
-
|| verification.sha256 !== members[index].sha256
|
|
982
|
-
) {
|
|
983
|
-
await removeNames(names);
|
|
984
|
-
return null;
|
|
985
|
-
}
|
|
986
|
-
fileEvidence[index].sha256 = "verified";
|
|
987
|
-
} else {
|
|
988
|
-
onProgress?.(progress(
|
|
989
|
-
source,
|
|
990
|
-
"cache",
|
|
991
|
-
completedBytes + modelFile.size,
|
|
992
|
-
observedBytes,
|
|
993
|
-
index,
|
|
994
|
-
modelFile.size,
|
|
995
|
-
));
|
|
996
|
-
}
|
|
997
|
-
completedBytes += modelFile.size;
|
|
998
|
-
}
|
|
999
|
-
let completion = manifest;
|
|
1000
|
-
if (kind !== "set") {
|
|
1001
|
-
completion = manifestFor(source, [{
|
|
1002
|
-
name: members[0].name,
|
|
1003
|
-
finalUrl: manifest.finalUrl ?? members[0].url,
|
|
1004
|
-
observedBytes,
|
|
1005
|
-
}]);
|
|
1006
|
-
}
|
|
1007
|
-
return Object.freeze({
|
|
1008
|
-
files: Object.freeze(modelFiles),
|
|
556
|
+
return completeValue({
|
|
557
|
+
files: completeValue(modelFiles),
|
|
1009
558
|
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
1010
|
-
manifest: completion,
|
|
1011
|
-
observedBytes,
|
|
1012
|
-
integrity: integritySnapshot(security, source, {
|
|
1013
|
-
observedBytes,
|
|
1014
|
-
byteLength: security.checks.byteLength ? "verified" : "unchecked",
|
|
1015
|
-
sha256: security.checks.sha256 ? "verified" : "unchecked",
|
|
1016
|
-
actualSha256: fileEvidence[0]?.actualSha256 ?? null,
|
|
1017
|
-
files: fileEvidence,
|
|
1018
|
-
}),
|
|
1019
559
|
});
|
|
1020
560
|
} catch (error) {
|
|
1021
561
|
if (!signal?.aborted) await removeNames(names);
|
|
@@ -1023,121 +563,33 @@ export function createDbopfsModelStore({
|
|
|
1023
563
|
}
|
|
1024
564
|
}
|
|
1025
565
|
|
|
1026
|
-
async function
|
|
1027
|
-
const security = resolveModelSecurity({ load: { secure: true } });
|
|
1028
|
-
return openCached(source, { signal, onProgress, security });
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function install(source, { signal, onProgress, security: configuredSecurity } = {}) {
|
|
1032
|
-
const security = resolveModelSecurity({ load: configuredSecurity });
|
|
1033
|
-
assertDescriptorChecks(source, security);
|
|
566
|
+
async function install(source, { signal } = {}) {
|
|
1034
567
|
const names = storageName(source);
|
|
1035
568
|
const members = sourceMetadata(source).files;
|
|
1036
569
|
await remove(source);
|
|
1037
570
|
const modelFiles = [];
|
|
1038
|
-
const manifestFiles = [];
|
|
1039
|
-
const fileEvidence = [];
|
|
1040
|
-
const expectedTotal = knownModelBytes(source);
|
|
1041
|
-
let observedBytes = 0;
|
|
1042
571
|
try {
|
|
1043
572
|
for (let index = 0; index < members.length; index += 1) {
|
|
1044
573
|
const member = members[index];
|
|
1045
574
|
const opened = await source.open(index, { signal });
|
|
1046
575
|
try {
|
|
1047
|
-
|
|
1048
|
-
throw fail(
|
|
1049
|
-
"ARCANE_AI_MODEL_SHARD_TOO_LARGE",
|
|
1050
|
-
`Model file ${member.name} exceeds Wllama's ${WLLAMA_MAX_FILE_BYTES}-byte boundary.`,
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
|
-
if (
|
|
1054
|
-
security.checks.byteLength
|
|
1055
|
-
&& opened.reportedBytes !== null
|
|
1056
|
-
&& opened.reportedBytes !== member.bytes
|
|
1057
|
-
) {
|
|
1058
|
-
throw fail(
|
|
1059
|
-
"ARCANE_AI_MODEL_SIZE_MISMATCH",
|
|
1060
|
-
"A model response Content-Length did not match its expected byte length.",
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
const downloadDigest = security.checks.sha256 ? createStreamingSha256() : null;
|
|
1064
|
-
const written = await write(names.models[index].name, opened.body, {
|
|
1065
|
-
signal,
|
|
1066
|
-
async onChunk(chunk, loaded) {
|
|
1067
|
-
downloadDigest?.update(chunk);
|
|
1068
|
-
if (loaded > WLLAMA_MAX_FILE_BYTES) {
|
|
1069
|
-
throw fail(
|
|
1070
|
-
"ARCANE_AI_MODEL_SHARD_TOO_LARGE",
|
|
1071
|
-
`Model file ${member.name} exceeds Wllama's ${WLLAMA_MAX_FILE_BYTES}-byte boundary.`,
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
if (security.checks.byteLength && loaded > member.bytes) {
|
|
1075
|
-
throw fail("ARCANE_AI_MODEL_SIZE_MISMATCH", "Downloaded model file exceeded its declared size.");
|
|
1076
|
-
}
|
|
1077
|
-
onProgress?.(progress(
|
|
1078
|
-
source,
|
|
1079
|
-
"download",
|
|
1080
|
-
observedBytes + loaded,
|
|
1081
|
-
expectedTotal,
|
|
1082
|
-
index,
|
|
1083
|
-
loaded,
|
|
1084
|
-
));
|
|
1085
|
-
},
|
|
1086
|
-
});
|
|
1087
|
-
if (security.checks.byteLength && written !== member.bytes) {
|
|
1088
|
-
throw fail(
|
|
1089
|
-
"ARCANE_AI_MODEL_SIZE_MISMATCH",
|
|
1090
|
-
"Downloaded model file bytes did not match the caller-supplied expected byte length.",
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
576
|
+
await write(names.models[index].name, opened.body, { signal });
|
|
1093
577
|
const modelFile = await file(names.models[index].name);
|
|
1094
|
-
if (!modelFile
|
|
578
|
+
if (!modelFile) {
|
|
1095
579
|
throw fail(
|
|
1096
580
|
"ARCANE_AI_MODEL_CACHE_REJECTED",
|
|
1097
|
-
"A stored model file
|
|
581
|
+
"A stored model file could not be reopened.",
|
|
1098
582
|
);
|
|
1099
583
|
}
|
|
1100
|
-
const evidence = {
|
|
1101
|
-
observedBytes: written,
|
|
1102
|
-
byteLength: security.checks.byteLength ? "verified" : "unchecked",
|
|
1103
|
-
sha256: security.checks.sha256 ? "pending" : "unchecked",
|
|
1104
|
-
actualSha256: null,
|
|
1105
|
-
};
|
|
1106
|
-
if (security.checks.sha256) {
|
|
1107
|
-
evidence.actualSha256 = downloadDigest.digestHex();
|
|
1108
|
-
if (evidence.actualSha256 !== member.sha256) {
|
|
1109
|
-
throw fail(
|
|
1110
|
-
"ARCANE_AI_MODEL_DIGEST_MISMATCH",
|
|
1111
|
-
"Downloaded model file bytes did not match the caller-supplied SHA-256 value.",
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
evidence.sha256 = "verified";
|
|
1115
|
-
}
|
|
1116
584
|
modelFiles.push(modelFile);
|
|
1117
|
-
fileEvidence.push(evidence);
|
|
1118
|
-
manifestFiles.push({ name: member.name, finalUrl: opened.finalUrl, observedBytes: written });
|
|
1119
|
-
observedBytes += written;
|
|
1120
585
|
} catch (error) {
|
|
1121
586
|
await opened.cancel?.(error).catch(() => undefined);
|
|
1122
587
|
throw error;
|
|
1123
588
|
}
|
|
1124
589
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const manifest = manifestFor(source, manifestFiles);
|
|
1128
|
-
await writeManifest(names.manifest, manifest, signal);
|
|
1129
|
-
return Object.freeze({
|
|
1130
|
-
files: Object.freeze(modelFiles),
|
|
590
|
+
return completeValue({
|
|
591
|
+
files: completeValue(modelFiles),
|
|
1131
592
|
file: modelFiles.length === 1 ? modelFiles[0] : null,
|
|
1132
|
-
manifest,
|
|
1133
|
-
observedBytes,
|
|
1134
|
-
integrity: integritySnapshot(security, source, {
|
|
1135
|
-
observedBytes,
|
|
1136
|
-
byteLength: security.checks.byteLength ? "verified" : "unchecked",
|
|
1137
|
-
sha256: security.checks.sha256 ? "verified" : "unchecked",
|
|
1138
|
-
actualSha256: fileEvidence[0]?.actualSha256 ?? null,
|
|
1139
|
-
files: fileEvidence,
|
|
1140
|
-
}),
|
|
1141
593
|
});
|
|
1142
594
|
} catch (error) {
|
|
1143
595
|
await remove(source).catch(() => undefined);
|
|
@@ -1147,42 +599,31 @@ export function createDbopfsModelStore({
|
|
|
1147
599
|
|
|
1148
600
|
async function ensure(source, {
|
|
1149
601
|
signal,
|
|
1150
|
-
onProgress,
|
|
1151
602
|
onCapabilityPolicy,
|
|
1152
603
|
offline = false,
|
|
1153
|
-
security: configuredSecurity,
|
|
1154
604
|
} = {}) {
|
|
1155
|
-
const
|
|
1156
|
-
assertDescriptorChecks(source, security);
|
|
1157
|
-
const cached = await openCached(source, { signal, onProgress, security });
|
|
605
|
+
const cached = await openCached(source, { signal });
|
|
1158
606
|
if (cached) {
|
|
1159
|
-
const storage = await storagePolicy(
|
|
607
|
+
const storage = await storagePolicy({ cached: true });
|
|
1160
608
|
onCapabilityPolicy?.(storage);
|
|
1161
|
-
return
|
|
609
|
+
return completeValue({ ...cached, cache: "cached", storage });
|
|
1162
610
|
}
|
|
1163
611
|
if (offline) {
|
|
1164
|
-
throw fail("ARCANE_AI_MODEL_OFFLINE_MISS", "No
|
|
612
|
+
throw fail("ARCANE_AI_MODEL_OFFLINE_MISS", "No cached offline model is available.");
|
|
1165
613
|
}
|
|
1166
|
-
const storage = await storagePolicy(
|
|
614
|
+
const storage = await storagePolicy();
|
|
1167
615
|
onCapabilityPolicy?.(storage);
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
storage.code,
|
|
1171
|
-
"Available browser storage is smaller than the model file set and completion manifest.",
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
const installed = await install(source, { signal, onProgress, security });
|
|
1175
|
-
const admittedStorage = await storagePolicy(source, { cached: installed, security });
|
|
616
|
+
const installed = await install(source, { signal });
|
|
617
|
+
const admittedStorage = await storagePolicy({ cached: true });
|
|
1176
618
|
onCapabilityPolicy?.(admittedStorage);
|
|
1177
|
-
return
|
|
619
|
+
return completeValue({ ...installed, cache: "installed", storage: admittedStorage });
|
|
1178
620
|
}
|
|
1179
621
|
|
|
1180
|
-
const store =
|
|
622
|
+
const store = completeValue({
|
|
1181
623
|
kind: "arcane-dbopfs-model-store",
|
|
1182
624
|
tableName,
|
|
1183
625
|
adapter: dbopfs,
|
|
1184
626
|
ready: () => table().then(() => undefined),
|
|
1185
|
-
openVerified,
|
|
1186
627
|
install,
|
|
1187
628
|
ensure,
|
|
1188
629
|
remove,
|
|
@@ -1196,7 +637,7 @@ function linkAbortSignal(externalSignal) {
|
|
|
1196
637
|
const forward = () => controller.abort(externalSignal.reason);
|
|
1197
638
|
if (externalSignal?.aborted) forward();
|
|
1198
639
|
else externalSignal?.addEventListener?.("abort", forward, { once: true });
|
|
1199
|
-
return
|
|
640
|
+
return completeValue({
|
|
1200
641
|
controller,
|
|
1201
642
|
release() {
|
|
1202
643
|
externalSignal?.removeEventListener?.("abort", forward);
|
|
@@ -1300,7 +741,7 @@ function createSerialRequestQueue(onDepth) {
|
|
|
1300
741
|
return ready;
|
|
1301
742
|
}
|
|
1302
743
|
|
|
1303
|
-
return
|
|
744
|
+
return completeValue({
|
|
1304
745
|
schedule,
|
|
1305
746
|
openStream,
|
|
1306
747
|
idle: () => tail,
|
|
@@ -1312,19 +753,139 @@ function responseFormat(structuredOutput) {
|
|
|
1312
753
|
return undefined;
|
|
1313
754
|
}
|
|
1314
755
|
if (structuredOutput === true || structuredOutput === "json") {
|
|
1315
|
-
return
|
|
756
|
+
return completeValue({ type: "json_object" });
|
|
1316
757
|
}
|
|
1317
758
|
if (typeof structuredOutput !== "object" || Array.isArray(structuredOutput)) {
|
|
1318
759
|
throw new TypeError("structuredOutput must be false, true, \"json\", or a JSON Schema object.");
|
|
1319
760
|
}
|
|
1320
|
-
return
|
|
761
|
+
return completeValue({
|
|
1321
762
|
type: "json_schema",
|
|
1322
|
-
json_schema:
|
|
763
|
+
json_schema: completeValue({ name: "arcane_response", strict: true, schema: structuredOutput }),
|
|
1323
764
|
});
|
|
1324
765
|
}
|
|
1325
766
|
|
|
767
|
+
function plainStructuralRecord(value) {
|
|
768
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
769
|
+
const prototype = Object.getPrototypeOf(value);
|
|
770
|
+
return prototype === Object.prototype || prototype === null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function validateToolMessageSchemas(value) {
|
|
774
|
+
if (value === undefined) return;
|
|
775
|
+
if (!Array.isArray(value)) throw new TypeError("tools must be an array.");
|
|
776
|
+
for (const [index, tool] of value.entries()) {
|
|
777
|
+
const parameters = tool?.function?.parameters;
|
|
778
|
+
const messageSchema = parameters?.properties?.message;
|
|
779
|
+
if (
|
|
780
|
+
!plainStructuralRecord(tool)
|
|
781
|
+
|| tool.type !== "function"
|
|
782
|
+
|| !plainStructuralRecord(tool.function)
|
|
783
|
+
|| !plainStructuralRecord(parameters)
|
|
784
|
+
|| parameters.type !== "object"
|
|
785
|
+
|| !plainStructuralRecord(parameters.properties)
|
|
786
|
+
|| !plainStructuralRecord(messageSchema)
|
|
787
|
+
|| messageSchema.type !== "string"
|
|
788
|
+
|| !Number.isInteger(messageSchema.minLength)
|
|
789
|
+
|| messageSchema.minLength < 1
|
|
790
|
+
|| !Array.isArray(parameters.required)
|
|
791
|
+
|| !parameters.required.includes("message")
|
|
792
|
+
) {
|
|
793
|
+
throw fail(
|
|
794
|
+
"ARCANE_AI_TOOL_MESSAGE_REQUIRED",
|
|
795
|
+
`tools[${String(index)}] must require a nonempty string parameters.properties.message.`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function validateRequestMessages(messages) {
|
|
802
|
+
let pendingToolCallId = null;
|
|
803
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
804
|
+
if (!plainStructuralRecord(message)) {
|
|
805
|
+
throw new TypeError(`messages[${String(messageIndex)}] must be a plain object.`);
|
|
806
|
+
}
|
|
807
|
+
const hasToolCalls = Object.hasOwn(message, "tool_calls");
|
|
808
|
+
const calls = validateToolCalls(message, `messages[${String(messageIndex)}]`);
|
|
809
|
+
let openedToolCall = false;
|
|
810
|
+
if (hasToolCalls) {
|
|
811
|
+
if (message?.role !== "assistant") {
|
|
812
|
+
throw fail(
|
|
813
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
814
|
+
`messages[${String(messageIndex)}].tool_calls is supported only for assistant messages.`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (pendingToolCallId !== null && calls.length) {
|
|
818
|
+
throw fail(
|
|
819
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
820
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (calls.length) {
|
|
824
|
+
pendingToolCallId = calls[0].id;
|
|
825
|
+
openedToolCall = true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (message?.role === "tool") {
|
|
829
|
+
if (typeof message.content !== "string" || !message.content.trim()) {
|
|
830
|
+
throw fail(
|
|
831
|
+
"ARCANE_AI_INVALID_TOOL_MESSAGE",
|
|
832
|
+
`messages[${String(messageIndex)}] must contain a nonblank user-facing tool result.`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (
|
|
836
|
+
pendingToolCallId === null
|
|
837
|
+
|| typeof message.tool_call_id !== "string"
|
|
838
|
+
|| message.tool_call_id !== pendingToolCallId
|
|
839
|
+
) {
|
|
840
|
+
throw fail(
|
|
841
|
+
"ARCANE_AI_INVALID_TOOL_MESSAGE",
|
|
842
|
+
`messages[${String(messageIndex)}] does not settle the pending structural tool call.`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
pendingToolCallId = null;
|
|
846
|
+
} else {
|
|
847
|
+
if (Object.hasOwn(message, "tool_call_id")) {
|
|
848
|
+
throw fail(
|
|
849
|
+
"ARCANE_AI_INVALID_TOOL_MESSAGE",
|
|
850
|
+
`messages[${String(messageIndex)}].tool_call_id is valid only for a tool result.`,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (pendingToolCallId !== null && !openedToolCall) {
|
|
854
|
+
throw fail(
|
|
855
|
+
"ARCANE_AI_TOOL_RESULT_REQUIRED",
|
|
856
|
+
`messages[${String(messageIndex)}] precedes the pending structural tool result.`,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (pendingToolCallId !== null) {
|
|
862
|
+
throw fail(
|
|
863
|
+
"ARCANE_AI_TOOL_RESULT_REQUIRED",
|
|
864
|
+
"The pending structural tool call must be settled before requesting another response.",
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function validateStructuralRequest(request) {
|
|
870
|
+
if (!plainStructuralRecord(request)) {
|
|
871
|
+
throw new TypeError("The browser-WASM LLM request must be a plain object.");
|
|
872
|
+
}
|
|
873
|
+
if (!Array.isArray(request.messages)) throw new TypeError("messages must be an array.");
|
|
874
|
+
validateRequestMessages(request.messages);
|
|
875
|
+
validateToolMessageSchemas(request.tools);
|
|
876
|
+
const parallelValues = [request.parallelToolCalls, request.parallel_tool_calls];
|
|
877
|
+
if (parallelValues.some(function enablesParallelBrowserWasmTools(value) {
|
|
878
|
+
return value !== undefined && value !== false;
|
|
879
|
+
})) {
|
|
880
|
+
throw fail(
|
|
881
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
882
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
1326
887
|
function completionOptions(request, abortSignal, stream) {
|
|
1327
|
-
|
|
888
|
+
validateStructuralRequest(request);
|
|
1328
889
|
const options = {
|
|
1329
890
|
messages: request.messages,
|
|
1330
891
|
stream,
|
|
@@ -1339,8 +900,11 @@ function completionOptions(request, abortSignal, stream) {
|
|
|
1339
900
|
["minP", "min_p"],
|
|
1340
901
|
["min_p", "min_p"],
|
|
1341
902
|
["repeatPenalty", "penalty_repeat"],
|
|
903
|
+
["repeat", "penalty_repeat"],
|
|
904
|
+
["repeat_penalty", "penalty_repeat"],
|
|
1342
905
|
["penalty_repeat", "penalty_repeat"],
|
|
1343
906
|
["maxTokens", "max_tokens"],
|
|
907
|
+
["maxOutputTokens", "max_tokens"],
|
|
1344
908
|
["max_tokens", "max_tokens"],
|
|
1345
909
|
["seed", "seed"],
|
|
1346
910
|
["stop", "stop"],
|
|
@@ -1348,6 +912,12 @@ function completionOptions(request, abortSignal, stream) {
|
|
|
1348
912
|
for (const [source, target] of copy) {
|
|
1349
913
|
if (request[source] !== undefined) options[target] = request[source];
|
|
1350
914
|
}
|
|
915
|
+
if (request.templateOptions !== undefined) {
|
|
916
|
+
if (!request.templateOptions || typeof request.templateOptions !== "object" || Array.isArray(request.templateOptions)) {
|
|
917
|
+
throw new TypeError("templateOptions must be a plain object when provided.");
|
|
918
|
+
}
|
|
919
|
+
options.chat_template_kwargs = { ...request.templateOptions };
|
|
920
|
+
}
|
|
1351
921
|
if (request.tools !== undefined) {
|
|
1352
922
|
if (!Array.isArray(request.tools)) throw new TypeError("tools must be an array.");
|
|
1353
923
|
options.tools = request.tools;
|
|
@@ -1356,51 +926,207 @@ function completionOptions(request, abortSignal, stream) {
|
|
|
1356
926
|
if (request.tool_choice !== undefined) options.tool_choice = request.tool_choice;
|
|
1357
927
|
if (request.parallelToolCalls !== undefined) options.parallel_tool_calls = request.parallelToolCalls;
|
|
1358
928
|
if (request.parallel_tool_calls !== undefined) options.parallel_tool_calls = request.parallel_tool_calls;
|
|
929
|
+
if (
|
|
930
|
+
request.tools?.length
|
|
931
|
+
&& request.parallelToolCalls === undefined
|
|
932
|
+
&& request.parallel_tool_calls === undefined
|
|
933
|
+
) options.parallel_tool_calls = false;
|
|
1359
934
|
const format = responseFormat(request.structuredOutput);
|
|
1360
935
|
if (format) options.response_format = format;
|
|
1361
936
|
return options;
|
|
1362
937
|
}
|
|
1363
938
|
|
|
1364
|
-
function validateToolCalls(message) {
|
|
1365
|
-
if (message
|
|
1366
|
-
|
|
1367
|
-
|
|
939
|
+
function validateToolCalls(message, location = "The model response") {
|
|
940
|
+
if (!plainStructuralRecord(message)) {
|
|
941
|
+
throw fail("ARCANE_AI_INVALID_PROVIDER_RESULT", `${location} is not a plain assistant message.`);
|
|
942
|
+
}
|
|
943
|
+
if (
|
|
944
|
+
Object.hasOwn(message, "toolCalls")
|
|
945
|
+
|| Object.hasOwn(message, "function_call")
|
|
946
|
+
|| Object.hasOwn(message, "functionCall")
|
|
947
|
+
) {
|
|
948
|
+
throw fail("ARCANE_AI_TOOL_CALL_INVALID", `${location} contains a noncanonical structural tool-call field.`);
|
|
949
|
+
}
|
|
950
|
+
if (!Object.hasOwn(message, "tool_calls")) return [];
|
|
951
|
+
const descriptor = Object.getOwnPropertyDescriptor(message, "tool_calls");
|
|
952
|
+
if (!descriptor || !Object.hasOwn(descriptor, "value") || !Array.isArray(descriptor.value)) {
|
|
953
|
+
throw fail("ARCANE_AI_TOOL_CALL_INVALID", `${location} contains malformed tool calls.`);
|
|
954
|
+
}
|
|
955
|
+
const calls = descriptor.value;
|
|
956
|
+
if (calls.length > 1) {
|
|
957
|
+
throw fail(
|
|
958
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
959
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
960
|
+
);
|
|
1368
961
|
}
|
|
1369
962
|
const ids = new Set();
|
|
1370
|
-
for (const call of
|
|
963
|
+
for (const call of calls) {
|
|
1371
964
|
if (
|
|
1372
|
-
|
|
1373
|
-
||
|
|
965
|
+
!plainStructuralRecord(call)
|
|
966
|
+
|| typeof call.id !== "string"
|
|
967
|
+
|| !call.id.trim()
|
|
1374
968
|
|| ids.has(call.id)
|
|
1375
969
|
|| call.type !== "function"
|
|
1376
|
-
||
|
|
1377
|
-
||
|
|
1378
|
-
||
|
|
970
|
+
|| !plainStructuralRecord(call.function)
|
|
971
|
+
|| typeof call.function.name !== "string"
|
|
972
|
+
|| !call.function.name.trim()
|
|
973
|
+
|| typeof call.function.arguments !== "string"
|
|
1379
974
|
) {
|
|
1380
|
-
throw fail("
|
|
975
|
+
throw fail("ARCANE_AI_TOOL_CALL_INVALID", `${location} contains malformed tool calls.`);
|
|
976
|
+
}
|
|
977
|
+
let argumentsRecord;
|
|
978
|
+
try {
|
|
979
|
+
argumentsRecord = JSON.parse(call.function.arguments);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
throw fail(
|
|
982
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
983
|
+
`${location} contains structural tool arguments that are not a JSON object.`,
|
|
984
|
+
error,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
if (!plainStructuralRecord(argumentsRecord)) {
|
|
988
|
+
throw fail(
|
|
989
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
990
|
+
`${location} contains structural tool arguments that are not a JSON object.`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
if (typeof argumentsRecord.message !== "string" || !argumentsRecord.message.trim()) {
|
|
994
|
+
throw fail(
|
|
995
|
+
"ARCANE_AI_TOOL_MESSAGE_REQUIRED",
|
|
996
|
+
`${location} structural tool arguments must include a nonempty user-facing message.`,
|
|
997
|
+
);
|
|
1381
998
|
}
|
|
1382
999
|
ids.add(call.id);
|
|
1383
1000
|
}
|
|
1001
|
+
return calls;
|
|
1384
1002
|
}
|
|
1385
1003
|
|
|
1386
1004
|
function validateCompletion(value, requestId) {
|
|
1387
1005
|
if (
|
|
1388
|
-
!value
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1006
|
+
!plainStructuralRecord(value)
|
|
1007
|
+
) {
|
|
1008
|
+
throw fail("ARCANE_AI_INVALID_PROVIDER_RESULT", "The model returned an invalid chat completion.");
|
|
1009
|
+
}
|
|
1010
|
+
const hasMessage = Object.hasOwn(value, "message");
|
|
1011
|
+
const hasChoices = Object.hasOwn(value, "choices");
|
|
1012
|
+
if (hasMessage === hasChoices) {
|
|
1013
|
+
throw fail(
|
|
1014
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
1015
|
+
"The model completion must contain exactly one message or choices envelope.",
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
if (hasMessage) {
|
|
1019
|
+
const messageDescriptor = Object.getOwnPropertyDescriptor(value, "message");
|
|
1020
|
+
if (
|
|
1021
|
+
!messageDescriptor
|
|
1022
|
+
|| !Object.hasOwn(messageDescriptor, "value")
|
|
1023
|
+
|| !plainStructuralRecord(messageDescriptor.value)
|
|
1024
|
+
|| messageDescriptor.value.role !== "assistant"
|
|
1025
|
+
) {
|
|
1026
|
+
throw fail("ARCANE_AI_INVALID_PROVIDER_RESULT", "The model returned an invalid assistant message.");
|
|
1027
|
+
}
|
|
1028
|
+
validateToolCalls(messageDescriptor.value);
|
|
1029
|
+
return requestId === undefined ? value : completeValue({ ...value, id: requestId });
|
|
1030
|
+
}
|
|
1031
|
+
const choicesDescriptor = Object.getOwnPropertyDescriptor(value, "choices");
|
|
1032
|
+
if (
|
|
1033
|
+
!choicesDescriptor
|
|
1034
|
+
|| !Object.hasOwn(choicesDescriptor, "value")
|
|
1035
|
+
|| !Array.isArray(choicesDescriptor.value)
|
|
1036
|
+
|| choicesDescriptor.value.length === 0
|
|
1392
1037
|
) {
|
|
1393
1038
|
throw fail("ARCANE_AI_INVALID_PROVIDER_RESULT", "The model returned an invalid chat completion.");
|
|
1394
1039
|
}
|
|
1040
|
+
const choices = choicesDescriptor.value;
|
|
1395
1041
|
const indexes = new Set();
|
|
1396
|
-
|
|
1397
|
-
|
|
1042
|
+
let toolCallCount = 0;
|
|
1043
|
+
for (let choicePosition = 0; choicePosition < choices.length; choicePosition += 1) {
|
|
1044
|
+
const choice = choices[choicePosition];
|
|
1045
|
+
const messageDescriptor = plainStructuralRecord(choice)
|
|
1046
|
+
? Object.getOwnPropertyDescriptor(choice, "message")
|
|
1047
|
+
: null;
|
|
1048
|
+
if (
|
|
1049
|
+
!plainStructuralRecord(choice)
|
|
1050
|
+
|| !Number.isSafeInteger(choice.index)
|
|
1051
|
+
|| choice.index < 0
|
|
1052
|
+
|| indexes.has(choice.index)
|
|
1053
|
+
|| !messageDescriptor
|
|
1054
|
+
|| !Object.hasOwn(messageDescriptor, "value")
|
|
1055
|
+
|| !plainStructuralRecord(messageDescriptor.value)
|
|
1056
|
+
|| messageDescriptor.value.role !== "assistant"
|
|
1057
|
+
) {
|
|
1398
1058
|
throw fail("ARCANE_AI_INVALID_PROVIDER_RESULT", "The model returned an invalid choice index.");
|
|
1399
1059
|
}
|
|
1400
1060
|
indexes.add(choice.index);
|
|
1401
|
-
validateToolCalls(
|
|
1061
|
+
const choiceToolCallCount = validateToolCalls(
|
|
1062
|
+
messageDescriptor.value,
|
|
1063
|
+
`The model response choice ${String(choicePosition)}`,
|
|
1064
|
+
).length;
|
|
1065
|
+
if (choicePosition > 0 && choiceToolCallCount) {
|
|
1066
|
+
throw fail(
|
|
1067
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
1068
|
+
"The model placed a structural tool call outside the selected first choice.",
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
toolCallCount += choiceToolCallCount;
|
|
1072
|
+
if (toolCallCount > 1) {
|
|
1073
|
+
throw fail(
|
|
1074
|
+
"ARCANE_AI_PARALLEL_TOOLS_UNSUPPORTED",
|
|
1075
|
+
"The Arcane chat session accepts one structural tool call at a time.",
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return requestId === undefined ? value : completeValue({ ...value, id: requestId });
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function isPublicStreamContentKey(key) {
|
|
1083
|
+
return key === "content"
|
|
1084
|
+
|| key === "text"
|
|
1085
|
+
|| key === "thinking"
|
|
1086
|
+
|| key === "reasoning"
|
|
1087
|
+
|| key === "reasoning_content";
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function projectPublicStreamContent(value, seen = new WeakSet()) {
|
|
1091
|
+
if (!value || typeof value !== "object" || seen.has(value)) return null;
|
|
1092
|
+
seen.add(value);
|
|
1093
|
+
if (Array.isArray(value)) {
|
|
1094
|
+
const result = [];
|
|
1095
|
+
for (const item of value) {
|
|
1096
|
+
const projected = projectPublicStreamContent(item, seen);
|
|
1097
|
+
if (projected !== null) result.push(projected);
|
|
1098
|
+
}
|
|
1099
|
+
seen.delete(value);
|
|
1100
|
+
return result.length ? result : null;
|
|
1402
1101
|
}
|
|
1403
|
-
|
|
1102
|
+
if (!plainStructuralRecord(value)) {
|
|
1103
|
+
seen.delete(value);
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
const result = {};
|
|
1107
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1108
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
1109
|
+
if (typeof key === "symbol") continue;
|
|
1110
|
+
const descriptor = descriptors[key];
|
|
1111
|
+
if (!Object.hasOwn(descriptor, "value")) continue;
|
|
1112
|
+
if (
|
|
1113
|
+
isPublicStreamContentKey(key)
|
|
1114
|
+
&& descriptor.value !== null
|
|
1115
|
+
&& descriptor.value !== undefined
|
|
1116
|
+
) {
|
|
1117
|
+
result[key] = descriptor.value;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const projected = projectPublicStreamContent(descriptor.value, seen);
|
|
1121
|
+
if (projected !== null) result[key] = projected;
|
|
1122
|
+
}
|
|
1123
|
+
seen.delete(value);
|
|
1124
|
+
return Object.keys(result).length ? result : null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function projectPublicStreamChunk(value) {
|
|
1128
|
+
if (typeof value === "string") return value;
|
|
1129
|
+
return projectPublicStreamContent(value);
|
|
1404
1130
|
}
|
|
1405
1131
|
|
|
1406
1132
|
function createCompletionAccumulator(modelId, requestId) {
|
|
@@ -1453,14 +1179,34 @@ function createCompletionAccumulator(modelId, requestId) {
|
|
|
1453
1179
|
const tool = record.tools.get(fragment.index) ?? {
|
|
1454
1180
|
index: fragment.index,
|
|
1455
1181
|
id: "",
|
|
1182
|
+
invalidArguments: false,
|
|
1183
|
+
invalidIdentity: false,
|
|
1456
1184
|
type: "",
|
|
1457
1185
|
name: "",
|
|
1458
1186
|
arguments: "",
|
|
1459
1187
|
};
|
|
1460
|
-
if (
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1188
|
+
if (fragment.id !== undefined) {
|
|
1189
|
+
if (typeof fragment.id !== "string" || !fragment.id || tool.id && tool.id !== fragment.id) {
|
|
1190
|
+
tool.invalidIdentity = true;
|
|
1191
|
+
} else {
|
|
1192
|
+
tool.id = fragment.id;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (fragment.type !== undefined) {
|
|
1196
|
+
if (typeof fragment.type !== "string" || !fragment.type || tool.type && tool.type !== fragment.type) {
|
|
1197
|
+
tool.invalidIdentity = true;
|
|
1198
|
+
} else {
|
|
1199
|
+
tool.type = fragment.type;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (fragment.function?.name !== undefined) {
|
|
1203
|
+
if (typeof fragment.function.name !== "string") tool.invalidIdentity = true;
|
|
1204
|
+
else tool.name += fragment.function.name;
|
|
1205
|
+
}
|
|
1206
|
+
if (fragment.function?.arguments !== undefined) {
|
|
1207
|
+
if (typeof fragment.function.arguments !== "string") tool.invalidArguments = true;
|
|
1208
|
+
else tool.arguments += fragment.function.arguments;
|
|
1209
|
+
}
|
|
1464
1210
|
record.tools.set(fragment.index, tool);
|
|
1465
1211
|
}
|
|
1466
1212
|
}
|
|
@@ -1479,11 +1225,19 @@ function createCompletionAccumulator(modelId, requestId) {
|
|
|
1479
1225
|
if (record.tools.size) {
|
|
1480
1226
|
message.tool_calls = [...record.tools.values()]
|
|
1481
1227
|
.sort((a, b) => a.index - b.index)
|
|
1482
|
-
.map((tool) =>
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1228
|
+
.map((tool) => {
|
|
1229
|
+
if (tool.invalidArguments || tool.invalidIdentity) {
|
|
1230
|
+
throw fail(
|
|
1231
|
+
"ARCANE_AI_TOOL_CALL_INVALID",
|
|
1232
|
+
"A streamed structural tool call changed or omitted an exact field.",
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
id: tool.id,
|
|
1237
|
+
type: tool.type,
|
|
1238
|
+
function: { name: tool.name, arguments: tool.arguments },
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
1487
1241
|
}
|
|
1488
1242
|
return { index: record.index, message, finish_reason: record.finish_reason };
|
|
1489
1243
|
}),
|
|
@@ -1491,7 +1245,7 @@ function createCompletionAccumulator(modelId, requestId) {
|
|
|
1491
1245
|
return validateCompletion(completion, requestId);
|
|
1492
1246
|
}
|
|
1493
1247
|
|
|
1494
|
-
return
|
|
1248
|
+
return completeValue({ push, result });
|
|
1495
1249
|
}
|
|
1496
1250
|
|
|
1497
1251
|
function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
@@ -1508,9 +1262,11 @@ function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
|
1508
1262
|
if (ended || linked.controller.signal.aborted) return;
|
|
1509
1263
|
const chunk = request.id === undefined ? value : { ...value, id: request.id };
|
|
1510
1264
|
accumulator.push(chunk);
|
|
1265
|
+
const publicChunk = projectPublicStreamChunk(chunk);
|
|
1266
|
+
if (publicChunk === null) return;
|
|
1511
1267
|
const waiter = waiters.shift();
|
|
1512
|
-
if (waiter) waiter.resolve({ value:
|
|
1513
|
-
else chunks.push(
|
|
1268
|
+
if (waiter) waiter.resolve({ value: publicChunk, done: false });
|
|
1269
|
+
else chunks.push(publicChunk);
|
|
1514
1270
|
}
|
|
1515
1271
|
|
|
1516
1272
|
function finish(error = null) {
|
|
@@ -1579,7 +1335,11 @@ function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
|
1579
1335
|
return new Promise((resolve, reject) => waiters.push({ resolve, reject }));
|
|
1580
1336
|
},
|
|
1581
1337
|
async return(value) {
|
|
1582
|
-
|
|
1338
|
+
Promise.resolve().then(() => this.cancel(
|
|
1339
|
+
"The stream consumer stopped before completion.",
|
|
1340
|
+
)).catch(function reportBrowserWasmStreamReturnCancellationFailure(error) {
|
|
1341
|
+
console.error("Arcane browser-WASM stream early-return cancellation failed.", error);
|
|
1342
|
+
});
|
|
1583
1343
|
return { value, done: true };
|
|
1584
1344
|
},
|
|
1585
1345
|
async throw(error) {
|
|
@@ -1590,23 +1350,133 @@ function callbackStreamHandle({ runtime, request, signal, onSettled }) {
|
|
|
1590
1350
|
return this;
|
|
1591
1351
|
},
|
|
1592
1352
|
};
|
|
1593
|
-
return
|
|
1353
|
+
return completeValue(handle);
|
|
1594
1354
|
}
|
|
1595
1355
|
|
|
1596
|
-
function
|
|
1356
|
+
function validatedV1StreamHandle(opened, request) {
|
|
1357
|
+
if (
|
|
1358
|
+
!opened
|
|
1359
|
+
|| typeof opened !== "object"
|
|
1360
|
+
|| typeof opened[Symbol.asyncIterator] !== "function"
|
|
1361
|
+
|| typeof opened.cancel !== "function"
|
|
1362
|
+
|| !opened.result
|
|
1363
|
+
|| typeof opened.result.then !== "function"
|
|
1364
|
+
) {
|
|
1365
|
+
if (typeof opened?.cancel === "function") {
|
|
1366
|
+
Promise.resolve().then(function cancelInvalidV1StreamHandle() {
|
|
1367
|
+
return opened.cancel("The v1 provider returned an invalid stream handle.");
|
|
1368
|
+
}).catch(function reportInvalidV1StreamCleanupFailure(error) {
|
|
1369
|
+
console.error("Arcane invalid v1 stream cleanup failed.", error);
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
throw fail(
|
|
1373
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
1374
|
+
"The browser-WASM adapter stream must expose an async iterator, result, and cancel().",
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
let iterator;
|
|
1378
|
+
try {
|
|
1379
|
+
iterator = opened[Symbol.asyncIterator]();
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
Promise.resolve().then(function cancelRejectedV1Iterator() {
|
|
1382
|
+
return opened.cancel(error);
|
|
1383
|
+
}).catch(function reportRejectedV1IteratorCleanupFailure(cleanupError) {
|
|
1384
|
+
console.error("Arcane rejected v1 stream iterator cleanup failed.", cleanupError);
|
|
1385
|
+
});
|
|
1386
|
+
throw error;
|
|
1387
|
+
}
|
|
1388
|
+
if (!iterator || typeof iterator.next !== "function") {
|
|
1389
|
+
Promise.resolve().then(function cancelInvalidV1Iterator() {
|
|
1390
|
+
return opened.cancel("The v1 provider returned an invalid stream iterator.");
|
|
1391
|
+
}).catch(function reportInvalidV1IteratorCleanupFailure(error) {
|
|
1392
|
+
console.error("Arcane invalid v1 stream iterator cleanup failed.", error);
|
|
1393
|
+
});
|
|
1394
|
+
throw fail(
|
|
1395
|
+
"ARCANE_AI_INVALID_PROVIDER_RESULT",
|
|
1396
|
+
"The browser-WASM adapter stream iterator has no next() method.",
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
const result = Promise.resolve(opened.result).then(
|
|
1400
|
+
function validateV1StreamTerminal(value) {
|
|
1401
|
+
return validateCompletion(value, request.id);
|
|
1402
|
+
},
|
|
1403
|
+
);
|
|
1404
|
+
result.catch(function retainV1StreamTerminalRejection() {});
|
|
1405
|
+
const handle = {
|
|
1406
|
+
result,
|
|
1407
|
+
cancel: function cancelValidatedV1Stream(reason) {
|
|
1408
|
+
return opened.cancel(reason);
|
|
1409
|
+
},
|
|
1410
|
+
async next(value) {
|
|
1411
|
+
let nextValue = value;
|
|
1412
|
+
while (true) {
|
|
1413
|
+
const next = await iterator.next(nextValue);
|
|
1414
|
+
nextValue = undefined;
|
|
1415
|
+
if (next.done) return { value: undefined, done: true };
|
|
1416
|
+
const projected = projectPublicStreamChunk(next.value);
|
|
1417
|
+
if (projected !== null) return { value: projected, done: false };
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
async return(value) {
|
|
1421
|
+
if (typeof iterator.return === "function") {
|
|
1422
|
+
Promise.resolve().then(function returnUnderlyingV1Stream() {
|
|
1423
|
+
return iterator.return(value);
|
|
1424
|
+
}).catch(function reportUnderlyingV1StreamReturnFailure(error) {
|
|
1425
|
+
console.error("Arcane v1 stream iterator return failed.", error);
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
Promise.resolve().then(function cancelReturnedV1Stream() {
|
|
1429
|
+
return opened.cancel("The stream consumer stopped before completion.");
|
|
1430
|
+
}).catch(function reportReturnedV1StreamCancellationFailure(error) {
|
|
1431
|
+
console.error("Arcane v1 stream early-return cancellation failed.", error);
|
|
1432
|
+
});
|
|
1433
|
+
return { value, done: true };
|
|
1434
|
+
},
|
|
1435
|
+
async throw(error) {
|
|
1436
|
+
await opened.cancel(error);
|
|
1437
|
+
throw error;
|
|
1438
|
+
},
|
|
1439
|
+
[Symbol.asyncIterator]() {
|
|
1440
|
+
return this;
|
|
1441
|
+
},
|
|
1442
|
+
};
|
|
1443
|
+
return completeValue(handle);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function positiveLoadInteger(value, field, fallback) {
|
|
1597
1447
|
const resolved = value === undefined ? fallback : value;
|
|
1598
|
-
if (!Number.isSafeInteger(resolved) || resolved < 1
|
|
1599
|
-
throw new RangeError(`${field} must be a positive safe integer
|
|
1448
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
1449
|
+
throw new RangeError(`${field} must be a positive safe integer.`);
|
|
1600
1450
|
}
|
|
1601
1451
|
return resolved;
|
|
1602
1452
|
}
|
|
1603
1453
|
|
|
1454
|
+
function optionalLoadBoolean(value, field) {
|
|
1455
|
+
if (value === undefined) return undefined;
|
|
1456
|
+
if (typeof value !== "boolean") throw new TypeError(`${field} must be a boolean when provided.`);
|
|
1457
|
+
return value;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function optionalLoadText(value, field) {
|
|
1461
|
+
if (value === undefined) return undefined;
|
|
1462
|
+
if (typeof value !== "string") throw new TypeError(`${field} must be a string when provided.`);
|
|
1463
|
+
return value;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function optionalTemplateDefaults(value) {
|
|
1467
|
+
if (value === undefined) return undefined;
|
|
1468
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1469
|
+
throw new TypeError("templateDefaults must be a plain object when provided.");
|
|
1470
|
+
}
|
|
1471
|
+
return { ...value };
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1604
1474
|
function measuredRuntimeCapabilities(runtimeCapabilities) {
|
|
1605
1475
|
const measuredDeviceMemory = Number(globalThis.navigator?.deviceMemory);
|
|
1606
1476
|
const deviceMemory = Number.isFinite(measuredDeviceMemory) && measuredDeviceMemory > 0
|
|
1607
1477
|
? measuredDeviceMemory
|
|
1608
1478
|
: null;
|
|
1609
|
-
return
|
|
1479
|
+
return completeValue({ ...runtimeCapabilities, deviceMemory });
|
|
1610
1480
|
}
|
|
1611
1481
|
|
|
1612
1482
|
function capabilityLoadPlan(runtimeCapabilities, defaults, options = {}) {
|
|
@@ -1632,32 +1502,36 @@ function capabilityLoadPlan(runtimeCapabilities, defaults, options = {}) {
|
|
|
1632
1502
|
configured.threads,
|
|
1633
1503
|
"threads",
|
|
1634
1504
|
Math.max(1, Math.min(4, hardwareConcurrency - 1 || 1)),
|
|
1635
|
-
64,
|
|
1636
1505
|
);
|
|
1637
1506
|
const contextTokens = positiveLoadInteger(
|
|
1638
1507
|
configured.contextTokens,
|
|
1639
1508
|
"contextTokens",
|
|
1640
1509
|
defaultContext,
|
|
1641
|
-
1_048_576,
|
|
1642
1510
|
);
|
|
1643
1511
|
const batchTokens = positiveLoadInteger(
|
|
1644
1512
|
configured.batchTokens,
|
|
1645
1513
|
"batchTokens",
|
|
1646
1514
|
Math.min(defaultBatch, contextTokens),
|
|
1647
|
-
contextTokens,
|
|
1648
1515
|
);
|
|
1649
1516
|
const microBatchTokens = positiveLoadInteger(
|
|
1650
1517
|
configured.microBatchTokens,
|
|
1651
1518
|
"microBatchTokens",
|
|
1652
1519
|
Math.min(defaultMicroBatch, batchTokens),
|
|
1653
|
-
batchTokens,
|
|
1654
1520
|
);
|
|
1655
|
-
|
|
1521
|
+
const reasoning = optionalLoadBoolean(configured.reasoning, "reasoning");
|
|
1522
|
+
const chatTemplate = optionalLoadText(configured.chatTemplate, "chatTemplate");
|
|
1523
|
+
const jinja = optionalLoadBoolean(configured.jinja, "jinja");
|
|
1524
|
+
const templateDefaults = optionalTemplateDefaults(configured.templateDefaults);
|
|
1525
|
+
return completeValue({
|
|
1656
1526
|
threads,
|
|
1657
1527
|
contextTokens,
|
|
1658
1528
|
batchTokens,
|
|
1659
1529
|
microBatchTokens,
|
|
1660
1530
|
gpuLayers: 99_999,
|
|
1531
|
+
...(reasoning!==undefined?{ reasoning }:{}),
|
|
1532
|
+
...(chatTemplate!==undefined?{ chatTemplate }:{}),
|
|
1533
|
+
...(jinja!==undefined?{ jinja }:{}),
|
|
1534
|
+
...(templateDefaults!==undefined?{ templateDefaults }:{}),
|
|
1661
1535
|
});
|
|
1662
1536
|
}
|
|
1663
1537
|
|
|
@@ -1666,23 +1540,27 @@ function sameLoadPlan(left, right) {
|
|
|
1666
1540
|
&& left?.contextTokens === right?.contextTokens
|
|
1667
1541
|
&& left?.batchTokens === right?.batchTokens
|
|
1668
1542
|
&& left?.microBatchTokens === right?.microBatchTokens
|
|
1669
|
-
&& left?.gpuLayers === right?.gpuLayers
|
|
1543
|
+
&& left?.gpuLayers === right?.gpuLayers
|
|
1544
|
+
&& left?.reasoning === right?.reasoning
|
|
1545
|
+
&& left?.chatTemplate === right?.chatTemplate
|
|
1546
|
+
&& left?.jinja === right?.jinja
|
|
1547
|
+
&& JSON.stringify(left?.templateDefaults) === JSON.stringify(right?.templateDefaults);
|
|
1670
1548
|
}
|
|
1671
1549
|
|
|
1672
1550
|
function stableModelFailure(error) {
|
|
1673
1551
|
const code = typeof error?.code === "string" ? error.code : "";
|
|
1674
1552
|
const message = typeof error?.message === "string" ? error.message : "";
|
|
1675
1553
|
if (code === "ARCANE_AI_MODEL_SHARD_TOO_LARGE") {
|
|
1676
|
-
return
|
|
1554
|
+
return completeValue({ code });
|
|
1677
1555
|
}
|
|
1678
1556
|
if (/(?:out of memory|allocation failed|failed to allocate|memory exhausted)/iu.test(message)) {
|
|
1679
|
-
return
|
|
1557
|
+
return completeValue({ code: "ARCANE_AI_MODEL_GPU_MEMORY_INSUFFICIENT" });
|
|
1680
1558
|
}
|
|
1681
1559
|
if (code === "ARCANE_AI_WEBGPU_EVIDENCE_INVALID") {
|
|
1682
|
-
return
|
|
1560
|
+
return completeValue({ code: "ARCANE_AI_MODEL_FULL_OFFLOAD_UNPROVEN" });
|
|
1683
1561
|
}
|
|
1684
1562
|
if (code === "ARCANE_AI_WEBGPU_REQUIRED" && /(?:offload|GPU|WebGPU)/iu.test(message)) {
|
|
1685
|
-
return
|
|
1563
|
+
return completeValue({ code: "ARCANE_AI_MODEL_WEBGPU_REQUIREMENT_FAILED" });
|
|
1686
1564
|
}
|
|
1687
1565
|
return null;
|
|
1688
1566
|
}
|
|
@@ -1697,10 +1575,10 @@ function capabilityPolicy(
|
|
|
1697
1575
|
failure = null,
|
|
1698
1576
|
) {
|
|
1699
1577
|
const reasons = [];
|
|
1700
|
-
const add = (code, compatibility, details = {}) => reasons.push(
|
|
1578
|
+
const add = (code, compatibility, details = {}) => reasons.push(completeValue({
|
|
1701
1579
|
code,
|
|
1702
1580
|
compatibility,
|
|
1703
|
-
details:
|
|
1581
|
+
details: completeValue(details),
|
|
1704
1582
|
}));
|
|
1705
1583
|
if (runtimeCapabilities.webAssembly !== true) {
|
|
1706
1584
|
add("ARCANE_AI_WEBASSEMBLY_UNAVAILABLE", "incompatible");
|
|
@@ -1714,37 +1592,21 @@ function capabilityPolicy(
|
|
|
1714
1592
|
if (runtimeCapabilities.webgpuApiPresent !== true) {
|
|
1715
1593
|
add("ARCANE_AI_WEBGPU_API_UNAVAILABLE", "incompatible");
|
|
1716
1594
|
}
|
|
1717
|
-
|
|
1718
|
-
if (oversized) {
|
|
1719
|
-
add("ARCANE_AI_MODEL_SHARD_TOO_LARGE", "incompatible", {
|
|
1720
|
-
name: oversized.name,
|
|
1721
|
-
bytes: oversized.bytes,
|
|
1722
|
-
maximumBytes: WLLAMA_MAX_FILE_BYTES,
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
if (failure && failure.code !== "ARCANE_AI_MODEL_SHARD_TOO_LARGE") {
|
|
1595
|
+
if (failure) {
|
|
1726
1596
|
add(failure.code, "incompatible");
|
|
1727
1597
|
}
|
|
1728
1598
|
if (storage?.compatibility === "incompatible") {
|
|
1729
|
-
add(storage.code, "incompatible"
|
|
1730
|
-
requiredBytes: storage.requiredBytes,
|
|
1731
|
-
availableBytes: storage.availableBytes,
|
|
1732
|
-
});
|
|
1599
|
+
add(storage.code, "incompatible");
|
|
1733
1600
|
} else if (!storage || storage.compatibility === "unknown") {
|
|
1734
|
-
add(storage?.code ?? "ARCANE_AI_STORAGE_NOT_MEASURED", "unknown"
|
|
1735
|
-
requiredBytes: storage?.requiredBytes ?? knownModelBytes(source),
|
|
1736
|
-
availableBytes: storage?.availableBytes ?? null,
|
|
1737
|
-
});
|
|
1601
|
+
add(storage?.code ?? "ARCANE_AI_STORAGE_NOT_MEASURED", "unknown");
|
|
1738
1602
|
}
|
|
1739
1603
|
const webgpu = runtimeEvidence?.webgpu;
|
|
1740
1604
|
if (state === "ready" && webgpu?.observed === true) {
|
|
1741
|
-
add(
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
logicalBufferDescriptorBytes: webgpu.buffers?.descriptorBytes ?? null,
|
|
1747
|
-
});
|
|
1605
|
+
add(
|
|
1606
|
+
"ARCANE_AI_WEBGPU_EXECUTION_OBSERVED",
|
|
1607
|
+
"compatible",
|
|
1608
|
+
{},
|
|
1609
|
+
);
|
|
1748
1610
|
} else if (runtimeCapabilities.webgpuApiPresent === true) {
|
|
1749
1611
|
add("ARCANE_AI_WEBGPU_EXECUTION_UNOBSERVED", "unknown", {
|
|
1750
1612
|
requestedGpuLayers: loadPlan.gpuLayers,
|
|
@@ -1755,18 +1617,14 @@ function capabilityPolicy(
|
|
|
1755
1617
|
: reasons.some((reason) => reason.compatibility === "unknown")
|
|
1756
1618
|
? "unknown"
|
|
1757
1619
|
: "compatible";
|
|
1758
|
-
return
|
|
1620
|
+
return completeValue({
|
|
1759
1621
|
protocol: CAPABILITY_POLICY_PROTOCOL,
|
|
1760
1622
|
compatibility,
|
|
1761
|
-
reasons:
|
|
1762
|
-
model:
|
|
1763
|
-
id: source.id,
|
|
1764
|
-
fileCount: sourceMetadata(source).files.length,
|
|
1765
|
-
declaredBytes: knownModelBytes(source),
|
|
1766
|
-
}),
|
|
1623
|
+
reasons: completeValue(reasons),
|
|
1624
|
+
model: completeValue({ id: source.id }),
|
|
1767
1625
|
load: loadPlan,
|
|
1768
1626
|
storage: storage ?? null,
|
|
1769
|
-
inputs:
|
|
1627
|
+
inputs: completeValue({
|
|
1770
1628
|
hardwareConcurrency: runtimeCapabilities.hardwareConcurrency,
|
|
1771
1629
|
deviceMemory: runtimeCapabilities.deviceMemory,
|
|
1772
1630
|
deviceMemoryMeaning: "coarse-system-memory-gib",
|
|
@@ -1799,8 +1657,8 @@ function providerModelSources(source, sources) {
|
|
|
1799
1657
|
if (source !== undefined && !list.includes(source)) {
|
|
1800
1658
|
throw new TypeError("The legacy default source must be one member of sources.");
|
|
1801
1659
|
}
|
|
1802
|
-
return
|
|
1803
|
-
sources:
|
|
1660
|
+
return completeValue({
|
|
1661
|
+
sources: completeValue(list.slice()),
|
|
1804
1662
|
defaultSource: source ?? list[0],
|
|
1805
1663
|
});
|
|
1806
1664
|
}
|
|
@@ -1834,11 +1692,9 @@ export function createBrowserWasmLlmProvider({
|
|
|
1834
1692
|
runtimeLoadDefaults,
|
|
1835
1693
|
);
|
|
1836
1694
|
let state = "unloaded";
|
|
1837
|
-
let progressState = null;
|
|
1838
1695
|
let errorState = null;
|
|
1839
1696
|
let cacheState = "unknown";
|
|
1840
1697
|
let activeSecurity = null;
|
|
1841
|
-
let activeIntegrity = null;
|
|
1842
1698
|
let queueDepth = 0;
|
|
1843
1699
|
let disposed = false;
|
|
1844
1700
|
let disposing = false;
|
|
@@ -1865,7 +1721,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
1865
1721
|
|
|
1866
1722
|
function capabilities() {
|
|
1867
1723
|
const runtimeCapabilities = measuredRuntimeCapabilities(runtime.capabilities());
|
|
1868
|
-
return
|
|
1724
|
+
return completeValue({
|
|
1869
1725
|
localOnly: true,
|
|
1870
1726
|
toolCalls: "structural-only",
|
|
1871
1727
|
webAssembly: runtimeCapabilities.webAssembly,
|
|
@@ -1899,7 +1755,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
1899
1755
|
|
|
1900
1756
|
function catalog() {
|
|
1901
1757
|
const runtimeCapabilities = measuredRuntimeCapabilities(runtime.capabilities());
|
|
1902
|
-
return
|
|
1758
|
+
return completeValue(modelSources.map((candidate) => {
|
|
1903
1759
|
const selected = candidate === activeSource;
|
|
1904
1760
|
const plan = selected
|
|
1905
1761
|
? activeLoadPlan
|
|
@@ -1913,7 +1769,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
1913
1769
|
selected ? state : "unloaded",
|
|
1914
1770
|
modelFailures.get(candidate.id) ?? null,
|
|
1915
1771
|
);
|
|
1916
|
-
return
|
|
1772
|
+
return completeValue({
|
|
1917
1773
|
...publicDescriptor(candidate),
|
|
1918
1774
|
compatibility: policy.compatibility,
|
|
1919
1775
|
compatibilityDetails: policy,
|
|
@@ -1926,8 +1782,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
1926
1782
|
app: context?.security,
|
|
1927
1783
|
binding: bindingSecurity,
|
|
1928
1784
|
});
|
|
1929
|
-
const
|
|
1930
|
-
return
|
|
1785
|
+
const publicSecurity = securitySnapshot(effectiveSecurity);
|
|
1786
|
+
return completeValue({
|
|
1931
1787
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
1932
1788
|
provider: "arcane-browser-wasm-wllama",
|
|
1933
1789
|
state,
|
|
@@ -1935,10 +1791,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
1935
1791
|
busy: activeCount > 0,
|
|
1936
1792
|
queued: Math.max(0, queueDepth - activeCount),
|
|
1937
1793
|
model: publicDescriptor(activeSource),
|
|
1938
|
-
cache:
|
|
1939
|
-
security:
|
|
1940
|
-
integrity,
|
|
1941
|
-
progress: progressState,
|
|
1794
|
+
cache: completeValue({ state: cacheState }),
|
|
1795
|
+
...(publicSecurity ? { security: publicSecurity } : {}),
|
|
1942
1796
|
error: errorState,
|
|
1943
1797
|
runtime: runtime.authority,
|
|
1944
1798
|
runtimeEvidence: runtime.evidence(),
|
|
@@ -1955,9 +1809,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
1955
1809
|
|| error?.code === "ARCANE_AI_WORKER_TERMINATION_UNCONFIRMED"
|
|
1956
1810
|
? "error"
|
|
1957
1811
|
: "unloaded";
|
|
1958
|
-
progressState = null;
|
|
1959
1812
|
errorState = state === "error"
|
|
1960
|
-
?
|
|
1813
|
+
? completeValue({
|
|
1961
1814
|
code: typeof error?.code === "string" ? error.code : "ARCANE_AI_RUNTIME_FAILED",
|
|
1962
1815
|
message: typeof error?.message === "string"
|
|
1963
1816
|
? error.message
|
|
@@ -1966,12 +1819,6 @@ export function createBrowserWasmLlmProvider({
|
|
|
1966
1819
|
: null;
|
|
1967
1820
|
}
|
|
1968
1821
|
|
|
1969
|
-
function report(value, options, context) {
|
|
1970
|
-
progressState = value;
|
|
1971
|
-
options?.onProgress?.(value);
|
|
1972
|
-
context?.reportProgress?.(value);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
1822
|
async function load(options = {}, context = {}) {
|
|
1976
1823
|
if (disposed || disposing) {
|
|
1977
1824
|
throw fail("ARCANE_AI_DISPOSED", "The browser-WASM provider is disposed or disposing.");
|
|
@@ -1988,22 +1835,11 @@ export function createBrowserWasmLlmProvider({
|
|
|
1988
1835
|
binding: bindingSecurity,
|
|
1989
1836
|
load: options.security,
|
|
1990
1837
|
});
|
|
1991
|
-
assertDescriptorChecks(requestedSource, effectiveSecurity);
|
|
1992
1838
|
const requestedLoadPlan = capabilityLoadPlan(
|
|
1993
1839
|
measuredRuntimeCapabilities(runtime.capabilities()),
|
|
1994
1840
|
runtimeLoadDefaults,
|
|
1995
1841
|
options,
|
|
1996
1842
|
);
|
|
1997
|
-
const oversized = oversizedModelFile(requestedSource);
|
|
1998
|
-
if (oversized) {
|
|
1999
|
-
modelFailures.set(requestedSource.id, Object.freeze({
|
|
2000
|
-
code: "ARCANE_AI_MODEL_SHARD_TOO_LARGE",
|
|
2001
|
-
}));
|
|
2002
|
-
throw fail(
|
|
2003
|
-
"ARCANE_AI_MODEL_SHARD_TOO_LARGE",
|
|
2004
|
-
`Model file ${oversized.name} exceeds Wllama's ${WLLAMA_MAX_FILE_BYTES}-byte boundary.`,
|
|
2005
|
-
);
|
|
2006
|
-
}
|
|
2007
1843
|
if (state === "ready") {
|
|
2008
1844
|
if (activeSource !== requestedSource) {
|
|
2009
1845
|
throw fail(
|
|
@@ -2017,14 +1853,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
2017
1853
|
"Unload the browser-WASM model before changing its context or batch load plan.",
|
|
2018
1854
|
);
|
|
2019
1855
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
return Object.freeze({ model: publicDescriptor(activeSource), status: status() });
|
|
2023
|
-
}
|
|
2024
|
-
throw fail(
|
|
2025
|
-
"ARCANE_AI_SECURITY_RELOAD_REQUIRED",
|
|
2026
|
-
"Unload the browser-WASM model before changing its effective security checks.",
|
|
2027
|
-
);
|
|
1856
|
+
activeSecurity = effectiveSecurity;
|
|
1857
|
+
return completeValue({ model: publicDescriptor(activeSource), status: status() });
|
|
2028
1858
|
}
|
|
2029
1859
|
if (loadPromise) {
|
|
2030
1860
|
if (activeSource !== requestedSource) {
|
|
@@ -2039,14 +1869,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
2039
1869
|
"The in-flight browser-WASM load uses a different context or batch plan.",
|
|
2040
1870
|
);
|
|
2041
1871
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
return loadPromise;
|
|
2045
|
-
}
|
|
2046
|
-
throw fail(
|
|
2047
|
-
"ARCANE_AI_SECURITY_RELOAD_REQUIRED",
|
|
2048
|
-
"The in-flight browser-WASM load uses different effective security checks.",
|
|
2049
|
-
);
|
|
1872
|
+
activeSecurity = effectiveSecurity;
|
|
1873
|
+
return loadPromise;
|
|
2050
1874
|
}
|
|
2051
1875
|
const externalSignal = options.signal ?? context.signal ?? null;
|
|
2052
1876
|
const linked = linkAbortSignal(externalSignal);
|
|
@@ -2056,9 +1880,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2056
1880
|
activeSource = requestedSource;
|
|
2057
1881
|
activeSecurity = effectiveSecurity;
|
|
2058
1882
|
activeLoadPlan = requestedLoadPlan;
|
|
2059
|
-
activeIntegrity = integritySnapshot(effectiveSecurity, activeSource);
|
|
2060
1883
|
state = "loading";
|
|
2061
|
-
progressState = null;
|
|
2062
1884
|
errorState = null;
|
|
2063
1885
|
loadPromise = (async () => {
|
|
2064
1886
|
try {
|
|
@@ -2066,21 +1888,13 @@ export function createBrowserWasmLlmProvider({
|
|
|
2066
1888
|
const admitted = await store.ensure(activeSource, {
|
|
2067
1889
|
signal,
|
|
2068
1890
|
offline: options.offline === true,
|
|
2069
|
-
security: effectiveSecurity,
|
|
2070
|
-
onProgress: (value) => report(value, options, context),
|
|
2071
1891
|
onCapabilityPolicy: (value) => { storagePolicies.set(activeSource.id, value); },
|
|
2072
1892
|
});
|
|
2073
1893
|
cacheState = admitted.cache;
|
|
2074
|
-
activeIntegrity = admitted.integrity;
|
|
2075
1894
|
throwIfAborted(signal, "load");
|
|
2076
1895
|
if (generation !== lifecycleGeneration || state !== "loading") {
|
|
2077
1896
|
throw fail("ARCANE_AI_OPERATION_SUPERSEDED", "The model load was superseded by unload.");
|
|
2078
1897
|
}
|
|
2079
|
-
report(
|
|
2080
|
-
progress(activeSource, "initialize", admitted.observedBytes, admitted.observedBytes),
|
|
2081
|
-
options,
|
|
2082
|
-
context,
|
|
2083
|
-
);
|
|
2084
1898
|
throwIfAborted(signal, "load");
|
|
2085
1899
|
if (generation !== lifecycleGeneration || state !== "loading") {
|
|
2086
1900
|
throw fail("ARCANE_AI_OPERATION_SUPERSEDED", "The model load was superseded by unload.");
|
|
@@ -2115,9 +1929,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
2115
1929
|
throw fail("ARCANE_AI_OPERATION_SUPERSEDED", "The model load was superseded by unload.");
|
|
2116
1930
|
}
|
|
2117
1931
|
state = "ready";
|
|
2118
|
-
progressState = null;
|
|
2119
1932
|
modelFailures.delete(activeSource.id);
|
|
2120
|
-
return
|
|
1933
|
+
return completeValue({ model: publicDescriptor(activeSource), status: status() });
|
|
2121
1934
|
} catch (error) {
|
|
2122
1935
|
let cleanupFailure = null;
|
|
2123
1936
|
try {
|
|
@@ -2135,7 +1948,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2135
1948
|
if (modelFailure) modelFailures.set(activeSource.id, modelFailure);
|
|
2136
1949
|
if (generation === lifecycleGeneration && state === "loading") {
|
|
2137
1950
|
state = "error";
|
|
2138
|
-
errorState =
|
|
1951
|
+
errorState = completeValue({ code: normalized.code, message: normalized.message });
|
|
2139
1952
|
}
|
|
2140
1953
|
throw normalized;
|
|
2141
1954
|
} finally {
|
|
@@ -2205,7 +2018,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2205
2018
|
if (error) reconcileRuntimeAfterRequestError(error);
|
|
2206
2019
|
},
|
|
2207
2020
|
});
|
|
2208
|
-
activeAbort =
|
|
2021
|
+
activeAbort = completeValue({
|
|
2209
2022
|
abort: (reason) => handle.cancel(reason),
|
|
2210
2023
|
});
|
|
2211
2024
|
return handle;
|
|
@@ -2239,10 +2052,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
2239
2052
|
throwIfAborted(signal, "unload");
|
|
2240
2053
|
await runtime.exit();
|
|
2241
2054
|
state = "unloaded";
|
|
2242
|
-
progressState = null;
|
|
2243
2055
|
errorState = null;
|
|
2244
2056
|
activeSecurity = null;
|
|
2245
|
-
activeIntegrity = null;
|
|
2246
2057
|
return status();
|
|
2247
2058
|
} catch (error) {
|
|
2248
2059
|
const normalized = normalizeArcaneAIError(error, {
|
|
@@ -2251,7 +2062,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2251
2062
|
signal: normalizationSignal(error, signal),
|
|
2252
2063
|
});
|
|
2253
2064
|
state = "error";
|
|
2254
|
-
errorState =
|
|
2065
|
+
errorState = completeValue({ code: normalized.code, message: normalized.message });
|
|
2255
2066
|
throw normalized;
|
|
2256
2067
|
} finally {
|
|
2257
2068
|
unloadPromise = null;
|
|
@@ -2287,7 +2098,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2287
2098
|
return runtime.probe(options);
|
|
2288
2099
|
}
|
|
2289
2100
|
|
|
2290
|
-
return
|
|
2101
|
+
return completeValue({
|
|
2291
2102
|
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
2292
2103
|
id: "arcane-browser-wasm-wllama",
|
|
2293
2104
|
model: publicDescriptor(defaultSource),
|
|
@@ -2324,22 +2135,6 @@ function assertV1LlmAdapterSelection(selection, providerId, modelIds, role) {
|
|
|
2324
2135
|
}
|
|
2325
2136
|
}
|
|
2326
2137
|
|
|
2327
|
-
function provider2ByteProgress(value) {
|
|
2328
|
-
const phase = typeof value?.phase === "string" ? value.phase.trim() : "";
|
|
2329
|
-
const completed = Number(value?.loaded);
|
|
2330
|
-
const total = value?.total === null ? null : Number(value?.total);
|
|
2331
|
-
if (
|
|
2332
|
-
!phase
|
|
2333
|
-
|| !Number.isSafeInteger(completed)
|
|
2334
|
-
|| completed < 0
|
|
2335
|
-
|| (total !== null && (!Number.isSafeInteger(total) || total < 0))
|
|
2336
|
-
|| (total !== null && completed > total)
|
|
2337
|
-
) {
|
|
2338
|
-
throw fail("ARCANE_AI_PROVIDER_PROGRESS_INVALID", "The browser-WASM provider returned invalid byte progress.");
|
|
2339
|
-
}
|
|
2340
|
-
return Object.freeze({ phase, completed, total, unit: "bytes", heartbeat: false });
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
2138
|
/**
|
|
2344
2139
|
* Projects the existing browser-WASM LLM provider into the provider-neutral
|
|
2345
2140
|
* Arcane AI /2 lifecycle without changing the provider's public v1 contract.
|
|
@@ -2369,7 +2164,7 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2369
2164
|
throw new TypeError("The browser-WASM LLM provider must be explicitly local-only.");
|
|
2370
2165
|
}
|
|
2371
2166
|
|
|
2372
|
-
const fallbackCatalog =
|
|
2167
|
+
const fallbackCatalog = completeValue([model]);
|
|
2373
2168
|
const initialCatalog = typeof provider.catalog === "function"
|
|
2374
2169
|
? provider.catalog()
|
|
2375
2170
|
: fallbackCatalog;
|
|
@@ -2393,11 +2188,10 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2393
2188
|
|
|
2394
2189
|
function authorityFor(selection) {
|
|
2395
2190
|
const selectedModel = catalogModels.get(selection.modelId);
|
|
2396
|
-
return
|
|
2191
|
+
return completeValue({
|
|
2397
2192
|
protocol: AI_MODEL_AUTHORITY_PROTOCOL,
|
|
2398
2193
|
providerId,
|
|
2399
2194
|
modelId: selectedModel.id,
|
|
2400
|
-
admitted: true,
|
|
2401
2195
|
localOnly: true,
|
|
2402
2196
|
model: selectedModel,
|
|
2403
2197
|
});
|
|
@@ -2424,19 +2218,18 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2424
2218
|
) {
|
|
2425
2219
|
throw fail("ARCANE_AI_PROVIDER_STATUS_INVALID", "The browser-WASM provider returned an invalid status.");
|
|
2426
2220
|
}
|
|
2427
|
-
return
|
|
2221
|
+
return completeValue({
|
|
2428
2222
|
state: disposed ? "disposed" : value.state,
|
|
2429
2223
|
loaded: disposed ? false : value.loaded,
|
|
2430
2224
|
busy: disposed ? false : value.busy,
|
|
2431
2225
|
cache: value.cache,
|
|
2432
|
-
|
|
2433
|
-
integrity: value.integrity,
|
|
2226
|
+
...(value.security?.secure===true?{security:{secure:true}}:{}),
|
|
2434
2227
|
capabilityPolicy: value.capabilityPolicy,
|
|
2435
2228
|
compatibility: value.capabilityPolicy?.compatibility ?? "unknown",
|
|
2436
2229
|
});
|
|
2437
2230
|
}
|
|
2438
2231
|
|
|
2439
|
-
const adapted =
|
|
2232
|
+
const adapted = completeValue({
|
|
2440
2233
|
protocol: AI_PROVIDER_PROTOCOL,
|
|
2441
2234
|
role: "llm",
|
|
2442
2235
|
id: providerId,
|
|
@@ -2448,7 +2241,7 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2448
2241
|
assertSelection(selection, role);
|
|
2449
2242
|
throwIfAborted(signal, "inspect");
|
|
2450
2243
|
if (disposed) {
|
|
2451
|
-
return
|
|
2244
|
+
return completeValue({
|
|
2452
2245
|
available: false,
|
|
2453
2246
|
code: "ARCANE_AI_DISPOSED",
|
|
2454
2247
|
message: "The browser-WASM provider is disposed.",
|
|
@@ -2463,13 +2256,13 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2463
2256
|
];
|
|
2464
2257
|
const missing = requirements.find(([available]) => !available)?.[1] ?? null;
|
|
2465
2258
|
if (missing) {
|
|
2466
|
-
return
|
|
2259
|
+
return completeValue({
|
|
2467
2260
|
available: false,
|
|
2468
2261
|
code: "ARCANE_AI_PROVIDER_UNAVAILABLE",
|
|
2469
2262
|
message: `The browser-WASM provider requires ${missing}.`,
|
|
2470
2263
|
});
|
|
2471
2264
|
}
|
|
2472
|
-
return
|
|
2265
|
+
return completeValue({ available: true, authority: authorityFor(selection) });
|
|
2473
2266
|
},
|
|
2474
2267
|
status,
|
|
2475
2268
|
async load({
|
|
@@ -2478,6 +2271,7 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2478
2271
|
signal = null,
|
|
2479
2272
|
progress = null,
|
|
2480
2273
|
security,
|
|
2274
|
+
...loadOptions
|
|
2481
2275
|
} = {}) {
|
|
2482
2276
|
assertSelection(selection, role);
|
|
2483
2277
|
throwIfAborted(signal, "load");
|
|
@@ -2485,10 +2279,10 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2485
2279
|
throw new TypeError("The provider/2 progress sink must be a function or null.");
|
|
2486
2280
|
}
|
|
2487
2281
|
await methods.load({
|
|
2282
|
+
...loadOptions,
|
|
2488
2283
|
modelId: selection.modelId,
|
|
2489
2284
|
signal,
|
|
2490
|
-
security,
|
|
2491
|
-
...(progress ? { onProgress: (value) => progress(provider2ByteProgress(value)) } : {}),
|
|
2285
|
+
...(security?.secure===true?{security:{secure:true}}:{}),
|
|
2492
2286
|
});
|
|
2493
2287
|
return status();
|
|
2494
2288
|
},
|
|
@@ -2496,8 +2290,22 @@ export function adaptV1LlmProvider(provider) {
|
|
|
2496
2290
|
assertSelection(selection, role);
|
|
2497
2291
|
assertActiveSelection(selection);
|
|
2498
2292
|
throwIfAborted(signal);
|
|
2499
|
-
if (operation === "chat")
|
|
2500
|
-
|
|
2293
|
+
if (operation === "chat") {
|
|
2294
|
+
validateStructuralRequest(payload);
|
|
2295
|
+
return Promise.resolve(methods.chat(payload, { signal })).then(
|
|
2296
|
+
function validateV1ChatTerminal(value) {
|
|
2297
|
+
return validateCompletion(value, payload.id);
|
|
2298
|
+
},
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
if (operation === "stream") {
|
|
2302
|
+
validateStructuralRequest(payload);
|
|
2303
|
+
return Promise.resolve(methods.stream(payload, { signal })).then(
|
|
2304
|
+
function wrapV1StreamResult(opened) {
|
|
2305
|
+
return validatedV1StreamHandle(opened, payload);
|
|
2306
|
+
},
|
|
2307
|
+
);
|
|
2308
|
+
}
|
|
2501
2309
|
throw fail("ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE", "The browser-WASM adapter supports only chat and stream.");
|
|
2502
2310
|
},
|
|
2503
2311
|
async unload({ role = "llm", selection, signal = null } = {}) {
|