llm-checker 3.5.3 → 3.5.6

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/README.md CHANGED
@@ -40,7 +40,7 @@ Choosing the right LLM for your hardware is complex. With thousands of model var
40
40
  |:---:|---|---|
41
41
  | **200+** | Dynamic Model Pool | Uses full scraped Ollama catalog/variants when available (with curated fallback) |
42
42
  | **4D** | Scoring Engine | Quality, Speed, Fit, Context — weighted by use case |
43
- | **Multi-GPU** | Hardware Detection | Apple Silicon, NVIDIA CUDA, AMD ROCm, Intel Arc, CPU |
43
+ | **Multi-GPU** | Hardware Detection | Apple Silicon, NVIDIA CUDA, AMD ROCm, Intel Arc, CPU, integrated/dedicated inventory visibility |
44
44
  | **Calibrated** | Memory Estimation | Bytes-per-parameter formula validated against real Ollama sizes |
45
45
  | **Zero** | Native Dependencies | Pure JavaScript — works on any Node.js 16+ system |
46
46
  | **Optional** | SQLite Search | Install `sql.js` to unlock `sync`, `search`, and `smart-recommend` |
@@ -82,6 +82,13 @@ npm install -g llm-checker
82
82
  npx llm-checker hw-detect
83
83
  ```
84
84
 
85
+ **Termux (Android):**
86
+ ```bash
87
+ pkg update
88
+ pkg install ollama
89
+ npm install -g llm-checker
90
+ ```
91
+
85
92
  **Requirements:**
86
93
  - Node.js 16+ (any version: 16, 18, 20, 22, 24)
87
94
  - [Ollama](https://ollama.ai) installed for running models
@@ -508,6 +515,16 @@ Metal:
508
515
  Memory Bandwidth: 273GB/s
509
516
  ```
510
517
 
518
+ On hybrid or integrated-only systems, `hw-detect` now also surfaces GPU topology explicitly:
519
+
520
+ ```
521
+ Dedicated GPUs: NVIDIA GeForce RTX 4060
522
+ Integrated GPUs: Intel Iris Xe Graphics
523
+ Assist path: Integrated/shared-memory GPU detected, runtime remains CPU
524
+ ```
525
+
526
+ This makes integrated GPUs visible even when the selected runtime backend is still CPU.
527
+
511
528
  ### `recommend` — Category Recommendations
512
529
 
513
530
  ```bash
@@ -531,19 +548,24 @@ Hardware Tier: HIGH | Models Analyzed: 205
531
548
  Coding:
532
549
  qwen2.5-coder:14b (14B)
533
550
  Score: 78/100
551
+ Fine-tuning: LoRA+QLoRA
534
552
  Command: ollama pull qwen2.5-coder:14b
535
553
 
536
554
  Reasoning:
537
555
  deepseek-r1:14b (14B)
538
556
  Score: 86/100
557
+ Fine-tuning: QLoRA
539
558
  Command: ollama pull deepseek-r1:14b
540
559
 
541
560
  Multimodal:
542
561
  llama3.2-vision:11b (11B)
543
562
  Score: 83/100
563
+ Fine-tuning: LoRA+QLoRA
544
564
  Command: ollama pull llama3.2-vision:11b
545
565
  ```
546
566
 
567
+ `check`, `recommend`, and `ai-check` include a fine-tuning suitability label in output to help choose between Full FT, LoRA, and QLoRA paths.
568
+
547
569
  ### `search` — Model Search
548
570
 
