arcane-os 0.88.1 → 0.90.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 +34 -0
- 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/core-client.md +59 -0
- package/docs/reference/core-native-packaging.md +62 -10
- package/docs/reference/core-repositories.md +104 -8
- package/docs/reference/git-text-snapshot.md +16 -1
- package/docs/reference/inventory/package-api.json +25 -25
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +4 -4
- package/docs/reference/local-ai.md +108 -9
- 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 +202 -35
- 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 +17 -6
- package/src/core/services/local-ai.mjs +183 -22
- package/src/git-text-snapshot.mjs +4 -2
- 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/repository.mjs +33 -8
- 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; });
|
|
@@ -5,6 +5,7 @@ import {runProcess} from './process.mjs';
|
|
|
5
5
|
import {ArcaneError, ERROR_CODES, throwIfAborted} from './errors.mjs';
|
|
6
6
|
import {createEventQueue} from './event-queue.mjs';
|
|
7
7
|
import {createGitIdentityRunner} from './git-identity.mjs';
|
|
8
|
+
import {captureRepositoryRemote} from './repository.mjs';
|
|
8
9
|
|
|
9
10
|
const is = new Is(false);
|
|
10
11
|
const snapshotRef = 'refs/arcane/text-snapshot';
|
|
@@ -14,11 +15,12 @@ const snapshotRef = 'refs/arcane/text-snapshot';
|
|
|
14
15
|
* One application service owns each dedicated cache directory. Construction
|
|
15
16
|
* performs no I/O; refresh coalesces callers and close drains accepted work.
|
|
16
17
|
*/
|
|
17
|
-
export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
18
|
+
export function createGitTextSnapshot({cacheDirectory, remote, remoteBase, ref, selectPath, gitIdentity, onEvent, run = runProcess} = {}) {
|
|
18
19
|
for (const [name, value] of Object.entries({cacheDirectory, remote, ref})) {
|
|
19
20
|
if (!is.string(value) || value === '') throw new TypeError(`${name} must be a nonempty string.`);
|
|
20
21
|
}
|
|
21
22
|
if (!is.function(selectPath)) throw new TypeError('selectPath must be a function.');
|
|
23
|
+
const remoteArgument = captureRepositoryRemote(remote, remoteBase);
|
|
22
24
|
const execute = createGitIdentityRunner(run, gitIdentity);
|
|
23
25
|
const directory = path.resolve(cacheDirectory);
|
|
24
26
|
let prepared = false;
|
|
@@ -68,7 +70,7 @@ export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath,
|
|
|
68
70
|
try {
|
|
69
71
|
await events.send({type: 'git.snapshot.refreshing', message: 'Refreshing the selected repository revision.', data: {ref}});
|
|
70
72
|
await prepare(events);
|
|
71
|
-
await git(['fetch', '--no-tags', '--no-write-fetch-head', '--',
|
|
73
|
+
await git(['fetch', '--no-tags', '--no-write-fetch-head', '--', remoteArgument, `+${ref}:${snapshotRef}`], {onEvent: events.send});
|
|
72
74
|
const selected = await git(['rev-parse', '--verify', `${snapshotRef}^{commit}`], {onEvent: events.send});
|
|
73
75
|
const revision = selected.stdout.trim();
|
|
74
76
|
if (retained?.revision === revision) {
|
|
@@ -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';
|