arcane-os 0.88.0 → 0.89.0
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 +32 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +31 -4
- package/browser-runtime/ai/core-local.mjs +105 -25
- package/browser-runtime/ai/decision-runtime.mjs +7 -0
- package/browser-runtime/ai/decision-worker.mjs +9 -1
- package/browser-runtime/core/client.mjs +6 -0
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/reference/ai/browser-decisions.md +89 -7
- package/docs/reference/cli.md +10 -0
- package/docs/reference/core-client.md +59 -0
- package/docs/reference/core-native-packaging.md +57 -10
- package/docs/reference/core-repositories.md +47 -3
- package/docs/reference/inventory/package-api.json +15 -15
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +4 -4
- package/docs/reference/local-ai.md +86 -3
- package/docs/reference/mcp-stdio.md +76 -1
- package/docs/reference/native-app-control.md +17 -5
- package/docs/reference/protocols.md +14 -0
- package/docs/reference/runtime-modules.md +9 -3
- package/docs/reference/sdk-api.md +146 -23
- package/package.json +2 -2
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +1 -5
- package/schemas/arcane-app.schema.json +13 -3
- package/schemas/arcane-package.schema.json +10 -1
- package/src/app-descriptor.mjs +6 -2
- package/src/cli/app-control.mjs +1 -1
- package/src/core/hosts/windows/ArcaneHost.cs +273 -5
- package/src/core/hosts/windows/ArcaneLauncher.cs +9 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +7 -7
- package/src/core/repositories.mjs +11 -1
- package/src/core/services/local-ai.mjs +183 -22
- package/src/installed-sdk-runtime.mjs +2 -0
- package/src/local-ai/config.mjs +16 -0
- package/src/local-ai/dev.mjs +2 -2
- package/src/local-ai/discover.mjs +3 -0
- package/src/local-ai/install.mjs +130 -20
- package/src/local-ai/llama-execution-windows.ps1 +158 -0
- package/src/local-ai/llama-execution.mjs +169 -0
- package/src/local-ai/server.mjs +38 -8
- package/src/native/portable-layout.mjs +13 -3
- package/src/workspace-operation-lock.mjs +23 -10
- package/src/workspace-runtime.mjs +97 -1
|
@@ -130,6 +130,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
130
130
|
const owner = {};
|
|
131
131
|
const events = createArcaneEventSource(owner, {source: 'core-local-ai', eventTypes: ['localai.state']});
|
|
132
132
|
let context;
|
|
133
|
+
let modelAssetOwner;
|
|
133
134
|
let closed = false;
|
|
134
135
|
let onnx;
|
|
135
136
|
let onnxRecovery;
|
|
@@ -334,17 +335,31 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
334
335
|
|
|
335
336
|
function restartEngine(engine) {
|
|
336
337
|
if (engine.recovering) return engine.recovering;
|
|
338
|
+
const selecting = engine.selecting;
|
|
337
339
|
const recovering = Promise.resolve().then(async function recoverSelectedEngine() {
|
|
338
340
|
lifetimeSignal.throwIfAborted();
|
|
341
|
+
if (selecting) await selecting;
|
|
342
|
+
const previous = engine.launch ?? engine.selection;
|
|
343
|
+
// Keep a second use across replacement, before the old process
|
|
344
|
+
// releases its own use on exit.
|
|
345
|
+
const projection = previous?.assetProjectionId
|
|
346
|
+
? modelAssetOwner.retain(previous.assetProjectionId) : undefined;
|
|
347
|
+
const replacement = previous ? {...previous, projection} : undefined;
|
|
339
348
|
engine.controller.abort();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
349
|
+
try {
|
|
350
|
+
await engine.server.close();
|
|
351
|
+
await Promise.allSettled([...engine.loads.values()]);
|
|
352
|
+
lifetimeSignal.throwIfAborted();
|
|
353
|
+
engine.models = [];
|
|
354
|
+
engine.modelChanges.clear();
|
|
355
|
+
engine.revision = 0;
|
|
356
|
+
engine.released = false;
|
|
357
|
+
startEngine(engine, replacement);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
try { await projection?.release(); }
|
|
360
|
+
catch (cleanupError) { throw new AggregateError([error, cleanupError], 'llama.cpp recovery and model release failed.', {cause: error}); }
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
348
363
|
});
|
|
349
364
|
engine.recovering = recovering;
|
|
350
365
|
function releaseRecovery() {
|
|
@@ -360,6 +375,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
360
375
|
if (!engine) throw failure('LOCAL_AI_RUNTIME_NOT_SELECTED', `${id} is not selected in this application's localAI configuration.`);
|
|
361
376
|
if (engine.recovering) await awaitReadiness(engine.recovering, signal);
|
|
362
377
|
if (engine.stopping) await awaitReadiness(engine.stopping, signal);
|
|
378
|
+
if (engine.selecting) await awaitReadiness(engine.selecting, signal);
|
|
363
379
|
// An external server exposes no readiness subscription during initial
|
|
364
380
|
// loading. A new explicit load makes one fresh attempt, without polling.
|
|
365
381
|
if (retryLoading && engine.state.state === 'loading') {
|
|
@@ -431,7 +447,115 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
431
447
|
return {...engine.state, models: [...engine.models]};
|
|
432
448
|
}
|
|
433
449
|
|
|
434
|
-
async function
|
|
450
|
+
async function activateLlamaSelection(engine, parameters, request) {
|
|
451
|
+
const {model, assetProjectionId, resourcePaths} = parameters;
|
|
452
|
+
const signal = AbortSignal.any([lifetimeSignal, request.signal]);
|
|
453
|
+
signal.throwIfAborted();
|
|
454
|
+
const previous = engine.launch ?? engine.selection;
|
|
455
|
+
const retained = previous?.model === model ? previous : undefined;
|
|
456
|
+
const projectionId = assetProjectionId ?? retained?.assetProjectionId;
|
|
457
|
+
const memberPath = resourcePaths?.model ?? (projectionId === retained?.assetProjectionId ? retained?.memberPath : undefined);
|
|
458
|
+
const executionTarget = parameters.executionTarget !== undefined
|
|
459
|
+
? parameters.executionTarget : retained ? retained.executionTarget : config.llamaCpp?.executionTarget;
|
|
460
|
+
if (executionTarget !== undefined && executionTarget !== null
|
|
461
|
+
&& (!is.string(executionTarget.deviceId) || !executionTarget.deviceId)) {
|
|
462
|
+
throw new TypeError('A llama.cpp execution target needs its host deviceId, or null for the configured default.');
|
|
463
|
+
}
|
|
464
|
+
if (projectionId !== undefined && (!is.string(memberPath) || !memberPath)) {
|
|
465
|
+
throw failure('LOCAL_AI_MODEL_RESOURCE_REQUIRED', 'Projected llama.cpp loading needs resourcePaths.model.');
|
|
466
|
+
}
|
|
467
|
+
if (resourcePaths !== undefined && projectionId === undefined) {
|
|
468
|
+
throw failure('LOCAL_AI_MODEL_PROJECTION_REQUIRED', 'resourcePaths needs its retained assetProjectionId.');
|
|
469
|
+
}
|
|
470
|
+
if (retained && retained.assetProjectionId === projectionId && retained.memberPath === memberPath
|
|
471
|
+
&& (retained.executionTarget?.deviceId ?? null) === (executionTarget?.deviceId ?? null)
|
|
472
|
+
&& engine.state.available) {
|
|
473
|
+
return prepareLlamaModel(engine, model, signal);
|
|
474
|
+
}
|
|
475
|
+
if (!await awaitReadiness(engine.server.management, signal)) {
|
|
476
|
+
if (engine.server.external) {
|
|
477
|
+
throw failure('LOCAL_AI_EXTERNAL_SELECTION_UNSUPPORTED', 'The existing llama.cpp server owns its model and device selection. Use an available listener for SDK-managed model loading.');
|
|
478
|
+
}
|
|
479
|
+
await engine.ready;
|
|
480
|
+
throw failure('LOCAL_AI_RUNTIME_UNAVAILABLE', 'No managed llama.cpp executable is available.');
|
|
481
|
+
}
|
|
482
|
+
let projection;
|
|
483
|
+
let launch;
|
|
484
|
+
function cancelSelectedLoad() {
|
|
485
|
+
if (engine.launch === launch) engine.controller.abort(signal.reason);
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const configuration = {...config.llamaCpp, executionTarget};
|
|
489
|
+
if (projectionId !== undefined) {
|
|
490
|
+
const assets = modelAssetOwner ?? await context.getService('model-assets');
|
|
491
|
+
if (!assets) throw failure('LOCAL_AI_MODEL_ASSETS_UNAVAILABLE', 'The Core model-assets service is unavailable.');
|
|
492
|
+
signal.throwIfAborted();
|
|
493
|
+
modelAssetOwner = assets;
|
|
494
|
+
projection = assets.retain(projectionId);
|
|
495
|
+
const member = projection.members.find(function selectedMember(value) { return value.path === memberPath; });
|
|
496
|
+
if (!member) throw failure('LOCAL_AI_MODEL_RESOURCE_UNAVAILABLE', `The projection does not contain ${memberPath}.`);
|
|
497
|
+
configuration.model = member.nativePath;
|
|
498
|
+
configuration.alias = model;
|
|
499
|
+
}
|
|
500
|
+
launch = {model, assetProjectionId: projectionId, memberPath, executionTarget, configuration, projection};
|
|
501
|
+
// One server owns this listener. Only its replacement is serialized;
|
|
502
|
+
// ordinary router model loads keep their independent existing flow.
|
|
503
|
+
signal.throwIfAborted();
|
|
504
|
+
engine.controller.abort();
|
|
505
|
+
await engine.server.close();
|
|
506
|
+
await Promise.allSettled([...engine.loads.values()]);
|
|
507
|
+
signal.throwIfAborted();
|
|
508
|
+
engine.models = [];
|
|
509
|
+
engine.modelChanges.clear();
|
|
510
|
+
engine.revision = 0;
|
|
511
|
+
engine.released = false;
|
|
512
|
+
startEngine(engine, launch);
|
|
513
|
+
signal.addEventListener('abort', cancelSelectedLoad, {once: true});
|
|
514
|
+
if (signal.aborted) cancelSelectedLoad();
|
|
515
|
+
await engine.ready;
|
|
516
|
+
signal.throwIfAborted();
|
|
517
|
+
const result = await prepareLlamaModel(engine, model, signal);
|
|
518
|
+
signal.throwIfAborted();
|
|
519
|
+
return result;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
try {
|
|
522
|
+
if (launch && engine.launch === launch) {
|
|
523
|
+
engine.controller.abort(error);
|
|
524
|
+
await engine.server.close();
|
|
525
|
+
}
|
|
526
|
+
await projection?.release();
|
|
527
|
+
} catch (cleanupError) {
|
|
528
|
+
throw new AggregateError([error, cleanupError], 'llama.cpp loading and model release failed.', {cause: error});
|
|
529
|
+
}
|
|
530
|
+
throw error;
|
|
531
|
+
} finally {
|
|
532
|
+
signal.removeEventListener('abort', cancelSelectedLoad);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function selectLlamaModel(parameters, request) {
|
|
537
|
+
const engine = engines.get('llama.cpp');
|
|
538
|
+
if (!engine) throw failure('LOCAL_AI_RUNTIME_NOT_SELECTED', "llama.cpp is not selected in this application's localAI configuration.");
|
|
539
|
+
const preceding = engine.selecting;
|
|
540
|
+
const recovering = engine.recovering;
|
|
541
|
+
const stopping = engine.stopping;
|
|
542
|
+
const task = (async function replaceSelectedLlamaModel() {
|
|
543
|
+
if (preceding) await Promise.allSettled([preceding]);
|
|
544
|
+
if (recovering) await recovering;
|
|
545
|
+
if (stopping) await stopping;
|
|
546
|
+
return activateLlamaSelection(engine, parameters, request);
|
|
547
|
+
})();
|
|
548
|
+
engine.selecting = task;
|
|
549
|
+
function releaseSelection() { if (engine.selecting === task) engine.selecting = null; }
|
|
550
|
+
task.then(releaseSelection, releaseSelection);
|
|
551
|
+
return task;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function loadLlama(parameters, request) {
|
|
555
|
+
const {model} = parameters;
|
|
556
|
+
if (parameters.assetProjectionId !== undefined || parameters.resourcePaths !== undefined || parameters.executionTarget !== undefined) {
|
|
557
|
+
return selectLlamaModel(parameters, request);
|
|
558
|
+
}
|
|
435
559
|
const engine = await engineReady('llama.cpp', request.signal, {retryLoading: true});
|
|
436
560
|
let task = engine.loads.get(model);
|
|
437
561
|
if (!task) {
|
|
@@ -451,6 +575,17 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
451
575
|
return awaitReadiness(task, request.signal);
|
|
452
576
|
}
|
|
453
577
|
|
|
578
|
+
async function releaseLlamaServer(engine) {
|
|
579
|
+
engine.controller.abort();
|
|
580
|
+
engine.stopping = engine.server.close();
|
|
581
|
+
await engine.stopping;
|
|
582
|
+
engine.stopping = null;
|
|
583
|
+
engine.released = true;
|
|
584
|
+
engine.models = engine.models.map(function unloadedModel(value) { return {...value, loaded: false}; });
|
|
585
|
+
publish();
|
|
586
|
+
return {...engine.state, models: [...engine.models], released: true, unloaded: true};
|
|
587
|
+
}
|
|
588
|
+
|
|
454
589
|
async function chatLlama({model, payload, stream = false, streamId}, request) {
|
|
455
590
|
model = payload?.model ?? model;
|
|
456
591
|
const engine = await engineReady('llama.cpp', request.signal, {retryLoading: true});
|
|
@@ -556,17 +691,38 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
556
691
|
return result;
|
|
557
692
|
}
|
|
558
693
|
|
|
559
|
-
function startEngine(engine) {
|
|
694
|
+
function startEngine(engine, launch) {
|
|
695
|
+
if (!launch && engine.selection) {
|
|
696
|
+
// A stopped selected projection may have been released by its
|
|
697
|
+
// preparation owner. Retaining it either succeeds or reports that
|
|
698
|
+
// it must be prepared again; never start a different default model.
|
|
699
|
+
const projection = engine.selection.assetProjectionId
|
|
700
|
+
? modelAssetOwner.retain(engine.selection.assetProjectionId) : undefined;
|
|
701
|
+
launch = {...engine.selection, projection};
|
|
702
|
+
}
|
|
703
|
+
if (launch) {
|
|
704
|
+
const {projection, ...selection} = launch;
|
|
705
|
+
engine.selection = selection;
|
|
706
|
+
}
|
|
560
707
|
engine.controller = new AbortController();
|
|
561
708
|
engine.signal = AbortSignal.any([lifetimeSignal, engine.controller.signal]);
|
|
562
709
|
engine.ollamaRevision = 0;
|
|
563
710
|
engine.ollamaAppliedRevision = 0;
|
|
564
711
|
const signal = engine.signal;
|
|
712
|
+
engine.launch = launch;
|
|
565
713
|
engine.server = createLocalAIServer({
|
|
566
714
|
id: engine.id, appRoot, runtime: runtimes.find(function runtimeRecord(record) { return record.id === engine.id; }),
|
|
567
|
-
configuration: engine.id === 'llama.cpp' ? config.llamaCpp : config.ollama,
|
|
568
|
-
signal, onEvent,
|
|
569
|
-
onState(state) { engine.state = {...engine.state, ...state, error: state.error ?? null}; publish(); }
|
|
715
|
+
configuration: engine.id === 'llama.cpp' ? launch?.configuration ?? config.llamaCpp : config.ollama,
|
|
716
|
+
signal, onEvent, executionDevices, requireManaged: Boolean(launch),
|
|
717
|
+
onState(state) { engine.state = {...engine.state, ...state, error: state.error ?? null}; publish(); },
|
|
718
|
+
async onExit() {
|
|
719
|
+
await launch?.projection?.release();
|
|
720
|
+
if (launch && engine.launch === launch) {
|
|
721
|
+
engine.launch = undefined;
|
|
722
|
+
engine.models = engine.models.map(function releasedModel(model) { return {...model, loaded: false}; });
|
|
723
|
+
if (!closed) publish();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
570
726
|
});
|
|
571
727
|
engine.ready = engine.server.ready.then(async function prepareModelCatalog() {
|
|
572
728
|
await refreshModels(engine, signal);
|
|
@@ -671,20 +827,25 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
671
827
|
},
|
|
672
828
|
'llama.load': loadLlama,
|
|
673
829
|
'llama.unload': async function unloadLlama({model}, request) {
|
|
830
|
+
const selected = engines.get('llama.cpp');
|
|
831
|
+
if (selected?.launch?.model === model && selected.selecting) {
|
|
832
|
+
selected.controller.abort(failure('LOCAL_AI_MODEL_UNLOADED', `The application unloaded ${model}.`));
|
|
833
|
+
}
|
|
834
|
+
if (selected?.selecting) await Promise.allSettled([selected.selecting]);
|
|
835
|
+
request.signal.throwIfAborted();
|
|
836
|
+
// A failed or cancelled projected startup may already have
|
|
837
|
+
// stopped. Release joins its cleanup rather than replaying the
|
|
838
|
+
// rejected readiness promise as a new unload failure.
|
|
839
|
+
if (selected?.selection?.model === model && !selected.state.available && !selected.server.external) {
|
|
840
|
+
return releaseLlamaServer(selected);
|
|
841
|
+
}
|
|
674
842
|
const engine = await engineReady('llama.cpp', request.signal, {resumeReleased: false});
|
|
675
843
|
if (engine.released) return {...engine.state, models: [...engine.models], released: true, unloaded: true};
|
|
676
844
|
engine.loadControllers.get(model)?.abort(failure('LOCAL_AI_MODEL_UNLOADED', `The application unloaded ${model}.`));
|
|
677
845
|
if (engine.loads.has(model)) await Promise.allSettled([engine.loads.get(model)]);
|
|
678
846
|
if (!engine.router) {
|
|
679
847
|
if (!engine.server.owned) return {...engine.state, models: [...engine.models], released: true, unloaded: false};
|
|
680
|
-
engine
|
|
681
|
-
engine.stopping = engine.server.close();
|
|
682
|
-
await engine.stopping;
|
|
683
|
-
engine.stopping = null;
|
|
684
|
-
engine.released = true;
|
|
685
|
-
engine.models = engine.models.map(function unloadedModel(value) { return {...value, loaded: false}; });
|
|
686
|
-
publish();
|
|
687
|
-
return {...engine.state, models: [...engine.models], released: true, unloaded: true};
|
|
848
|
+
return releaseLlamaServer(engine);
|
|
688
849
|
}
|
|
689
850
|
await requestLocalJSON({url: engine.server.url, path: '/models/unload', method: 'POST', payload: {model}, signal: request.signal});
|
|
690
851
|
return refreshModels(engine, request.signal);
|
|
@@ -712,7 +873,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
712
873
|
]);
|
|
713
874
|
stopONNXSubscription?.();
|
|
714
875
|
await Promise.allSettled([...jobs, ...(onnxRecovery ? [onnxRecovery] : []), ...[...engines.values()].flatMap(function pendingWork(engine) {
|
|
715
|
-
return [...engine.loads.values(), ...(engine.recovering ? [engine.recovering] : [])];
|
|
876
|
+
return [...engine.loads.values(), ...(engine.recovering ? [engine.recovering] : []), ...(engine.selecting ? [engine.selecting] : [])];
|
|
716
877
|
})]);
|
|
717
878
|
events.dispose();
|
|
718
879
|
const failures = results.filter(function failed(result) { return result.status === 'rejected'; }).map(function cause(result) { return result.reason; });
|
|
@@ -74,6 +74,7 @@ async function writeWorkspaceLock(lockPath,lock){
|
|
|
74
74
|
export async function materializeInstalledSdkRuntime({
|
|
75
75
|
workspaceRoot,
|
|
76
76
|
sdkPackageSource,
|
|
77
|
+
browserDecisions=false,
|
|
77
78
|
workspaceOperationLease,
|
|
78
79
|
signal,
|
|
79
80
|
onEvent
|
|
@@ -93,6 +94,7 @@ export async function materializeInstalledSdkRuntime({
|
|
|
93
94
|
runtimeRoot:authority.installation.runtimeRoot,
|
|
94
95
|
browserRuntimeRoot:authority.installation.browserRuntimeRoot,
|
|
95
96
|
sdkVersion:authority.installation.packageVersion,
|
|
97
|
+
browserDecisions,
|
|
96
98
|
signal,
|
|
97
99
|
onEvent
|
|
98
100
|
});
|
package/src/local-ai/config.mjs
CHANGED
|
@@ -10,6 +10,21 @@ import {normalizeWhisperRuntimeRequirement} from './whisper/config.mjs';
|
|
|
10
10
|
const is = new Is(false);
|
|
11
11
|
export const LOCAL_AI_RUNTIME_IDS = ['llama.cpp', 'ollama', 'onnx', 'stable-diffusion.cpp', 'whisper.cpp'];
|
|
12
12
|
|
|
13
|
+
/** Select a llama distribution without discovering hardware or acquiring it. */
|
|
14
|
+
export function normalizeLlamaRuntimeRequirement(record) {
|
|
15
|
+
const backend = record.backend ?? 'auto';
|
|
16
|
+
if (!['auto', 'cpu', 'cuda'].includes(backend)) {
|
|
17
|
+
throw new ArcaneError(ERROR_CODES.usage, `Unknown llama.cpp backend: ${String(backend)}.`);
|
|
18
|
+
}
|
|
19
|
+
if (record.companionUrl !== undefined && (!is.string(record.companionUrl) || !record.companionUrl)) {
|
|
20
|
+
throw new ArcaneError(ERROR_CODES.usage, 'llama.cpp companionUrl must be a nonempty string.');
|
|
21
|
+
}
|
|
22
|
+
if (record.url && backend === 'cuda' && !record.companionUrl) {
|
|
23
|
+
throw new ArcaneError(ERROR_CODES.usage, 'A custom llama.cpp CUDA archive requires an explicit companionUrl for its CUDA runtime libraries.');
|
|
24
|
+
}
|
|
25
|
+
return {...record, backend};
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
/** Image requirements describe assets without acquiring or loading a model. */
|
|
14
29
|
export function normalizeImageRuntimeRequirement(record) {
|
|
15
30
|
const backend = record.backend ?? 'auto';
|
|
@@ -61,6 +76,7 @@ export function normalizeLocalAIConfig(value) {
|
|
|
61
76
|
if (!record || !LOCAL_AI_RUNTIME_IDS.includes(record.id)) {
|
|
62
77
|
throw new ArcaneError(ERROR_CODES.usage, `Unknown local AI runtime: ${String(record?.id ?? item)}.`);
|
|
63
78
|
}
|
|
79
|
+
if (record.id === 'llama.cpp') return normalizeLlamaRuntimeRequirement(record);
|
|
64
80
|
if (record.id === 'stable-diffusion.cpp') return normalizeImageRuntimeRequirement(record);
|
|
65
81
|
if (record.id === 'whisper.cpp') return normalizeWhisperRuntimeRequirement(record);
|
|
66
82
|
return {...record};
|
package/src/local-ai/dev.mjs
CHANGED
|
@@ -47,8 +47,8 @@ export function createDevelopmentLocalAI({
|
|
|
47
47
|
const imageSelected = selected.runtimes.some(function selectedImageRuntime(requirement) {
|
|
48
48
|
return requirement.id === 'stable-diffusion.cpp';
|
|
49
49
|
});
|
|
50
|
-
if (imageSelected || selected.runtimes.some(function
|
|
51
|
-
return requirement.id
|
|
50
|
+
if (imageSelected || selected.runtimes.some(function selectedProjectedRuntime(requirement) {
|
|
51
|
+
return ['onnx', 'llama.cpp'].includes(requirement.id);
|
|
52
52
|
})) {
|
|
53
53
|
modelAssets = createModelAssetService({appRoot});
|
|
54
54
|
definitions.push(modelAssets);
|
|
@@ -27,6 +27,9 @@ export async function discoverLocalAIRuntimes({config,appRoot,signal}={}){
|
|
|
27
27
|
const outcomes=await Promise.allSettled(selected.map(async function discoverRuntime(requirement){
|
|
28
28
|
throwIfAborted(signal);
|
|
29
29
|
if(!['llama.cpp','ollama'].includes(requirement.id))return {missing:requirement};
|
|
30
|
+
// A PATH executable or HTTP listener cannot establish its compiled
|
|
31
|
+
// backend. An explicit distribution goes through its installer owner.
|
|
32
|
+
if(requirement.id==='llama.cpp'&&requirement.backend&&requirement.backend!=='auto')return {missing:requirement};
|
|
30
33
|
const configuration=config[requirement.id==='llama.cpp'?'llamaCpp':'ollama']??{};
|
|
31
34
|
const url=localAIServerURL(requirement.id,configuration);
|
|
32
35
|
const concreteVersion=requirement.version&&requirement.version!=='latest';
|
package/src/local-ai/install.mjs
CHANGED
|
@@ -5,16 +5,28 @@ import {createRequire} from 'node:module';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {Readable} from 'node:stream';
|
|
7
7
|
import {pipeline} from 'node:stream/promises';
|
|
8
|
+
import packageMetadata from '../../package.json' with {type:'json'};
|
|
8
9
|
import {ArcaneError,ERROR_CODES,normalizeError,throwIfAborted} from '../errors.mjs';
|
|
9
10
|
import {createEventQueue} from '../event-queue.mjs';
|
|
10
11
|
import {runProcess} from '../process.mjs';
|
|
11
12
|
import {extractLocalAIArchive} from './archive.mjs';
|
|
12
|
-
import {normalizeImageRuntimeRequirement} from './config.mjs';
|
|
13
|
+
import {normalizeImageRuntimeRequirement,normalizeLlamaRuntimeRequirement} from './config.mjs';
|
|
13
14
|
import {normalizeWhisperRuntimeRequirement} from './whisper/config.mjs';
|
|
14
15
|
import {bundledWhisperRuntime, installedWhisperRuntime, installWhisperDistribution, whisperRuntimeSelection} from './whisper/install.mjs';
|
|
15
16
|
|
|
16
17
|
const is=new Is(false);
|
|
17
18
|
const installations=new Map();
|
|
19
|
+
const onnxWindowsDistribution={
|
|
20
|
+
version:'1.30.0',
|
|
21
|
+
revision:'1.30.0-directml-reshape-1',
|
|
22
|
+
asset:'arcane-onnx-runtime-windows-x64.tar.gz'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function selectedONNXNativeDistribution(runtime,platform,architecture){
|
|
26
|
+
if(runtime.id!=='onnx'||runtime.url||platform!=='win32'||architecture!=='x64')return null;
|
|
27
|
+
return (runtime.version??onnxWindowsDistribution.version)===onnxWindowsDistribution.version
|
|
28
|
+
?onnxWindowsDistribution:null;
|
|
29
|
+
}
|
|
18
30
|
|
|
19
31
|
function selectRuntimes(runtimes){
|
|
20
32
|
if(!is.array(runtimes)){
|
|
@@ -24,7 +36,8 @@ function selectRuntimes(runtimes){
|
|
|
24
36
|
for(const item of runtimes){
|
|
25
37
|
const requirement=is.string(item)?{id:item}:item;
|
|
26
38
|
const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement)
|
|
27
|
-
:requirement?.id==='whisper.cpp'?normalizeWhisperRuntimeRequirement(requirement)
|
|
39
|
+
:requirement?.id==='whisper.cpp'?normalizeWhisperRuntimeRequirement(requirement)
|
|
40
|
+
:requirement?.id==='llama.cpp'?normalizeLlamaRuntimeRequirement(requirement):requirement;
|
|
28
41
|
if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx','stable-diffusion.cpp','whisper.cpp'].includes(runtime.id)){
|
|
29
42
|
throw new ArcaneError(ERROR_CODES.targetUnavailable,`Local AI runtime installation is unavailable for ${String(runtime?.id)}.`);
|
|
30
43
|
}
|
|
@@ -32,7 +45,9 @@ function selectRuntimes(runtimes){
|
|
|
32
45
|
if(prior&&runtime.id==='whisper.cpp'&&whisperRuntimeSelection(prior)!==whisperRuntimeSelection(runtime)){
|
|
33
46
|
throw new ArcaneError(ERROR_CODES.usage,'Select one complete whisper.cpp runtime and model requirement for this operation.');
|
|
34
47
|
}
|
|
35
|
-
if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url
|
|
48
|
+
if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url
|
|
49
|
+
||(['stable-diffusion.cpp','llama.cpp'].includes(runtime.id)&&prior.backend!==runtime.backend)
|
|
50
|
+
||(runtime.id==='llama.cpp'&&prior.companionUrl!==runtime.companionUrl))){
|
|
36
51
|
throw new ArcaneError(ERROR_CODES.usage,`Select one ${runtime.id} runtime version and URL for this operation.`);
|
|
37
52
|
}
|
|
38
53
|
if(runtime.version!==undefined&&(!is.string(runtime.version)||!runtime.version)){
|
|
@@ -53,7 +68,7 @@ function selectedDirectory(directory){
|
|
|
53
68
|
return path.resolve(directory);
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
function releaseAssetNames(id,version,platform,architecture){
|
|
71
|
+
function releaseAssetNames(id,version,platform,architecture,backend){
|
|
57
72
|
if(id==='nemo-speech'){
|
|
58
73
|
if(!['x64','arm64'].includes(architecture))return [];
|
|
59
74
|
const arch=architecture==='x64'?'x86_64':'aarch64';
|
|
@@ -71,6 +86,9 @@ function releaseAssetNames(id,version,platform,architecture){
|
|
|
71
86
|
if(platform==='linux')return [`ollama-linux-${arch}.tar.zst`,`ollama-linux-${arch}.tgz`];
|
|
72
87
|
return [];
|
|
73
88
|
}
|
|
89
|
+
if(id==='llama.cpp'&&backend==='cuda'){
|
|
90
|
+
return platform==='win32'&&architecture==='x64'?[`llama-${version}-bin-win-cuda-13.4-x64.zip`]:[];
|
|
91
|
+
}
|
|
74
92
|
if(!['x64','arm64'].includes(architecture))return [];
|
|
75
93
|
if(platform==='win32')return [`llama-${version}-bin-win-cpu-${architecture}.zip`];
|
|
76
94
|
if(platform==='linux')return [`llama-${version}-bin-ubuntu-${architecture}.tar.gz`,`llama-${version}-bin-ubuntu-${architecture}.zip`];
|
|
@@ -93,9 +111,17 @@ async function selectRelease(runtime,platform,architecture,signal){
|
|
|
93
111
|
if (runtime.id === 'stable-diffusion.cpp') {
|
|
94
112
|
return selectImageRelease(runtime, platform, architecture, signal);
|
|
95
113
|
}
|
|
114
|
+
const backend=runtime.id==='llama.cpp'?llamaBackendCandidate(runtime,platform,architecture):undefined;
|
|
96
115
|
if(runtime.url){
|
|
97
116
|
const name=path.posix.basename(new URL(runtime.url).pathname);
|
|
98
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
version:runtime.version??'custom',url:runtime.url,name,
|
|
119
|
+
...(runtime.id==='llama.cpp'?{backend,requestedBackend:runtime.backend}:{}),
|
|
120
|
+
...(backend==='cuda'?{companion:{
|
|
121
|
+
url:runtime.companionUrl,
|
|
122
|
+
name:path.posix.basename(new URL(runtime.companionUrl).pathname)
|
|
123
|
+
}}:{})
|
|
124
|
+
};
|
|
99
125
|
}
|
|
100
126
|
const repository=runtime.id==='nemo-speech'?'NVIDIA/NeMo-Speech.cpp':runtime.id==='llama.cpp'?'ggml-org/llama.cpp':'ollama/ollama';
|
|
101
127
|
const version=runtime.version??(runtime.id==='nemo-speech'?'0.2.0':undefined);
|
|
@@ -112,12 +138,38 @@ async function selectRelease(runtime,platform,architecture,signal){
|
|
|
112
138
|
}
|
|
113
139
|
}
|
|
114
140
|
// Select only published format variants for this exact target distribution.
|
|
115
|
-
const names=releaseAssetNames(runtime.id,release.tag_name,platform,architecture);
|
|
141
|
+
const names=releaseAssetNames(runtime.id,release.tag_name,platform,architecture,backend);
|
|
116
142
|
const asset=release.assets?.find(function isSelectedAsset(candidate){return names.includes(candidate.name);});
|
|
117
143
|
if(!asset){
|
|
118
144
|
throw new ArcaneError(ERROR_CODES.targetUnavailable,`${runtime.id} ${release.tag_name} has no published ${platform}/${architecture} runtime archive${names.length?` named ${names.join(' or ')}`:''}.`);
|
|
119
145
|
}
|
|
120
|
-
|
|
146
|
+
let companion;
|
|
147
|
+
if(backend==='cuda'){
|
|
148
|
+
const companionName='cudart-llama-bin-win-cuda-13.4-x64.zip';
|
|
149
|
+
const companionAsset=release.assets?.find(function selectedCudaRuntime(candidate){return candidate.name===companionName;});
|
|
150
|
+
if(!companionAsset){
|
|
151
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable,`llama.cpp ${release.tag_name} has no matching CUDA runtime archive named ${companionName}.`);
|
|
152
|
+
}
|
|
153
|
+
companion={url:companionAsset.browser_download_url,name:companionAsset.name};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
version:release.tag_name,url:asset.browser_download_url,name:asset.name,
|
|
157
|
+
...(runtime.id==='llama.cpp'?{backend,requestedBackend:runtime.backend}:{}),
|
|
158
|
+
...(companion?{companion}:{})
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function llamaBackendCandidate(runtime,platform,architecture){
|
|
163
|
+
if(runtime.backend==='cuda'){
|
|
164
|
+
if(platform!=='win32'||architecture!=='x64'){
|
|
165
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable,`The selected llama.cpp CUDA distribution is unavailable for ${platform}/${architecture}.`);
|
|
166
|
+
}
|
|
167
|
+
return 'cuda';
|
|
168
|
+
}
|
|
169
|
+
// A custom automatic archive declares no backend capability. Official macOS
|
|
170
|
+
// archives include Metal; requested CPU execution is configured at launch.
|
|
171
|
+
if(runtime.url)return runtime.backend;
|
|
172
|
+
return platform==='darwin'?'metal':'cpu';
|
|
121
173
|
}
|
|
122
174
|
|
|
123
175
|
async function imageBackendCandidate(backend, platform, signal) {
|
|
@@ -368,6 +420,7 @@ async function installedRuntime(base,runtime,platform,architecture,signal,reuseW
|
|
|
368
420
|
const imageBackend = runtime.id === 'stable-diffusion.cpp' && !runtime.url
|
|
369
421
|
? await imageBackendCandidate(runtime.backend, platform, signal)
|
|
370
422
|
: runtime.backend;
|
|
423
|
+
const llamaBackend=runtime.id==='llama.cpp'?llamaBackendCandidate(runtime,platform,architecture):null;
|
|
371
424
|
for(const entry of entries){
|
|
372
425
|
throwIfAborted(signal);
|
|
373
426
|
if(!entry.isDirectory())continue;
|
|
@@ -380,6 +433,8 @@ async function installedRuntime(base,runtime,platform,architecture,signal,reuseW
|
|
|
380
433
|
}
|
|
381
434
|
if(installation.requestVersion!==(runtime.version??null)||installation.requestUrl!==(runtime.url??null))continue;
|
|
382
435
|
if (runtime.id === 'stable-diffusion.cpp' && installation.requestBackend !== runtime.backend) continue;
|
|
436
|
+
if(runtime.id==='llama.cpp'&&((installation.requestBackend??'auto')!==runtime.backend
|
|
437
|
+
||(installation.requestCompanionUrl??null)!==(runtime.companionUrl??null)))continue;
|
|
383
438
|
let whisperRequirement;
|
|
384
439
|
if (runtime.id === 'whisper.cpp') {
|
|
385
440
|
if (!installation.requestWhisper) continue;
|
|
@@ -388,6 +443,7 @@ async function installedRuntime(base,runtime,platform,architecture,signal,reuseW
|
|
|
388
443
|
}
|
|
389
444
|
const record=installation.runtime;
|
|
390
445
|
if(record?.id!==runtime.id||record.platform!==platform||record.architecture!==architecture)continue;
|
|
446
|
+
if(runtime.id==='llama.cpp'&&record.backend!==undefined&&record.backend!==llamaBackend)continue;
|
|
391
447
|
try{
|
|
392
448
|
if(runtime.id==='whisper.cpp'){
|
|
393
449
|
if(await installedWhisperRuntime(record,signal,!reuseWhisper))return reuseWhisper?{runtime:record,requirement:whisperRequirement}:record;
|
|
@@ -423,6 +479,8 @@ async function installedRuntime(base,runtime,platform,architecture,signal,reuseW
|
|
|
423
479
|
}
|
|
424
480
|
if(runtime.id==='onnx'){
|
|
425
481
|
if(!is.string(record.root)||!is.string(record.modulePath))continue;
|
|
482
|
+
const distribution=selectedONNXNativeDistribution(runtime,platform,architecture);
|
|
483
|
+
if(distribution&&record.nativeDistributionRevision!==distribution.revision)continue;
|
|
426
484
|
const locations=await onnxRuntimeModule(record.root,platform,architecture,signal);
|
|
427
485
|
return {...record,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
|
|
428
486
|
}
|
|
@@ -436,7 +494,11 @@ async function installedRuntime(base,runtime,platform,architecture,signal,reuseW
|
|
|
436
494
|
}
|
|
437
495
|
continue;
|
|
438
496
|
}
|
|
439
|
-
if((await stat(record.executable)).isFile())return {
|
|
497
|
+
if((await stat(record.executable)).isFile())return {
|
|
498
|
+
...record,
|
|
499
|
+
...(runtime.id==='llama.cpp'?{backend:record.backend??llamaBackend,requestedBackend:runtime.backend}:{}),
|
|
500
|
+
...(runtime.version?{requestedVersion:runtime.version}:{})
|
|
501
|
+
};
|
|
440
502
|
}catch(error){
|
|
441
503
|
if(runtime.id==='onnx'&&(error.code==='MODULE_NOT_FOUND'||error instanceof SyntaxError))continue;
|
|
442
504
|
if(error.code!=='ENOENT')throw error;
|
|
@@ -519,7 +581,7 @@ async function onnxRuntimeModule(root,platform,architecture,signal){
|
|
|
519
581
|
}
|
|
520
582
|
|
|
521
583
|
async function installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent}){
|
|
522
|
-
const version=runtime.version??
|
|
584
|
+
const version=runtime.version??onnxWindowsDistribution.version;
|
|
523
585
|
await onEvent({type:'local-ai.install.installing',message:`Installing onnxruntime-node ${version} for ${platform}/${architecture}.`,data:{id:runtime.id,version,platform,architecture}});
|
|
524
586
|
await mkdir(root,{recursive:true});
|
|
525
587
|
await writeFile(path.join(root,'package.json'),'{"private":true}\n',{flag:'wx',signal});
|
|
@@ -528,14 +590,34 @@ async function installONNXPackage(runtime,{root,directory,platform,architecture,
|
|
|
528
590
|
'--no-audit','--no-fund','--foreground-scripts','--registry=https://registry.npmjs.org/',
|
|
529
591
|
'--cache',path.join(directory,'npm-cache'),`onnxruntime-node@${runtime.url??version}`
|
|
530
592
|
],{cwd:root,env:{ONNXRUNTIME_NODE_INSTALL:'skip'},signal,onEvent});
|
|
531
|
-
|
|
593
|
+
const distribution=selectedONNXNativeDistribution(runtime,platform,architecture);
|
|
594
|
+
if(distribution){
|
|
595
|
+
const archive=path.join(path.dirname(root),distribution.asset);
|
|
596
|
+
const nativeDirectory=path.join(root,'node_modules','onnxruntime-node','bin','napi-v6',platform,architecture);
|
|
597
|
+
const url=`https://github.com/TheWizardNexus/arcane-os-sdk/releases/download/${packageMetadata.version}/${distribution.asset}`;
|
|
598
|
+
await onEvent({type:'local-ai.install.downloading',message:`Downloading ${distribution.asset}.`,data:{id:runtime.id,asset:distribution.asset,nativeDistributionRevision:distribution.revision}});
|
|
599
|
+
const response=await upstreamResponse(url,signal);
|
|
600
|
+
await pipeline(Readable.fromWeb(response.body),createWriteStream(archive,{flags:'wx'}),{signal});
|
|
601
|
+
await onEvent({type:'local-ai.install.extracting',message:`Preparing ${distribution.asset}.`,data:{id:runtime.id,asset:distribution.asset,nativeDistributionRevision:distribution.revision}});
|
|
602
|
+
// This is the new attempt's package. Running installations retain their
|
|
603
|
+
// original trees; the same-version JavaScript and binding stay intact.
|
|
604
|
+
await extractLocalAIArchive({archive,directory:nativeDirectory,signal,onEvent});
|
|
605
|
+
await unlink(archive);
|
|
606
|
+
}
|
|
607
|
+
const locations=await onnxRuntimeModule(root,platform,architecture,signal);
|
|
608
|
+
return {...locations,...(distribution?{nativeDistributionRevision:distribution.revision}:{})};
|
|
532
609
|
}
|
|
533
610
|
|
|
534
611
|
async function installRuntime(runtime,{directory,platform,architecture,signal,onEvent}){
|
|
535
612
|
const runtimeBase=path.join(directory,runtime.id,`${platform}-${architecture}`,encodeURIComponent(runtime.version??'default'));
|
|
536
|
-
const base=
|
|
613
|
+
const base=['stable-diffusion.cpp','llama.cpp'].includes(runtime.id)?path.join(runtimeBase,runtime.backend):runtimeBase;
|
|
537
614
|
await onEvent({type:'local-ai.install.starting',message:`Preparing ${runtime.id} for ${platform}/${architecture}.`,data:{id:runtime.id,platform,architecture}});
|
|
538
|
-
|
|
615
|
+
let installed=await installedRuntime(base,runtime,platform,architecture,signal);
|
|
616
|
+
// Previous default installations predate backend-specific directories. Keep
|
|
617
|
+
// those working trees reusable for the same automatic requirement.
|
|
618
|
+
if(!installed&&runtime.id==='llama.cpp'&&runtime.backend==='auto'){
|
|
619
|
+
installed=await installedRuntime(runtimeBase,runtime,platform,architecture,signal);
|
|
620
|
+
}
|
|
539
621
|
if(installed){
|
|
540
622
|
await onEvent({type:'local-ai.install.available',message:`${runtime.id} ${installed.version} is already installed.`,data:installed});
|
|
541
623
|
return installed;
|
|
@@ -550,6 +632,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
550
632
|
const attempt=await mkdtemp(path.join(base,'install-'));
|
|
551
633
|
const root=path.join(attempt,'runtime');
|
|
552
634
|
const archive=release&&runtime.id!=='stable-diffusion.cpp'?path.join(attempt,release.name):null;
|
|
635
|
+
const companionArchive=release?.companion?path.join(attempt,'companion',release.companion.name):null;
|
|
553
636
|
let completed=false;
|
|
554
637
|
try{
|
|
555
638
|
let locations;
|
|
@@ -563,19 +646,46 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
563
646
|
{root, directory, platform, architecture, signal, onEvent}
|
|
564
647
|
);
|
|
565
648
|
}else{
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
649
|
+
const assets=[{url:release.url,name:release.name,archive}];
|
|
650
|
+
if(companionArchive){
|
|
651
|
+
await mkdir(path.dirname(companionArchive));
|
|
652
|
+
assets.push({...release.companion,archive:companionArchive});
|
|
653
|
+
}
|
|
654
|
+
const downloads=await Promise.allSettled(assets.map(async function downloadRuntimeArchive(asset){
|
|
655
|
+
await onEvent({
|
|
656
|
+
type:'local-ai.install.downloading',
|
|
657
|
+
message:runtime.id==='llama.cpp'?`Downloading ${runtime.id} ${release.version}: ${asset.name}.`:`Downloading ${runtime.id} ${release.version}.`,
|
|
658
|
+
data:{id:runtime.id,version:release.version,platform,architecture,...(runtime.id==='llama.cpp'?{asset:asset.name}:{})}
|
|
659
|
+
});
|
|
660
|
+
const response=await upstreamResponse(asset.url,signal);
|
|
661
|
+
await pipeline(Readable.fromWeb(response.body),createWriteStream(asset.archive,{flags:'wx'}),{signal});
|
|
662
|
+
}));
|
|
663
|
+
const failures=downloads.filter(function archiveDownloadFailed(outcome){return outcome.status==='rejected';});
|
|
664
|
+
if(failures.length===1)throw failures[0].reason;
|
|
665
|
+
if(failures.length>1){
|
|
666
|
+
throw new AggregateError(failures.map(function archiveDownloadFailure(outcome){return outcome.reason;}),`Download of ${runtime.id} archives failed.`);
|
|
667
|
+
}
|
|
668
|
+
// The pair downloads independently, then extracts in order because
|
|
669
|
+
// both archives populate the same new runtime directory.
|
|
670
|
+
for(const asset of assets){
|
|
671
|
+
await onEvent({
|
|
672
|
+
type:'local-ai.install.extracting',
|
|
673
|
+
message:runtime.id==='llama.cpp'?`Extracting ${runtime.id} ${release.version}: ${asset.name}.`:`Extracting ${runtime.id} ${release.version}.`,
|
|
674
|
+
data:{id:runtime.id,version:release.version,...(runtime.id==='llama.cpp'?{asset:asset.name}:{})}
|
|
675
|
+
});
|
|
676
|
+
await extractLocalAIArchive({archive:asset.archive,directory:root,signal,onEvent});
|
|
677
|
+
}
|
|
678
|
+
locations={version:release.version,
|
|
679
|
+
...(runtime.id==='llama.cpp'?{backend:release.backend,requestedBackend:release.requestedBackend}:{}),
|
|
680
|
+
...(runtime.id==='nemo-speech'
|
|
572
681
|
?await nemoRuntimeDirectories(root,signal)
|
|
573
682
|
:{executable:await runtimeExecutable(root,runtime.id,platform,signal)})};
|
|
574
683
|
}
|
|
575
684
|
const record={id:runtime.id,platform,architecture,root,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
|
|
576
685
|
throwIfAborted(signal);
|
|
577
686
|
if(archive)await unlink(archive);
|
|
578
|
-
await
|
|
687
|
+
if(companionArchive)await unlink(companionArchive);
|
|
688
|
+
await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(['stable-diffusion.cpp','llama.cpp'].includes(runtime.id)?{requestBackend:runtime.backend}:{}),...(runtime.id==='llama.cpp'?{requestCompanionUrl:runtime.companionUrl??null}:{}),...(runtime.id==='whisper.cpp'?{requestWhisper:whisperRuntimeSelection(runtime)}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
|
|
579
689
|
completed=true;
|
|
580
690
|
await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${record.version} is available.`,data:record});
|
|
581
691
|
return record;
|
|
@@ -595,7 +705,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
595
705
|
|
|
596
706
|
function shareInstallation(runtime,options){
|
|
597
707
|
throwIfAborted(options.signal);
|
|
598
|
-
const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,
|
|
708
|
+
const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,['stable-diffusion.cpp','llama.cpp'].includes(runtime.id)?runtime.backend:null,runtime.id==='whisper.cpp'?whisperRuntimeSelection(runtime):null,runtime.id==='llama.cpp'?runtime.companionUrl??null:null]);
|
|
599
709
|
let entry=installations.get(key);
|
|
600
710
|
if(entry?.controller.signal.aborted){
|
|
601
711
|
// The prior cancelled attempt owns its cleanup. A later request waits
|