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
@@ -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):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||(runtime.id==='stable-diffusion.cpp'&&prior.backend!==runtime.backend))){
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 {version:runtime.version??'custom',url:runtime.url,name};
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
- return {version:release.tag_name,url:asset.browser_download_url,name:asset.name};
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 {...record,...(runtime.version?{requestedVersion:runtime.version}:{})};
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??'1.30.0';
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
- return onnxRuntimeModule(root,platform,architecture,signal);
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=runtime.id==='stable-diffusion.cpp'?path.join(runtimeBase,runtime.backend):runtimeBase;
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
- const installed=await installedRuntime(base,runtime,platform,architecture,signal);
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
- await onEvent({type:'local-ai.install.downloading',message:`Downloading ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version,platform,architecture}});
567
- const response=await upstreamResponse(release.url,signal);
568
- await pipeline(Readable.fromWeb(response.body),createWriteStream(archive,{flags:'wx'}),{signal});
569
- await onEvent({type:'local-ai.install.extracting',message:`Extracting ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version}});
570
- await extractLocalAIArchive({archive,directory:root,signal,onEvent});
571
- locations={version:release.version,...(runtime.id==='nemo-speech'
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 writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),...(runtime.id==='whisper.cpp'?{requestWhisper:whisperRuntimeSelection(runtime)}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
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,runtime.id==='stable-diffusion.cpp'?runtime.backend:null,runtime.id==='whisper.cpp'?whisperRuntimeSelection(runtime):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
@@ -0,0 +1,158 @@
1
+ # Invoked through stdin by the SDK's owned process; stdout contains one JSON record.
2
+ $ErrorActionPreference = 'Stop'
3
+ $ProgressPreference = 'SilentlyContinue'
4
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
5
+
6
+ try {
7
+ # Enumerate the installed driver's devices in this helper only. An empty
8
+ # CUDA_VISIBLE_DEVICES value hides every device, so remove the inherited value.
9
+ [Environment]::SetEnvironmentVariable('CUDA_VISIBLE_DEVICES', $null, [EnvironmentVariableTarget]::Process)
10
+
11
+ Add-Type -TypeDefinition @'
12
+ using System;
13
+ using System.Globalization;
14
+ using System.Runtime.InteropServices;
15
+ using System.Text;
16
+
17
+ namespace Arcane.LocalAI {
18
+ [StructLayout(LayoutKind.Sequential)]
19
+ public struct AdapterLuid {
20
+ public uint LowPart;
21
+ public int HighPart;
22
+ }
23
+
24
+ public sealed class CudaDeviceIdentity {
25
+ public string Uuid;
26
+ public uint NodeMask;
27
+ public AdapterLuid Luid;
28
+ public string UuidFunction;
29
+
30
+ // NVIDIA CU_DEVICE_ATTRIBUTE_TCC_DRIVER is 35. CUDA's Windows LUID
31
+ // contract is undefined in TCC mode, so only WDDM devices can be matched.
32
+ // https://nvidia.github.io/cuda-python/cuda-bindings/latest/module/driver.html
33
+ private const int TccDriverAttribute = 35;
34
+
35
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
36
+ private static extern int cuInit(uint flags);
37
+
38
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
39
+ private static extern int cuDeviceGetCount(out int count);
40
+
41
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
42
+ private static extern int cuDeviceGet(out int device, int ordinal);
43
+
44
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
45
+ private static extern int cuDeviceGetAttribute(out int value, int attribute, int device);
46
+
47
+ // CUDA writes the native Windows LUID layout: unsigned LowPart followed
48
+ // by signed HighPart. Marshaling the struct preserves that native layout.
49
+ // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-luid
50
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
51
+ private static extern int cuDeviceGetLuid(out AdapterLuid luid, out uint nodeMask, int device);
52
+
53
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
54
+ private static extern int cuDeviceGetUuid_v2([Out] byte[] uuid, int device);
55
+
56
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
57
+ private static extern int cuDeviceGetUuid([Out] byte[] uuid, int device);
58
+
59
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
60
+ private static extern int cuGetErrorName(int error, out IntPtr name);
61
+
62
+ [DllImport("nvcuda.dll", ExactSpelling = true)]
63
+ private static extern int cuGetErrorString(int error, out IntPtr message);
64
+
65
+ private static void Check(int result, string operation) {
66
+ if (result == 0) return;
67
+ IntPtr name;
68
+ IntPtr message;
69
+ int nameResult = cuGetErrorName(result, out name);
70
+ int messageResult = cuGetErrorString(result, out message);
71
+ throw new InvalidOperationException(
72
+ operation + " returned CUDA error " + result + ": " +
73
+ (nameResult == 0 ? Marshal.PtrToStringAnsi(name) : "cuGetErrorName returned " + nameResult) + "; " +
74
+ (messageResult == 0 ? Marshal.PtrToStringAnsi(message) : "cuGetErrorString returned " + messageResult)
75
+ );
76
+ }
77
+
78
+ private static string ReadUuid(int device, out string function) {
79
+ // CUuuid's 16 native octets are unavoidable ABI storage. CUDA UUID
80
+ // text keeps their order; System.Guid would reverse several fields.
81
+ byte[] uuid = new byte[16];
82
+ int result;
83
+ try {
84
+ result = cuDeviceGetUuid_v2(uuid, device);
85
+ function = "cuDeviceGetUuid_v2";
86
+ } catch (EntryPointNotFoundException) {
87
+ // Older drivers expose the original function for this same
88
+ // device. No device or backend selection changes on this path.
89
+ result = cuDeviceGetUuid(uuid, device);
90
+ function = "cuDeviceGetUuid";
91
+ }
92
+ Check(result, function);
93
+ StringBuilder text = new StringBuilder("GPU-");
94
+ for (int index = 0; index < uuid.Length; index++) {
95
+ if (index == 4 || index == 6 || index == 8 || index == 10) text.Append('-');
96
+ text.Append(uuid[index].ToString("x2", CultureInfo.InvariantCulture));
97
+ }
98
+ return text.ToString();
99
+ }
100
+
101
+ public static CudaDeviceIdentity Resolve(uint lowPart, int highPart) {
102
+ Check(cuInit(0), "cuInit");
103
+ int count;
104
+ Check(cuDeviceGetCount(out count), "cuDeviceGetCount");
105
+ CudaDeviceIdentity matched = null;
106
+ int tccCount = 0;
107
+ for (int ordinal = 0; ordinal < count; ordinal++) {
108
+ int device;
109
+ Check(cuDeviceGet(out device, ordinal), "cuDeviceGet(" + ordinal + ")");
110
+ int tcc;
111
+ Check(cuDeviceGetAttribute(out tcc, TccDriverAttribute, device), "cuDeviceGetAttribute(TCC_DRIVER, " + ordinal + ")");
112
+ if (tcc != 0) {
113
+ tccCount++;
114
+ continue;
115
+ }
116
+ AdapterLuid luid;
117
+ uint nodeMask;
118
+ Check(cuDeviceGetLuid(out luid, out nodeMask, device), "cuDeviceGetLuid(" + ordinal + ")");
119
+ if (luid.LowPart != lowPart || luid.HighPart != highPart) continue;
120
+ if (matched != null) {
121
+ throw new InvalidOperationException("Multiple CUDA devices share the selected adapter LUID; the catalog selection cannot identify one CUDA device.");
122
+ }
123
+ string function;
124
+ string uuid = ReadUuid(device, out function);
125
+ matched = new CudaDeviceIdentity { Uuid = uuid, NodeMask = nodeMask, Luid = luid, UuidFunction = function };
126
+ }
127
+ if (matched == null) {
128
+ throw new InvalidOperationException(
129
+ "No WDDM CUDA device matches the selected adapter LUID. CUDA devices enumerated: " + count +
130
+ "; TCC devices without a defined LUID: " + tccCount + "."
131
+ );
132
+ }
133
+ return matched;
134
+ }
135
+ }
136
+ }
137
+ '@
138
+
139
+ $lowPart = [uint32]::Parse($env:ARCANE_LLAMA_LUID_LOW, [Globalization.CultureInfo]::InvariantCulture)
140
+ $highPart = [int32]::Parse($env:ARCANE_LLAMA_LUID_HIGH, [Globalization.CultureInfo]::InvariantCulture)
141
+ $device = [Arcane.LocalAI.CudaDeviceIdentity]::Resolve($lowPart, $highPart)
142
+ [ordered]@{
143
+ uuid = $device.Uuid
144
+ nodeMask = $device.NodeMask
145
+ adapterLuid = [ordered]@{
146
+ lowPart = $device.Luid.LowPart
147
+ highPart = $device.Luid.HighPart
148
+ }
149
+ driverMode = 'wddm'
150
+ uuidFunction = $device.UuidFunction
151
+ } | ConvertTo-Json -Depth 4 -Compress
152
+ } catch {
153
+ [Console]::Error.WriteLine($_.Exception.ToString())
154
+ if ($null -ne $_.ErrorDetails) { [Console]::Error.WriteLine($_.ErrorDetails.Message) }
155
+ if ($null -ne $_.InvocationInfo) { [Console]::Error.WriteLine($_.InvocationInfo.PositionMessage) }
156
+ if ($_.ScriptStackTrace) { [Console]::Error.WriteLine($_.ScriptStackTrace) }
157
+ exit 1
158
+ }
@@ -0,0 +1,169 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import Is from 'strong-type';
4
+ import {ArcaneError, ERROR_CODES, throwIfAborted} from '../errors.mjs';
5
+ import {runProcess} from '../process.mjs';
6
+
7
+ const is = new Is(false);
8
+
9
+ /**
10
+ * Resolve one explicit catalog selection for a new native llama server lifetime.
11
+ * Default selection performs no discovery. Hardware identity and launch options
12
+ * are configuration evidence; the server's observed execution target stays null.
13
+ */
14
+ export async function resolveLlamaExecution({
15
+ executionTarget, executionDevices, runtime, signal, onEvent
16
+ } = {}) {
17
+ throwIfAborted(signal);
18
+ if (executionTarget === undefined || executionTarget === null) {
19
+ const cpu = runtime?.requestedBackend === 'cpu';
20
+ return {
21
+ args: cpu ? ['--device', 'none', '--gpu-layers', '0'] : [],
22
+ env: {},
23
+ execution: {
24
+ requestedTarget: null,
25
+ resolvedDevice: null,
26
+ resolution: 'automatic',
27
+ reason: cpu ? 'runtime-backend-selected' : 'engine-default-required',
28
+ configuredTarget: cpu ? {backend: 'cpu', device: 'none', gpuLayers: 0} : null,
29
+ observedTarget: null,
30
+ fallback: false
31
+ }
32
+ };
33
+ }
34
+ if (!is.function(executionDevices?.resolveTarget)) {
35
+ throw new ArcaneError(
36
+ 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED',
37
+ 'Explicit llama.cpp device selection requires the host execution-device catalog.',
38
+ {details: {requestedTarget: executionTarget}}
39
+ );
40
+ }
41
+ // The persistent device ID is resolved again for this load because a Windows
42
+ // adapter LUID belongs to the current host session, not the saved selection.
43
+ const resolution = await executionDevices.resolveTarget(
44
+ {executionTarget, refresh: true, signal}
45
+ );
46
+ throwIfAborted(signal);
47
+ const execution = {
48
+ ...resolution,
49
+ configuredTarget: null,
50
+ observedTarget: null,
51
+ fallback: false
52
+ };
53
+ if (resolution.resolution !== 'matched' || !resolution.resolvedDevice) {
54
+ throw new ArcaneError(
55
+ resolution.resolution === 'unsupported'
56
+ ? 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED'
57
+ : 'LOCAL_AI_EXECUTION_TARGET_UNAVAILABLE',
58
+ `The selected llama.cpp execution device could not be resolved: ${resolution.reason}.`,
59
+ {details: execution}
60
+ );
61
+ }
62
+ const device = resolution.resolvedDevice;
63
+ if (device.kind === 'cpu') {
64
+ execution.configuredTarget = {
65
+ backend: 'cpu', deviceId: device.deviceId, device: 'none', gpuLayers: 0
66
+ };
67
+ return {args: ['--device', 'none', '--gpu-layers', '0'], env: {}, execution};
68
+ }
69
+ if (device.kind !== 'gpu' || process.platform !== 'win32') {
70
+ throw new ArcaneError(
71
+ 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED',
72
+ `Exact llama.cpp ${device.kind} selection is unavailable on ${process.platform}.`,
73
+ {details: execution}
74
+ );
75
+ }
76
+ if (runtime?.requestedBackend === 'cpu') {
77
+ throw new ArcaneError(
78
+ 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED',
79
+ 'The selected GPU is incompatible with the explicit llama.cpp CPU runtime configuration.',
80
+ {details: execution}
81
+ );
82
+ }
83
+ if (runtime?.backend !== 'cuda') {
84
+ throw new ArcaneError(
85
+ 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED',
86
+ 'Exact Windows GPU selection requires a llama.cpp CUDA runtime.',
87
+ {details: {...execution, runtimeBackend: runtime?.backend ?? null}}
88
+ );
89
+ }
90
+ const adapterLuid = device.addresses?.adapterLuid;
91
+ if (!is.integer(adapterLuid?.lowPart) || adapterLuid.lowPart < 0 || adapterLuid.lowPart > 0xffffffff
92
+ || !is.integer(adapterLuid?.highPart) || adapterLuid.highPart < -0x80000000 || adapterLuid.highPart > 0x7fffffff) {
93
+ throw new ArcaneError(
94
+ 'LOCAL_AI_EXECUTION_TARGET_UNSUPPORTED',
95
+ 'The selected Windows GPU has no current adapter LUID for CUDA identity resolution.',
96
+ {details: execution}
97
+ );
98
+ }
99
+ const identity = await resolveWindowsCudaIdentity({adapterLuid, signal, onEvent});
100
+ throwIfAborted(signal);
101
+ execution.configuredTarget = {
102
+ backend: 'cuda',
103
+ deviceId: device.deviceId,
104
+ device: 'CUDA0',
105
+ uuid: identity.uuid,
106
+ nodeMask: identity.nodeMask,
107
+ adapterLuid: identity.adapterLuid,
108
+ driverMode: identity.driverMode,
109
+ identitySource: 'nvidia-cuda-driver',
110
+ uuidFunction: identity.uuidFunction,
111
+ splitMode: 'none',
112
+ mainGpu: 0,
113
+ gpuLayers: 'all'
114
+ };
115
+ if (identity.diagnostics !== '') {
116
+ execution.diagnostics = [{source: 'cuda-driver', message: identity.diagnostics}];
117
+ }
118
+ // A single full UUID makes CUDA0 identify the selected physical GPU in the
119
+ // fresh server process, independent of DXGI or inherited CUDA ordinal order.
120
+ return {
121
+ args: ['--device', 'CUDA0', '--split-mode', 'none', '--main-gpu', '0', '--gpu-layers', 'all'],
122
+ env: {CUDA_VISIBLE_DEVICES: identity.uuid},
123
+ execution
124
+ };
125
+ }
126
+
127
+ async function resolveWindowsCudaIdentity({adapterLuid, signal, onEvent}) {
128
+ const script = await readFile(
129
+ new URL('./llama-execution-windows.ps1', import.meta.url),
130
+ {encoding: 'utf8', signal}
131
+ );
132
+ throwIfAborted(signal);
133
+ const command = process.env.SystemRoot
134
+ ? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
135
+ : 'powershell.exe';
136
+ async function* identityInput() {
137
+ yield `& {\n${script}\n}\n\n`;
138
+ }
139
+ const result = await runProcess(
140
+ command,
141
+ ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '-'],
142
+ {
143
+ signal,
144
+ onEvent,
145
+ env: {
146
+ ARCANE_LLAMA_LUID_LOW: String(adapterLuid.lowPart),
147
+ ARCANE_LLAMA_LUID_HIGH: String(adapterLuid.highPart)
148
+ },
149
+ input: identityInput(),
150
+ captureOutput: true,
151
+ emitOutputEvents: false
152
+ }
153
+ );
154
+ throwIfAborted(signal);
155
+ let identity;
156
+ try {
157
+ identity = JSON.parse(result.stdout);
158
+ if (!is.string(identity?.uuid) || identity.uuid === '' || !is.integer(identity.nodeMask)) {
159
+ throw new TypeError('The CUDA driver identity helper returned no device identity.');
160
+ }
161
+ } catch (error) {
162
+ throw new ArcaneError(
163
+ ERROR_CODES.operationFailed,
164
+ 'Could not read the selected CUDA device identity.',
165
+ {cause: error, details: result}
166
+ );
167
+ }
168
+ return {...identity, diagnostics: result.stderr};
169
+ }