549
571
  ```bash
@@ -16,6 +16,7 @@ function getLLMChecker() {
16
16
  const { getLogger } = require('../src/utils/logger');
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const { normalizePlatform, isTermuxEnvironment } = require('../src/utils/platform');
19
20
  const {
20
21
  SUPPORTED_RUNTIMES,
21
22
  normalizeRuntime,
@@ -23,6 +24,7 @@ const {
23
24
  getRuntimeDisplayName,
24
25
  getRuntimeCommandSet
25
26
  } = require('../src/runtime/runtime-support');
27
+ const { evaluateFineTuningSupport } = require('../src/models/fine-tuning-support');
26
28
  const { CalibrationManager } = require('../src/calibration/calibration-manager');
27
29
  const { SUPPORTED_CALIBRATION_OBJECTIVES } = require('../src/calibration/schemas');
28
30
  const {
@@ -512,8 +514,23 @@ if (!program.commands.some((cmd) => cmd.name() === 'help')) {
512
514
 
513
515
  // Ollama installation helper
514
516
  function getOllamaInstallInstructions() {
515
- const platform = os.platform();
517
+ const rawPlatform = os.platform();
518
+ const platform = normalizePlatform(rawPlatform);
516
519
  const arch = os.arch();
520
+
521
+ if (isTermuxEnvironment(rawPlatform, process.env)) {
522
+ return {
523
+ name: `Termux (Android${arch ? ` ${arch}` : ''})`,
524
+ downloadUrl: 'https://github.com/termux/termux-packages',
525
+ instructions: [
526
+ '1. Update Termux packages: pkg update',
527
+ '2. Install Ollama from the Termux repository: pkg install ollama',
528
+ '3. Start Ollama in the current shell: ollama serve',
529
+ '4. In a new Termux session, test with: ollama run llama3.2:1b'
530
+ ],
531
+ alternativeInstall: 'pkg install ollama'
532
+ };
533
+ }
517
534
 
518
535
  const instructions = {
519
536
  'darwin': {
@@ -545,8 +562,8 @@ function getOllamaInstallInstructions() {
545
562
  '1. Review official installation options:',
546
563
  ' https://github.com/ollama/ollama/blob/main/docs/linux.md',
547
564
  '2. Prefer a package manager (apt/dnf/pacman) when available',
548
- '3. Start service after install:',
549
- ' sudo systemctl start ollama',
565
+ '3. Start Ollama after install:',
566
+ ' ollama serve',
550
567
  '4. Test with: ollama run llama2:7b'
551
568
  ],
552
569
  alternativeInstall: 'Manual install: https://github.com/ollama/ollama/blob/main/docs/linux.md'
@@ -855,6 +872,13 @@ function calculateModelCompatibilityScore(model, hardware) {
855
872
  return Math.max(0, Math.min(100, Math.round(score)));
856
873
  }
857
874
 
875
+ function formatGpuInventoryList(models = []) {
876
+ if (!Array.isArray(models) || models.length === 0) return 'None';
877
+ return models
878
+ .map(({ name, count }) => (count > 1 ? `${count}x ${name}` : name))
879
+ .join(' + ');
880
+ }
881
+
858
882
  // Helper function to get hardware tier for display
859
883
  function getHardwareTierForDisplay(hardware) {
860
884
  const ram = hardware.memory.total;
@@ -862,9 +886,15 @@ function getHardwareTierForDisplay(hardware) {
862
886
  const gpuModel = hardware.gpu?.model || '';
863
887
  const vramGB = hardware.gpu?.vram || 0;
864
888
 
865
- // Check if it's integrated GPU (should cap tier)
866
- const isIntegratedGPU = /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(gpuModel);
867
- const hasDedicatedGPU = vramGB > 0 && !isIntegratedGPU;
889
+ const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
890
+ ? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
891
+ : '';
892
+ const isIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
893
+ ? hardware.summary.hasIntegratedGPU
894
+ : /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(`${gpuModel} ${integratedGpuInventory}`);
895
+ const hasDedicatedGPU = typeof hardware.summary?.hasDedicatedGPU === 'boolean'
896
+ ? hardware.summary.hasDedicatedGPU
897
+ : (vramGB > 0 && !isIntegratedGPU);
868
898
  const isAppleSilicon = process.platform === 'darwin' && (gpuModel.toLowerCase().includes('apple') || gpuModel.toLowerCase().includes('m1') || gpuModel.toLowerCase().includes('m2') || gpuModel.toLowerCase().includes('m3') || gpuModel.toLowerCase().includes('m4'));
869
899
 
870
900
  // Base tier calculation
@@ -931,6 +961,8 @@ function displaySystemInfo(hardware, analysis) {
931
961
  const cpuColor = hardware.cpu.cores >= 8 ? chalk.green : hardware.cpu.cores >= 4 ? chalk.yellow : chalk.red;
932
962
  const ramColor = hardware.memory.total >= 32 ? chalk.green : hardware.memory.total >= 16 ? chalk.yellow : chalk.red;
933
963
  const gpuColor = hardware.gpu.dedicated ? chalk.green : chalk.hex('#FFA500');
964
+ const integratedList = formatGpuInventoryList(hardware.gpu.integratedGpuModels || hardware.summary?.integratedGpuModels);
965
+ const dedicatedList = formatGpuInventoryList(hardware.gpu.dedicatedGpuModels || hardware.summary?.dedicatedGpuModels);
934
966
 
935
967
  const lines = [
936
968
  `${chalk.cyan('CPU:')} ${cpuColor(hardware.cpu.brand)} ${chalk.gray(`(${hardware.cpu.cores} cores, ${hardware.cpu.speed}GHz)`)}`,
@@ -938,6 +970,8 @@ function displaySystemInfo(hardware, analysis) {
938
970
  `${chalk.cyan('RAM:')} ${ramColor(hardware.memory.total + 'GB')}`,
939
971
  `${chalk.cyan('GPU:')} ${gpuColor(hardware.gpu.model || 'Not detected')}`,
940
972
  `${chalk.cyan('VRAM:')} ${hardware.gpu.vram === 0 && hardware.gpu.model && hardware.gpu.model.toLowerCase().includes('apple') ? 'Unified Memory' : `${hardware.gpu.vram || 'N/A'}GB`}${hardware.gpu.dedicated ? chalk.green(' (Dedicated)') : chalk.hex('#FFA500')(' (Integrated)')}`,
973
+ `${chalk.cyan('Dedicated GPUs:')} ${chalk.green(dedicatedList)}`,
974
+ `${chalk.cyan('Integrated GPUs:')} ${chalk.hex('#FFA500')(integratedList)}`,
941
975
  ];
942
976
 
943
977
  const tier = analysis.summary.hardwareTier?.replace('_', ' ').toUpperCase() || 'UNKNOWN';
@@ -1203,7 +1237,7 @@ function displayLegacyRecommendations(recommendations) {
1203
1237
  console.log(chalk.cyan('╰'));
1204
1238
  }
1205
1239
 
1206
- function displayIntelligentRecommendations(intelligentData) {
1240
+ function displayIntelligentRecommendations(intelligentData, hardware = null) {
1207
1241
  if (!intelligentData || !intelligentData.summary) return;
1208
1242
 
1209
1243
  const { summary, recommendations } = intelligentData;
@@ -1220,10 +1254,12 @@ function displayIntelligentRecommendations(intelligentData) {
1220
1254
  // Mostrar mejor modelo general
1221
1255
  if (summary.best_overall) {
1222
1256
  const best = summary.best_overall;
1257
+ const bestFineTuning = evaluateFineTuningSupport(best, hardware || {});
1223
1258
  console.log(chalk.red('│') + ` ${chalk.bold.yellow('BEST OVERALL:')} ${chalk.green.bold(best.name)}`);
1224
1259
  console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(best.command)}`);
