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.
Files changed (44) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/browser-runtime/ai/browser-decisions.mjs +31 -4
  3. package/browser-runtime/ai/core-local.mjs +105 -25
  4. package/browser-runtime/ai/decision-runtime.mjs +7 -0
  5. package/browser-runtime/ai/decision-worker.mjs +9 -1
  6. package/browser-runtime/core/client.mjs +6 -0
  7. package/browser-runtime/core/contracts.mjs +10 -0
  8. package/docs/reference/ai/browser-decisions.md +89 -7
  9. package/docs/reference/core-client.md +59 -0
  10. package/docs/reference/core-native-packaging.md +62 -10
  11. package/docs/reference/core-repositories.md +104 -8
  12. package/docs/reference/git-text-snapshot.md +16 -1
  13. package/docs/reference/inventory/package-api.json +25 -25
  14. package/docs/reference/inventory/runtime-components.json +1 -1
  15. package/docs/reference/inventory/runtime-modules.json +4 -4
  16. package/docs/reference/local-ai.md +108 -9
  17. package/docs/reference/mcp-stdio.md +76 -1
  18. package/docs/reference/native-app-control.md +17 -5
  19. package/docs/reference/protocols.md +14 -0
  20. package/docs/reference/runtime-modules.md +9 -3
  21. package/docs/reference/sdk-api.md +202 -35
  22. package/package.json +2 -2
  23. package/runtime/arcane/modules/RiskSignalAnalyzer.js +1 -5
  24. package/schemas/arcane-app.schema.json +13 -3
  25. package/schemas/arcane-package.schema.json +10 -1
  26. package/src/app-descriptor.mjs +6 -2
  27. package/src/cli/app-control.mjs +1 -1
  28. package/src/core/hosts/windows/ArcaneHost.cs +273 -5
  29. package/src/core/hosts/windows/ArcaneLauncher.cs +9 -0
  30. package/src/core/hosts/windows/ArcaneWindowControl.cs +7 -7
  31. package/src/core/repositories.mjs +17 -6
  32. package/src/core/services/local-ai.mjs +183 -22
  33. package/src/git-text-snapshot.mjs +4 -2
  34. package/src/installed-sdk-runtime.mjs +2 -0
  35. package/src/local-ai/config.mjs +16 -0
  36. package/src/local-ai/dev.mjs +2 -2
  37. package/src/local-ai/discover.mjs +3 -0
  38. package/src/local-ai/install.mjs +130 -20
  39. package/src/local-ai/llama-execution-windows.ps1 +158 -0
  40. package/src/local-ai/llama-execution.mjs +169 -0
  41. package/src/local-ai/server.mjs +38 -8
  42. package/src/native/portable-layout.mjs +13 -3
  43. package/src/repository.mjs +33 -8
  44. 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
- await engine.server.close();
341
- await Promise.allSettled([...engine.loads.values()]);
342
- lifetimeSignal.throwIfAborted();
343
- engine.models = [];
344
- engine.modelChanges.clear();
345
- engine.revision = 0;
346
- engine.released = false;
347
- startEngine(engine);
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 loadLlama({model}, request) {
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.controller.abort();
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', '--', remote, `+${ref}:${snapshotRef}`], {onEvent: events.send});
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
  });
@@ -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};
@@ -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 selectedONNXRuntime(requirement) {
51
- return requirement.id === 'onnx';
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';