1225
1260
  console.log(chalk.red('│') + ` Score: ${chalk.yellow.bold(best.score)}/100 | Category: ${chalk.magenta(best.category)}`);
1226
1261
  console.log(chalk.red('│') + ` Quantization: ${chalk.white.bold(best.quantization || 'Q4_K_M')}`);
1262
+ console.log(chalk.red('│') + ` Fine-tuning: ${chalk.blue.bold(bestFineTuning.shortLabel)}`);
1227
1263
  console.log(chalk.red('│'));
1228
1264
  }
1229
1265
 
@@ -1242,11 +1278,13 @@ function displayIntelligentRecommendations(intelligentData) {
1242
1278
  const icon = categories[category] || 'Other';
1243
1279
  const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
1244
1280
  const scoreColor = getScoreColor(model.score);
1281
+ const fineTuningSupport = evaluateFineTuningSupport(model, hardware || {});
1245
1282
 
1246
1283
  console.log(chalk.red('│') + ` ${chalk.bold.white(categoryName)} (${icon}):`);
1247
1284
  console.log(chalk.red('│') + ` ${chalk.green(model.name)} (${model.size})`);
1248
1285
  console.log(chalk.red('│') + ` Score: ${scoreColor.bold(model.score)}/100 | Pulls: ${chalk.gray(model.pulls?.toLocaleString() || 'N/A')}`);
1249
1286
  console.log(chalk.red('│') + ` Quantization: ${chalk.white.bold(model.quantization || 'Q4_K_M')}`);
1287
+ console.log(chalk.red('│') + ` Fine-tuning: ${chalk.blue.bold(fineTuningSupport.shortLabel)}`);
1250
1288
  console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(model.command)}`);
1251
1289
  console.log(chalk.red('│'));
1252
1290
  });
@@ -2079,6 +2117,8 @@ async function displayModelRecommendations(analysis, hardware, useCase = 'genera
2079
2117
  const realSize = getRealSizeFromOllamaCache(model) || estimateModelSize(model);
2080
2118
  console.log(`Size: ${chalk.white(realSize)}`);
2081
2119
  console.log(`Compatibility Score: ${chalk.green.bold(model.adjustedScore || model.score || 'N/A')}/100`);
2120
+ const fineTuningSupport = evaluateFineTuningSupport(model, hardware);
2121
+ console.log(`Fine-tuning: ${chalk.blue.bold(fineTuningSupport.shortLabel)}`);
2082
2122
 
2083
2123
  if (index === 0) {
2084
2124
  console.log(`Reason: ${chalk.gray(reason)}`);
@@ -3628,7 +3668,7 @@ Calibrated routing examples:
3628
3668
  displaySystemInfo(hardware, { summary: { hardwareTier: intelligentRecommendations.summary.hardware_tier } });
3629
3669
 
3630
3670
  // Mostrar recomendaciones
3631
- displayIntelligentRecommendations(intelligentRecommendations);
3671
+ displayIntelligentRecommendations(intelligentRecommendations, hardware);
3632
3672
  displayCalibratedRoutingDecision('recommend', calibratedPolicy, routeDecision, routingPreference.warnings);
3633
3673
 
3634
3674
  if (policyConfig && policyEvaluation && policyEnforcement) {
@@ -5019,6 +5059,11 @@ program
5019
5059
  console.log(` Tier: ${chalk.cyan(detector.getHardwareTier().replace('_', ' ').toUpperCase())}`);
5020
5060
  console.log(` Max model size: ${chalk.green(detector.getMaxModelSize() + 'GB')}`);
5021
5061
  console.log(` Best backend: ${chalk.cyan(hardware.summary.bestBackend)}`);
5062
+ console.log(` Dedicated GPUs: ${chalk.green(formatGpuInventoryList(hardware.summary.dedicatedGpuModels))}`);
5063
+ console.log(` Integrated GPUs: ${chalk.hex('#FFA500')(formatGpuInventoryList(hardware.summary.integratedGpuModels))}`);
5064
+ if (hardware.summary.hasIntegratedGPU && hardware.summary.bestBackend === 'cpu') {
5065
+ console.log(` Assist path: ${chalk.yellow('Integrated/shared-memory GPU detected, runtime remains CPU')}`);
5066
+ }
5022
5067
 
5023
5068
  // CPU
5024
5069
  if (hardware.cpu) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-checker",
3
- "version": "3.5.3",
3
+ "version": "3.5.6",
4
4
  "description": "Intelligent CLI tool with AI-powered model selection that analyzes your hardware and recommends optimal LLM models for your system",
5
5
  "bin": {
6
6
  "llm-checker": "bin/cli.js",
@@ -107,6 +107,7 @@
107
107
  "npm": ">=8.0.0"
108
108
  },
109
109
  "os": [
110
+ "android",
110
111
  "darwin",
111
112
  "linux",
112
113
  "win32"
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const { MULTI_OBJECTIVE_WEIGHTS } = require('../models/scoring-config');
12
+ const { normalizePlatform } = require('../utils/platform');
12
13
 
13
14
  class MultiObjectiveSelector {
14
15
  constructor() {
@@ -346,7 +347,14 @@ class MultiObjectiveSelector {
346
347
  const unified = isAppleSilicon;
347
348
 
348
349
  // Detect PC platform (Windows/Linux) to match main algorithm
349
- const isPC = !isAppleSilicon && (process.platform === 'win32' || process.platform === 'linux');
350
+ const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
351
+ ? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
352
+ : '';
353
+ const hasIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
354
+ ? hardware.summary.hasIntegratedGPU
355
+ : /iris xe|uhd.*graphics|vega.*integrated|radeon.*graphics/i.test(`${gpuModel} ${integratedGpuInventory}`);
356
+ const platform = normalizePlatform(hardware?.os?.platform || process.platform);
357
+ const isPC = !isAppleSilicon && (platform === 'win32' || platform === 'linux');
350
358
 
351
359
  // 1) Effective memory for model weights (45%) - Apple Silicon & PC optimized
352
360
  let effMem;
@@ -446,7 +454,7 @@ class MultiObjectiveSelector {
446
454
  tier = bumpTier(tier, +1); // High-end GPU boost
447
455
  } else if (!vramGB && !unified) {
448
456
  tier = bumpTier(tier, -1); // CPU-only penalty (moderate)
449
- } else if (/iris xe|uhd.*graphics|vega.*integrated|radeon.*graphics/i.test(gpuModel)) {
457
+ } else if (hasIntegratedGPU) {
450
458
  tier = bumpTier(tier, -1); // iGPU penalty
451
459
  } else if (vramGB > 0 && vramGB < 6) {
452
460
  tier = bumpTier(tier, -1); // Low VRAM penalty
@@ -658,7 +666,10 @@ class MultiObjectiveSelector {
658
666
  const cpuModel = hardware.cpu?.brand || hardware.cpu?.model || '';
659
667
  const gpuModel = hardware.gpu?.model || '';
660
668
  const cpu = cpuModel.toLowerCase();
661
- const gpu = gpuModel.toLowerCase();
669
+ const integratedGpuInventory = Array.isArray(hardware.summary?.integratedGpuModels)
670
+ ? hardware.summary.integratedGpuModels.map(({ name }) => name).join(' ')
671
+ : '';
672
+ const gpu = `${gpuModel} ${integratedGpuInventory}`.toLowerCase();
662
673
  const cores = hardware.cpu?.physicalCores || hardware.cpu?.cores || 1;
663
674
 
664
675
  let specs = {
@@ -738,13 +749,16 @@ class MultiObjectiveSelector {
738
749
  const cores = hardware.cpu?.physicalCores || hardware.cpu?.cores || 1;
739
750
  const baseSpeed = hardware.cpu?.speed || 2.0;
740
751
  const vramGB = hardware.gpu?.vram || 0;
752
+ const hasIntegratedGPU = typeof hardware.summary?.hasIntegratedGPU === 'boolean'
753
+ ? hardware.summary.hasIntegratedGPU
754
+ : false;
741
755
 
742
756
  // Use improved CPU estimation function for more realistic and varying speeds
743
757
  const hasAVX512 = cpuModel.toLowerCase().includes('intel') &&
744
758
  (cpuModel.includes('13th') || cpuModel.includes('14th') || cpuModel.includes('12th'));
745
759
 
746
760
  // GPU-based calculation (dedicated GPU only)
747
- if (vramGB > 0 && !gpuModel.toLowerCase().includes('iris') && !gpuModel.toLowerCase().includes('integrated')) {
761
+ if (vramGB > 0 && !hasIntegratedGPU) {
748
762
  let gpuTPS = 20; // Conservative GPU baseline
749
763
  if (gpuModel.toLowerCase().includes('gb10') ||
750
764
  gpuModel.toLowerCase().includes('grace blackwell') ||
@@ -780,7 +794,7 @@ class MultiObjectiveSelector {
780
794
  threads: cores,
781
795
  paramsB: params,
782
796
  avx512: hasAVX512,
783
- isIrisXe: gpuModel.toLowerCase().includes('iris xe')
797
+ isIrisXe: hasIntegratedGPU && gpuModel.toLowerCase().includes('iris xe')
784
798
  });
785
799
  }
786
800
  }
@@ -7,6 +7,7 @@
7
7
  const { execSync } = require('child_process');
8
8
  const os = require('os');
9
9
  const fs = require('fs');
10
+ const { normalizePlatform } = require('../../utils/platform');
10
11
 
11
12
  class CPUDetector {
12
13
  constructor() {
@@ -87,10 +88,12 @@ class CPUDetector {
87
88
  * Get physical core count
88
89
  */
89
90
  getPhysicalCores() {
91
+ const platform = normalizePlatform();
92
+
90
93
  try {
91
- if (process.platform === 'darwin') {
94
+ if (platform === 'darwin') {
92
95
  return parseInt(execSync('sysctl -n hw.physicalcpu', { encoding: 'utf8', timeout: 5000 }).trim());
93
- } else if (process.platform === 'linux') {
96
+ } else if (platform === 'linux') {
94
97
  const cpuInfo = fs.readFileSync('/proc/cpuinfo', 'utf8');
95
98
  const coreIds = new Set();
96
99
  const matches = cpuInfo.matchAll(/core id\s*:\s*(\d+)/g);
@@ -98,7 +101,7 @@ class CPUDetector {
98
101
  coreIds.add(match[1]);
99
102
  }
100
103
  return coreIds.size || os.cpus().length;
101
- } else if (process.platform === 'win32') {
104
+ } else if (platform === 'win32') {
102
105
  const physicalCores = this.getWindowsPhysicalCoreCount();
103
106
  return physicalCores || os.cpus().length;
104
107
  }
@@ -112,18 +115,20 @@ class CPUDetector {
112
115
  * Get maximum CPU frequency
113
116
  */
114
117
  getMaxFrequency() {
118
+ const platform = normalizePlatform();
119
+
115
120
  try {
116
- if (process.platform === 'darwin') {
121
+ if (platform === 'darwin') {
117
122
  // macOS doesn't expose max frequency easily
118
123
  const cpus = os.cpus();
119
124
  return cpus.length > 0 ? cpus[0].speed : 0;
120
- } else if (process.platform === 'linux') {
125
+ } else if (platform === 'linux') {
121
126
  const maxFreq = fs.readFileSync(
122
127
  '/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq',
123
128
  'utf8'
124
129
  );
125
130
  return Math.round(parseInt(maxFreq) / 1000); // kHz to MHz
126
- } else if (process.platform === 'win32') {
131
+ } else if (platform === 'win32') {
127
132
  const maxClock = this.getWindowsMaxClockSpeed();
128
133
  return maxClock || (os.cpus()[0]?.speed || 0);
129
134
  }
@@ -206,13 +211,15 @@ class CPUDetector {
206
211
  l3: 0
207
212
  };
208
213
 
214
+ const platform = normalizePlatform();
215
+
209
216
  try {
210
- if (process.platform === 'darwin') {
217
+ if (platform === 'darwin') {
211
218
  cache.l1d = parseInt(execSync('sysctl -n hw.l1dcachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 || 0;
212
219
  cache.l1i = parseInt(execSync('sysctl -n hw.l1icachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 || 0;
213
220
  cache.l2 = parseInt(execSync('sysctl -n hw.l2cachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 / 1024 || 0;
214
221
  cache.l3 = parseInt(execSync('sysctl -n hw.l3cachesize', { encoding: 'utf8', timeout: 5000 })) / 1024 / 1024 || 0;
215
- } else if (process.platform === 'linux') {
222
+ } else if (platform === 'linux') {
216
223
  // Parse from /sys/devices/system/cpu/cpu0/cache/
217
224
  const cachePath = '/sys/devices/system/cpu/cpu0/cache';
218
225
  if (fs.existsSync(cachePath)) {
@@ -268,8 +275,10 @@ class CPUDetector {
268
275
  bestSimd: 'none'
269
276
  };
270
277
 
278
+ const platform = normalizePlatform();
279
+
271
280
  try {
272
- if (process.platform === 'darwin') {
281
+ if (platform === 'darwin') {
273
282
  // For Apple Silicon (ARM64), use ARM features
274
283
  if (process.arch === 'arm64') {
275
284
  caps.neon = true; // All Apple Silicon has NEON
@@ -308,7 +317,7 @@ class CPUDetector {
308
317
  }
309
318
  }
310
319
 
311
- } else if (process.platform === 'linux') {
320
+ } else if (platform === 'linux') {
312
321
  // Linux - check /proc/cpuinfo
313
322
  const cpuInfo = fs.readFileSync('/proc/cpuinfo', 'utf8').toLowerCase();
314
323
  const flags = cpuInfo.match(/flags\s*:\s*(.+)/)?.[1] || '';
@@ -334,7 +343,7 @@ class CPUDetector {
334
343
  caps.dotprod = flags.includes('asimddp');
335
344
  }
336
345
 
337
- } else if (process.platform === 'win32') {
346
+ } else if (platform === 'win32') {
338
347
  // Windows - use WMIC or assume based on CPU model
339
348
  const cpuName = os.cpus()[0]?.model?.toLowerCase() || '';
340
349
 
@@ -248,13 +248,20 @@ class ROCmDetector {
248
248
  timeout: 10000
249
249
  });
250
250
 
251
- // Parse memory info
252
- const memMatches = memInfo.matchAll(/GPU\[(\d+)\].*?Total.*?:\s*(\d+)/g);
251
+ // Parse memory info. Newer rocm-smi reports bytes "(B)" while some
252
+ // systems expose MiB; normalize to GB safely.
253
253
  const gpuMemory = {};
254
- for (const match of memMatches) {
255
- const idx = parseInt(match[1]);
256
- const memMB = parseInt(match[2]);
257
- gpuMemory[idx] = Math.round(memMB / 1024); // Convert to GB
254
+ const memLines = String(memInfo || '').split('\n');
255
+ for (const line of memLines) {
256
+ const lineMatch = line.match(/GPU\[(\d+)\].*?Total.*?Memory\s*(?:\(([^)]+)\))?\s*:\s*(\d+)/i);
257
+ if (!lineMatch) continue;
258
+
259
+ const idx = parseInt(lineMatch[1], 10);
260
+ const unitHint = lineMatch[2] || '';
261
+ const rawValue = parseInt(lineMatch[3], 10);
262
+
263
+ if (!Number.isFinite(rawValue) || rawValue <= 0) continue;
264
+ gpuMemory[idx] = this.normalizeRocmMemoryToGB(rawValue, unitHint);
258
265
  }
259
266
 
260
267
  // Get temperature and utilization
@@ -312,6 +319,45 @@ class ROCmDetector {
312
319
  }
313
320
  }
314
321
 
322
+ /**
323
+ * Normalize rocm-smi memory values to GB.
324
+ * rocm-smi may report bytes "(B)" or MiB depending on version/system.
325
+ */
326
+ normalizeRocmMemoryToGB(value, unitHint = '') {
327
+ const numericValue = Number(value);
328
+ if (!Number.isFinite(numericValue) || numericValue <= 0) {
329
+ return 0;
330
+ }
331
+
332
+ const unit = String(unitHint || '').toLowerCase();
333
+
334
+ if (unit.includes('gib') || unit.includes('gb')) {
335
+ return Math.round(numericValue);
336
+ }
337
+
338
+ if (unit.includes('mib') || unit.includes('mb')) {
339
+ return Math.round(numericValue / 1024);
340
+ }
341
+
342
+ if (unit.includes('kib') || unit.includes('kb')) {
343
+ return Math.round(numericValue / (1024 * 1024));
344
+ }
345
+
346
+ if (unit === 'b' || unit === 'bytes') {
347
+ return Math.round(numericValue / (1024 ** 3));
348
+ }
349
+
350
+ // Unit was not provided. Use value magnitude heuristics.
351
+ if (numericValue >= 1024 ** 3) {
352
+ return Math.round(numericValue / (1024 ** 3));
353
+ }
354
+ if (numericValue >= 1024) {
355
+ return Math.round(numericValue / 1024);
356
+ }
357
+
358
+ return Math.round(numericValue);
359
+ }
360
+
315
361
  /**
316
362
  * Detect GPUs via rocminfo
317
363
  */
@@ -1,5 +1,6 @@
1
1
  const si = require('systeminformation');
2
2
  const UnifiedDetector = require('./unified-detector');
3
+ const { normalizePlatform } = require('../utils/platform');
3
4
 
4
5
  class HardwareDetector {
5
6
  constructor() {
@@ -255,33 +256,75 @@ class HardwareDetector {
255
256
  }
256
257
 
257
258
  const primaryType = unified.primary.type || 'cpu';
258
- if (primaryType === 'cpu') {
259
- return;
260
- }
261
-
262
259
  const summary = unified.summary;
263
- const backendInfo = unified.backends?.[primaryType]?.info || {};
264
- const backendGPUs = Array.isArray(backendInfo.gpus) ? backendInfo.gpus : [];
265
- const gpuCount = summary.gpuCount || backendGPUs.length || systemInfo.gpu.gpuCount || 1;
266
-
267
- const totalVRAM = typeof summary.totalVRAM === 'number' ? summary.totalVRAM : systemInfo.gpu.vram;
268
- const perGPUVRAM = backendGPUs[0]?.memory?.total
269
- || (gpuCount > 0 && totalVRAM > 0 ? Math.round(totalVRAM / gpuCount) : 0);
260
+ const hasFallbackDedicatedGpu = Boolean(
261
+ primaryType === 'cpu' &&
262
+ unified.systemGpu?.available &&
263
+ Array.isArray(unified.systemGpu.gpus) &&
264
+ unified.systemGpu.gpus.some((gpu) => gpu.type === 'dedicated')
265
+ );
266
+ const backendInfo = hasFallbackDedicatedGpu
267
+ ? unified.systemGpu
268
+ : (unified.backends?.[primaryType]?.info || {});
270
269
 
271
- const modelFromUnified = summary.gpuInventory || summary.gpuModel || systemInfo.gpu.model;
270
+ const backendGPUs = Array.isArray(backendInfo.gpus) ? backendInfo.gpus : [];
271
+ const dedicatedBackendGPUs = backendGPUs.filter((gpu) => gpu?.type !== 'integrated');
272
+ const dedicatedInventoryModels = Array.isArray(summary.dedicatedGpuModels) ? summary.dedicatedGpuModels : [];
273
+ const integratedInventoryModels = Array.isArray(summary.integratedGpuModels) ? summary.integratedGpuModels : [];
274
+ const hasDedicatedGPU = Boolean(
275
+ summary.hasDedicatedGPU ||
276
+ dedicatedInventoryModels.length > 0 ||
277
+ dedicatedBackendGPUs.length > 0
278
+ );
279
+ const hasIntegratedGPU = Boolean(
280
+ summary.hasIntegratedGPU ||
281
+ integratedInventoryModels.length > 0 ||
282
+ backendGPUs.some((gpu) => gpu?.type === 'integrated')
283
+ );
284
+ const primaryDedicatedModel = dedicatedInventoryModels[0]?.name || dedicatedBackendGPUs[0]?.name || null;
285
+ const primaryIntegratedModel = integratedInventoryModels[0]?.name || null;
286
+
287
+ const gpuCount = summary.gpuCount ||
288
+ dedicatedBackendGPUs.length ||
289
+ backendGPUs.length ||
290
+ systemInfo.gpu.gpuCount ||
291
+ 1;
292
+
293
+ const totalVRAMFromUnified = typeof summary.totalVRAM === 'number' ? summary.totalVRAM : 0;
294
+ const totalVRAMFromFallback = dedicatedBackendGPUs.reduce((sum, gpu) => {
295
+ const amount = Number(gpu?.memory?.total || gpu?.memoryTotal || 0);
296
+ return sum + (Number.isFinite(amount) ? amount : 0);
297
+ }, 0);
298
+ const totalVRAM = totalVRAMFromUnified || totalVRAMFromFallback || systemInfo.gpu.vram;
299
+ const perGPUVRAM = dedicatedBackendGPUs[0]?.memory?.total ||
300
+ (hasDedicatedGPU && gpuCount > 0 && totalVRAM > 0 ? Math.round(totalVRAM / Math.max(1, summary.dedicatedGpuCount || gpuCount)) : 0);
301
+
302
+ const fallbackModel = primaryDedicatedModel || primaryIntegratedModel || backendGPUs[0]?.name || null;
303
+ const modelFromUnified = summary.gpuModel || fallbackModel || systemInfo.gpu.model;
272
304
  const vendor = this.inferVendorFromGPUModel(modelFromUnified, systemInfo.gpu.vendor);
305
+ const isAppleUnified = primaryType === 'metal';
306
+
307
+ systemInfo.summary = {
308
+ ...summary
309
+ };
273
310
 
274
311
  systemInfo.gpu = {
275
312
  ...systemInfo.gpu,
276
313
  model: modelFromUnified,
277
314
  vendor,
278
- vram: totalVRAM || systemInfo.gpu.vram,
279
- vramPerGPU: perGPUVRAM || systemInfo.gpu.vramPerGPU || 0,
280
- dedicated: primaryType !== 'metal',
281
- gpuCount,
315
+ vram: isAppleUnified ? systemInfo.gpu.vram : (hasDedicatedGPU ? (totalVRAM || systemInfo.gpu.vram) : 0),
316
+ vramPerGPU: isAppleUnified ? (systemInfo.gpu.vramPerGPU || 0) : (perGPUVRAM || systemInfo.gpu.vramPerGPU || 0),
317
+ dedicated: hasDedicatedGPU,
318
+ gpuCount: summary.gpuCount || gpuCount,
282
319
  isMultiGPU: Boolean(summary.isMultiGPU || gpuCount > 1),
283
320
  gpuInventory: summary.gpuInventory || null,
284
- backend: primaryType,
321
+ hasIntegratedGPU,
322
+ hasDedicatedGPU,
323
+ integratedGpuCount: summary.integratedGpuCount || 0,
324
+ dedicatedGpuCount: summary.dedicatedGpuCount || 0,
325
+ integratedGpuModels: integratedInventoryModels,
326
+ dedicatedGpuModels: dedicatedInventoryModels,
327
+ backend: hasFallbackDedicatedGpu ? 'generic' : primaryType,
285
328
  driverVersion: backendInfo.driver || systemInfo.gpu.driverVersion
286
329
  };
287
330
  } catch (error) {
@@ -301,8 +344,11 @@ class HardwareDetector {
301
344
  }
302
345
 
303
346
  processOSInfo(osInfo) {
347
+ const rawPlatform = osInfo.platform || process.platform;
348
+
304
349
  return {
305
- platform: osInfo.platform || process.platform,
350
+ platform: normalizePlatform(rawPlatform),
351
+ platformRaw: rawPlatform,
306
352
  distro: osInfo.distro || 'Unknown',
307
353
  release: osInfo.release || 'Unknown',
308
354
  codename: osInfo.codename || 'Unknown',