llm-checker 3.1.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/LICENSE +21 -0
- package/README.md +418 -0
- package/analyzer/compatibility.js +584 -0
- package/analyzer/performance.js +505 -0
- package/bin/CLAUDE.md +12 -0
- package/bin/enhanced_cli.js +3118 -0
- package/bin/test-deterministic.js +41 -0
- package/package.json +96 -0
- package/src/CLAUDE.md +12 -0
- package/src/ai/intelligent-selector.js +615 -0
- package/src/ai/model-selector.js +312 -0
- package/src/ai/multi-objective-selector.js +820 -0
- package/src/commands/check.js +58 -0
- package/src/data/CLAUDE.md +11 -0
- package/src/data/model-database.js +637 -0
- package/src/data/sync-manager.js +279 -0
- package/src/hardware/CLAUDE.md +12 -0
- package/src/hardware/backends/CLAUDE.md +11 -0
- package/src/hardware/backends/apple-silicon.js +318 -0
- package/src/hardware/backends/cpu-detector.js +490 -0
- package/src/hardware/backends/cuda-detector.js +417 -0
- package/src/hardware/backends/intel-detector.js +436 -0
- package/src/hardware/backends/rocm-detector.js +440 -0
- package/src/hardware/detector.js +573 -0
- package/src/hardware/pc-optimizer.js +635 -0
- package/src/hardware/specs.js +286 -0
- package/src/hardware/unified-detector.js +442 -0
- package/src/index.js +2289 -0
- package/src/models/CLAUDE.md +17 -0
- package/src/models/ai-check-selector.js +806 -0
- package/src/models/catalog.json +426 -0
- package/src/models/deterministic-selector.js +1145 -0
- package/src/models/expanded_database.js +1142 -0
- package/src/models/intelligent-selector.js +532 -0
- package/src/models/requirements.js +310 -0
- package/src/models/scoring-config.js +57 -0
- package/src/models/scoring-engine.js +715 -0
- package/src/ollama/.cache/README.md +33 -0
- package/src/ollama/CLAUDE.md +24 -0
- package/src/ollama/client.js +438 -0
- package/src/ollama/enhanced-client.js +113 -0
- package/src/ollama/enhanced-scraper.js +634 -0
- package/src/ollama/manager.js +357 -0
- package/src/ollama/native-scraper.js +776 -0
- package/src/plugins/CLAUDE.md +11 -0
- package/src/plugins/examples/custom_model_plugin.js +87 -0
- package/src/plugins/index.js +295 -0
- package/src/utils/CLAUDE.md +11 -0
- package/src/utils/config.js +359 -0
- package/src/utils/formatter.js +315 -0
- package/src/utils/logger.js +272 -0
- package/src/utils/model-classifier.js +167 -0
- package/src/utils/verbose-progress.js +266 -0
|
@@ -0,0 +1,3118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { Command } = require('commander');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { table } = require('table');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
// LLMChecker is loaded lazily to avoid slow systeminformation init
|
|
9
|
+
let _LLMChecker = null;
|
|
10
|
+
function getLLMChecker() {
|
|
11
|
+
if (!_LLMChecker) {
|
|
12
|
+
_LLMChecker = require('../src/index');
|
|
13
|
+
}
|
|
14
|
+
return _LLMChecker;
|
|
15
|
+
}
|
|
16
|
+
const { getLogger } = require('../src/utils/logger');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
// ASCII Art for each command - Large text banners
|
|
21
|
+
const ASCII_ART = {
|
|
22
|
+
'hw-detect': `
|
|
23
|
+
██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ███████╗
|
|
24
|
+
██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔══██╗██╔══██╗██╔════╝
|
|
25
|
+
███████║███████║██████╔╝██║ ██║██║ █╗ ██║███████║██████╔╝█████╗
|
|
26
|
+
██╔══██║██╔══██║██╔══██╗██║ ██║██║███╗██║██╔══██║██╔══██╗██╔══╝
|
|
27
|
+
██║ ██║██║ ██║██║ ██║██████╔╝╚███╔███╔╝██║ ██║██║ ██║███████╗
|
|
28
|
+
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝
|
|
29
|
+
DETECTION`,
|
|
30
|
+
|
|
31
|
+
'smart-recommend': `
|
|
32
|
+
███████╗███╗ ███╗ █████╗ ██████╗ ████████╗
|
|
33
|
+
██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝
|
|
34
|
+
███████╗██╔████╔██║███████║██████╔╝ ██║
|
|
35
|
+
╚════██║██║╚██╔╝██║██╔══██║██╔══██╗ ██║
|
|
36
|
+
███████║██║ ╚═╝ ██║██║ ██║██║ ██║ ██║
|
|
37
|
+
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
|
|
38
|
+
RECOMMEND - AI Powered`,
|
|
39
|
+
|
|
40
|
+
'search': `
|
|
41
|
+
███████╗███████╗ █████╗ ██████╗ ██████╗██╗ ██╗
|
|
42
|
+
██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██║ ██║
|
|
43
|
+
███████╗█████╗ ███████║██████╔╝██║ ███████║
|
|
44
|
+
╚════██║██╔══╝ ██╔══██║██╔══██╗██║ ██╔══██║
|
|
45
|
+
███████║███████╗██║ ██║██║ ██║╚██████╗██║ ██║
|
|
46
|
+
╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
|
|
47
|
+
6900+ Models Available`,
|
|
48
|
+
|
|
49
|
+
'sync': `
|
|
50
|
+
███████╗██╗ ██╗███╗ ██╗ ██████╗
|
|
51
|
+
██╔════╝╚██╗ ██╔╝████╗ ██║██╔════╝
|
|
52
|
+
███████╗ ╚████╔╝ ██╔██╗ ██║██║
|
|
53
|
+
╚════██║ ╚██╔╝ ██║╚██╗██║██║
|
|
54
|
+
███████║ ██║ ██║ ╚████║╚██████╗
|
|
55
|
+
╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝
|
|
56
|
+
Database Synchronization`,
|
|
57
|
+
|
|
58
|
+
'check': `
|
|
59
|
+
██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
|
|
60
|
+
██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
|
|
61
|
+
██║ ███████║█████╗ ██║ █████╔╝
|
|
62
|
+
██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
|
|
63
|
+
╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
|
|
64
|
+
╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
|
|
65
|
+
Compatibility Analysis`,
|
|
66
|
+
|
|
67
|
+
'installed': `
|
|
68
|
+
██╗███╗ ██╗███████╗████████╗ █████╗ ██╗ ██╗ ███████╗██████╗
|
|
69
|
+
██║████╗ ██║██╔════╝╚══██╔══╝██╔══██╗██║ ██║ ██╔════╝██╔══██╗
|
|
70
|
+
██║██╔██╗ ██║███████╗ ██║ ███████║██║ ██║ █████╗ ██║ ██║
|
|
71
|
+
██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║ ██║ ██╔══╝ ██║ ██║
|
|
72
|
+
██║██║ ╚████║███████║ ██║ ██║ ██║███████╗███████╗███████╗██████╔╝
|
|
73
|
+
╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═════╝
|
|
74
|
+
Local Models`,
|
|
75
|
+
|
|
76
|
+
'ai-check': `
|
|
77
|
+
█████╗ ██╗ ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗
|
|
78
|
+
██╔══██╗██║ ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝
|
|
79
|
+
███████║██║ ██║ ███████║█████╗ ██║ █████╔╝
|
|
80
|
+
██╔══██║██║ ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗
|
|
81
|
+
██║ ██║██║ ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗
|
|
82
|
+
╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝
|
|
83
|
+
AI-Powered Evaluation`,
|
|
84
|
+
|
|
85
|
+
'ai-run': `
|
|
86
|
+
█████╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
|
|
87
|
+
██╔══██╗██║ ██╔══██╗██║ ██║████╗ ██║
|
|
88
|
+
███████║██║ ██████╔╝██║ ██║██╔██╗ ██║
|
|
89
|
+
██╔══██║██║ ██╔══██╗██║ ██║██║╚██╗██║
|
|
90
|
+
██║ ██║██║ ██║ ██║╚██████╔╝██║ ╚████║
|
|
91
|
+
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
92
|
+
Launch & Execute Model`,
|
|
93
|
+
|
|
94
|
+
'demo': `
|
|
95
|
+
██████╗ ███████╗███╗ ███╗ ██████╗
|
|
96
|
+
██╔══██╗██╔════╝████╗ ████║██╔═══██╗
|
|
97
|
+
██║ ██║█████╗ ██╔████╔██║██║ ██║
|
|
98
|
+
██║ ██║██╔══╝ ██║╚██╔╝██║██║ ██║
|
|
99
|
+
██████╔╝███████╗██║ ╚═╝ ██║╚██████╔╝
|
|
100
|
+
╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝
|
|
101
|
+
Interactive Preview`,
|
|
102
|
+
|
|
103
|
+
'ollama': `
|
|
104
|
+
██████╗ ██╗ ██╗ █████╗ ███╗ ███╗ █████╗
|
|
105
|
+
██╔═══██╗██║ ██║ ██╔══██╗████╗ ████║██╔══██╗
|
|
106
|
+
██║ ██║██║ ██║ ███████║██╔████╔██║███████║
|
|
107
|
+
██║ ██║██║ ██║ ██╔══██║██║╚██╔╝██║██╔══██║
|
|
108
|
+
╚██████╔╝███████╗███████╗██║ ██║██║ ╚═╝ ██║██║ ██║
|
|
109
|
+
╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
110
|
+
Status & Integration`,
|
|
111
|
+
|
|
112
|
+
'recommend': `
|
|
113
|
+
██████╗ ███████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗███████╗███╗ ██╗██████╗
|
|
114
|
+
██╔══██╗██╔════╝██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔════╝████╗ ██║██╔══██╗
|
|
115
|
+
██████╔╝█████╗ ██║ ██║ ██║██╔████╔██║██╔████╔██║█████╗ ██╔██╗ ██║██║ ██║
|
|
116
|
+
██╔══██╗██╔══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║██║ ██║
|
|
117
|
+
██║ ██║███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║███████╗██║ ╚████║██████╔╝
|
|
118
|
+
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚═════╝
|
|
119
|
+
Top Picks For You`,
|
|
120
|
+
|
|
121
|
+
'list-models': `
|
|
122
|
+
███╗ ███╗ ██████╗ ██████╗ ███████╗██╗ ███████╗
|
|
123
|
+
████╗ ████║██╔═══██╗██╔══██╗██╔════╝██║ ██╔════╝
|
|
124
|
+
██╔████╔██║██║ ██║██║ ██║█████╗ ██║ ███████╗
|
|
125
|
+
██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ██║ ╚════██║
|
|
126
|
+
██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗███████╗███████║
|
|
127
|
+
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝
|
|
128
|
+
Browse All Available`
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Function to display ASCII art for a command
|
|
132
|
+
function showAsciiArt(command) {
|
|
133
|
+
if (ASCII_ART[command]) {
|
|
134
|
+
console.log(chalk.cyan(ASCII_ART[command]));
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Function to search Ollama models by use case
|
|
140
|
+
function getOllamaCacheFile(filename) {
|
|
141
|
+
try {
|
|
142
|
+
const homePath = path.join(os.homedir(), '.llm-checker', 'cache', 'ollama', filename);
|
|
143
|
+
const legacyPath = path.join(__dirname, '../src/ollama/.cache', filename);
|
|
144
|
+
if (fs.existsSync(homePath)) return homePath;
|
|
145
|
+
if (fs.existsSync(legacyPath)) return legacyPath;
|
|
146
|
+
return homePath; // default preferred path
|
|
147
|
+
} catch {
|
|
148
|
+
return path.join(__dirname, '../src/ollama/.cache', filename);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function searchOllamaModelsForUseCase(useCase, hardware) {
|
|
153
|
+
try {
|
|
154
|
+
const cacheFile = getOllamaCacheFile('ollama-detailed-models.json');
|
|
155
|
+
if (!fs.existsSync(cacheFile)) return [];
|
|
156
|
+
|
|
157
|
+
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
158
|
+
const models = cacheData.models || [];
|
|
159
|
+
|
|
160
|
+
// Filter models by use case with typo tolerance
|
|
161
|
+
const useCaseModels = models.filter(model => {
|
|
162
|
+
const lowerUseCase = useCase.toLowerCase();
|
|
163
|
+
switch (lowerUseCase) {
|
|
164
|
+
case 'creative':
|
|
165
|
+
case 'writing':
|
|
166
|
+
return model.primary_category === 'creative';
|
|
167
|
+
|
|
168
|
+
case 'coding':
|
|
169
|
+
case 'code':
|
|
170
|
+
return model.primary_category === 'coding';
|
|
171
|
+
|
|
172
|
+
case 'chat':
|
|
173
|
+
case 'conversation':
|
|
174
|
+
case 'talking':
|
|
175
|
+
return model.primary_category === 'chat';
|
|
176
|
+
|
|
177
|
+
case 'multimodal':
|
|
178
|
+
case 'vision':
|
|
179
|
+
return model.primary_category === 'multimodal';
|
|
180
|
+
|
|
181
|
+
case 'embeddings':
|
|
182
|
+
case 'embedings': // typo tolerance
|
|
183
|
+
case 'embedding':
|
|
184
|
+
case 'embeding': // typo tolerance
|
|
185
|
+
return model.primary_category === 'embeddings';
|
|
186
|
+
|
|
187
|
+
case 'reasoning':
|
|
188
|
+
case 'reason':
|
|
189
|
+
return model.primary_category === 'reasoning';
|
|
190
|
+
|
|
191
|
+
default:
|
|
192
|
+
// Check for partial matches
|
|
193
|
+
if (lowerUseCase.includes('embed')) return model.primary_category === 'embeddings';
|
|
194
|
+
if (lowerUseCase.includes('code')) return model.primary_category === 'coding';
|
|
195
|
+
if (lowerUseCase.includes('creat')) return model.primary_category === 'creative';
|
|
196
|
+
if (lowerUseCase.includes('chat') || lowerUseCase.includes('talk')) return model.primary_category === 'chat';
|
|
197
|
+
if (lowerUseCase.includes('vision') || lowerUseCase.includes('multimodal')) return model.primary_category === 'multimodal';
|
|
198
|
+
if (lowerUseCase.includes('reason')) return model.primary_category === 'reasoning';
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Convert Ollama models to compatible format and add basic compatibility scoring
|
|
204
|
+
return useCaseModels.map(model => {
|
|
205
|
+
// Find a suitable variant (prefer 7b-13b for high-end hardware)
|
|
206
|
+
let bestVariant = null;
|
|
207
|
+
if (model.variants && model.variants.length > 0) {
|
|
208
|
+
// For high-tier hardware, prefer 7B-13B models
|
|
209
|
+
bestVariant = model.variants.find(v =>
|
|
210
|
+
v.real_size_gb >= 3 && v.real_size_gb <= 15 &&
|
|
211
|
+
!v.tag.includes('-instruct') && !v.tag.includes('-code')
|
|
212
|
+
) || model.variants[0];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const size = bestVariant ? bestVariant.real_size_gb : 7;
|
|
216
|
+
const ollamaTag = bestVariant ? bestVariant.tag : model.model_identifier + ':latest';
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
name: model.model_name || model.model_identifier,
|
|
220
|
+
size: size + 'GB',
|
|
221
|
+
type: 'ollama',
|
|
222
|
+
category: model.primary_category,
|
|
223
|
+
specialization: model.primary_category,
|
|
224
|
+
primary_category: model.primary_category,
|
|
225
|
+
categories: model.categories,
|
|
226
|
+
requirements: {
|
|
227
|
+
ram: Math.max(4, Math.ceil(size * 1.2)),
|
|
228
|
+
vram: 0,
|
|
229
|
+
cpu_cores: 2,
|
|
230
|
+
storage: size,
|
|
231
|
+
recommended_ram: Math.max(8, Math.ceil(size * 1.5))
|
|
232
|
+
},
|
|
233
|
+
frameworks: ['ollama'],
|
|
234
|
+
performance: {
|
|
235
|
+
speed: size <= 7 ? 'fast' : size <= 13 ? 'medium' : 'slow',
|
|
236
|
+
quality: model.primary_category === 'coding' ? 'excellent_for_code' :
|
|
237
|
+
model.primary_category === 'creative' ? 'excellent_for_creative' : 'good',
|
|
238
|
+
context_length: 4096,
|
|
239
|
+
tokens_per_second_estimate: size <= 7 ? '30-50' : '15-30'
|
|
240
|
+
},
|
|
241
|
+
installation: {
|
|
242
|
+
ollama: `ollama pull ${ollamaTag}`,
|
|
243
|
+
description: model.detailed_description || model.description || `${model.primary_category} model`
|
|
244
|
+
},
|
|
245
|
+
ollamaId: model.model_identifier,
|
|
246
|
+
ollamaTag: ollamaTag,
|
|
247
|
+
source: 'ollama_database',
|
|
248
|
+
// Basic compatibility score (can be improved)
|
|
249
|
+
score: calculateBasicCompatibilityScore(size, hardware),
|
|
250
|
+
isOllamaInstalled: false,
|
|
251
|
+
ollamaAvailable: true
|
|
252
|
+
};
|
|
253
|
+
}).slice(0, 10); // Limit to top 10 models
|
|
254
|
+
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.warn('Error searching Ollama models:', error.message);
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Basic compatibility scoring for Ollama models
|
|
262
|
+
function calculateBasicCompatibilityScore(modelSizeGB, hardware) {
|
|
263
|
+
const totalRAM = hardware.memory?.total || 8;
|
|
264
|
+
const availableRAM = totalRAM * 0.8; // Assume 80% available
|
|
265
|
+
|
|
266
|
+
// RAM compatibility
|
|
267
|
+
let ramScore = 0;
|
|
268
|
+
if (modelSizeGB * 1.5 <= availableRAM) {
|
|
269
|
+
ramScore = 100;
|
|
270
|
+
} else if (modelSizeGB <= availableRAM) {
|
|
271
|
+
ramScore = 80;
|
|
272
|
+
} else {
|
|
273
|
+
ramScore = Math.max(0, 50 - (modelSizeGB - availableRAM) * 10);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Size efficiency (prefer 7B-13B for high-end hardware)
|
|
277
|
+
let sizeScore = 100;
|
|
278
|
+
if (totalRAM >= 16) { // High-end hardware
|
|
279
|
+
if (modelSizeGB >= 7 && modelSizeGB <= 13) {
|
|
280
|
+
sizeScore = 100;
|
|
281
|
+
} else if (modelSizeGB < 7) {
|
|
282
|
+
sizeScore = 85; // Small models are okay but not optimal
|
|
283
|
+
} else {
|
|
284
|
+
sizeScore = Math.max(60, 100 - (modelSizeGB - 13) * 5);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return Math.round((ramScore * 0.7 + sizeScore * 0.3));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Function to get real size directly from Ollama cache
|
|
292
|
+
function estimateModelSize(model) {
|
|
293
|
+
// Extract parameter count from model name (e.g., "3B", "7B", "13B")
|
|
294
|
+
const nameMatch = model.name.match(/(\d+\.?\d*)[bB]\b/i);
|
|
295
|
+
if (nameMatch) {
|
|
296
|
+
const paramCount = parseFloat(nameMatch[1]);
|
|
297
|
+
// Estimate size using Q4_K_M quantization (~0.5 bytes per parameter + overhead)
|
|
298
|
+
const estimatedGB = Math.round((paramCount * 0.5 + 0.5) * 10) / 10;
|
|
299
|
+
return `~${estimatedGB}GB (Q4_K_M)`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Try to extract from model identifier or fallback patterns
|
|
303
|
+
if (model.model_identifier) {
|
|
304
|
+
const idMatch = model.model_identifier.match(/(\d+\.?\d*)b/i);
|
|
305
|
+
if (idMatch) {
|
|
306
|
+
const paramCount = parseFloat(idMatch[1]);
|
|
307
|
+
const estimatedGB = Math.round((paramCount * 0.5 + 0.5) * 10) / 10;
|
|
308
|
+
return `~${estimatedGB}GB (Q4_K_M)`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Known model size patterns
|
|
313
|
+
const sizeMappings = {
|
|
314
|
+
'tinyllama': '~1.1GB (Q4_K_M)',
|
|
315
|
+
'mobilellama': '~1.4GB (Q4_K_M)',
|
|
316
|
+
'phi': '~2.7GB (Q4_K_M)',
|
|
317
|
+
'gemma': '~5.3GB (Q4_K_M)',
|
|
318
|
+
'llama.*3b': '~2.0GB (Q4_K_M)',
|
|
319
|
+
'llama.*7b': '~4.4GB (Q4_K_M)',
|
|
320
|
+
'llama.*13b': '~7.8GB (Q4_K_M)',
|
|
321
|
+
'dolphincoder': '~4.2GB (Q4_K_M)',
|
|
322
|
+
'deepseek-coder': '~4.0GB (Q4_K_M)',
|
|
323
|
+
'starcoder': '~8.4GB (Q4_K_M)'
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const modelNameLower = (model.name || '').toLowerCase();
|
|
327
|
+
for (const [pattern, size] of Object.entries(sizeMappings)) {
|
|
328
|
+
if (new RegExp(pattern, 'i').test(modelNameLower)) {
|
|
329
|
+
return size;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// If we have size field but it's not formatted well
|
|
334
|
+
if (model.size && typeof model.size === 'string') {
|
|
335
|
+
const sizeMatch = model.size.match(/(\d+\.?\d*)\s*(GB|MB|B)/i);
|
|
336
|
+
if (sizeMatch) {
|
|
337
|
+
const num = parseFloat(sizeMatch[1]);
|
|
338
|
+
const unit = sizeMatch[2].toUpperCase();
|
|
339
|
+
if (unit === 'GB') return `${num}GB`;
|
|
340
|
+
if (unit === 'MB') return `${Math.round(num / 1024 * 10) / 10}GB`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Final fallback
|
|
345
|
+
return '~4.5GB (estimated)';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getRealSizeFromOllamaCache(model) {
|
|
349
|
+
try {
|
|
350
|
+
const cacheFile = getOllamaCacheFile('ollama-detailed-models.json');
|
|
351
|
+
if (!fs.existsSync(cacheFile)) return null;
|
|
352
|
+
|
|
353
|
+
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
354
|
+
const models = cacheData.models || [];
|
|
355
|
+
|
|
356
|
+
// Try to find the model by different strategies
|
|
357
|
+
let targetModel = null;
|
|
358
|
+
|
|
359
|
+
// Strategy 1: Match by ollamaId directly (e.g., "codellama")
|
|
360
|
+
if (model.ollamaId) {
|
|
361
|
+
// Special case: if looking for phind-codellama but model is actually CodeLlama, use codellama instead
|
|
362
|
+
if (model.ollamaId === 'phind-codellama' &&
|
|
363
|
+
(model.name.toLowerCase().includes('codellama') || model.name.toLowerCase().includes('code llama'))) {
|
|
364
|
+
targetModel = models.find(m => m.model_identifier === 'codellama');
|
|
365
|
+
}
|
|
366
|
+
// Special case: DeepSeek Coder has wrong ollamaId
|
|
367
|
+
else if (model.ollamaId === 'deepseek-v2.5' &&
|
|
368
|
+
model.name.toLowerCase().includes('deepseek') &&
|
|
369
|
+
model.name.toLowerCase().includes('coder')) {
|
|
370
|
+
targetModel = models.find(m => m.model_identifier === 'deepseek-coder');
|
|
371
|
+
}
|
|
372
|
+
// Special case: TinyLlama incorrectly mapped to llama-pro
|
|
373
|
+
else if (model.ollamaId === 'llama-pro' &&
|
|
374
|
+
model.name && model.name.toLowerCase().includes('tinyllama')) {
|
|
375
|
+
targetModel = models.find(m => m.model_identifier === 'tinyllama');
|
|
376
|
+
} else {
|
|
377
|
+
targetModel = models.find(m => m.model_identifier === model.ollamaId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Strategy 2: Match by name similarity
|
|
382
|
+
if (!targetModel && model.name) {
|
|
383
|
+
const modelNameLower = model.name.toLowerCase();
|
|
384
|
+
|
|
385
|
+
// Special handling for specific models - be very specific
|
|
386
|
+
if (modelNameLower.includes('deepseek') && modelNameLower.includes('coder')) {
|
|
387
|
+
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'deepseek-coder');
|
|
388
|
+
} else if (modelNameLower.includes('llama3.3')) {
|
|
389
|
+
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.3');
|
|
390
|
+
} else if (modelNameLower.includes('llama3.2')) {
|
|
391
|
+
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.2');
|
|
392
|
+
} else if (modelNameLower.includes('llama3.1') || modelNameLower.includes('llama 3.1')) {
|
|
393
|
+
targetModel = models.find(m => m.model_identifier.toLowerCase() === 'llama3.1');
|
|
394
|
+
} else {
|
|
395
|
+
targetModel = models.find(m => {
|
|
396
|
+
const identifier = m.model_identifier.toLowerCase();
|
|
397
|
+
return identifier.includes('codellama') && modelNameLower.includes('codellama') ||
|
|
398
|
+
identifier.includes('qwen') && modelNameLower.includes('qwen') ||
|
|
399
|
+
identifier.includes('mistral') && modelNameLower.includes('mistral');
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!targetModel || !targetModel.variants) return null;
|
|
405
|
+
|
|
406
|
+
// Extract size from model name (e.g., "CodeLlama 7B" -> "7b")
|
|
407
|
+
let targetSize = null;
|
|
408
|
+
if (model.size) {
|
|
409
|
+
targetSize = model.size.toLowerCase().replace('b', '') + 'b';
|
|
410
|
+
} else if (model.name) {
|
|
411
|
+
const sizeMatch = model.name.match(/(\d+\.?\d*)[bB]/);
|
|
412
|
+
if (sizeMatch) {
|
|
413
|
+
targetSize = sizeMatch[1] + 'b';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
// Find the right variant
|
|
419
|
+
let variant = null;
|
|
420
|
+
if (targetSize) {
|
|
421
|
+
// Look for exact size match (e.g., "codellama:7b")
|
|
422
|
+
variant = targetModel.variants.find(v =>
|
|
423
|
+
v.tag.includes(':' + targetSize) &&
|
|
424
|
+
!v.tag.includes('-instruct') &&
|
|
425
|
+
!v.tag.includes(':code-') // Exclude variants like ":code-" but allow "coder"
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Fallback to latest or first variant
|
|
431
|
+
if (!variant) {
|
|
432
|
+
variant = targetModel.variants.find(v => v.tag.includes(':latest')) ||
|
|
433
|
+
targetModel.variants[0];
|
|
434
|
+
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (variant && variant.real_size_gb) {
|
|
438
|
+
return variant.real_size_gb + 'GB';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return null;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.warn('Error reading Ollama cache:', error.message);
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const program = new Command();
|
|
449
|
+
|
|
450
|
+
program
|
|
451
|
+
.name('llm-checker')
|
|
452
|
+
.description('Check which LLM models your computer can run')
|
|
453
|
+
.version(require('../package.json').version);
|
|
454
|
+
|
|
455
|
+
const logger = getLogger({ console: false });
|
|
456
|
+
|
|
457
|
+
// Ollama installation helper
|
|
458
|
+
function getOllamaInstallInstructions() {
|
|
459
|
+
const platform = os.platform();
|
|
460
|
+
const arch = os.arch();
|
|
461
|
+
|
|
462
|
+
const instructions = {
|
|
463
|
+
'darwin': {
|
|
464
|
+
name: 'macOS',
|
|
465
|
+
downloadUrl: 'https://ollama.com/download/mac',
|
|
466
|
+
instructions: [
|
|
467
|
+
'1. Download Ollama for macOS from the link above',
|
|
468
|
+
'2. Open the downloaded .pkg file and follow the installer',
|
|
469
|
+
'3. Once installed, open Terminal and run: ollama serve',
|
|
470
|
+
'4. In a new terminal window, test with: ollama run llama2:7b'
|
|
471
|
+
],
|
|
472
|
+
alternativeInstall: 'brew install ollama'
|
|
473
|
+
},
|
|
474
|
+
'win32': {
|
|
475
|
+
name: 'Windows',
|
|
476
|
+
downloadUrl: 'https://ollama.com/download/windows',
|
|
477
|
+
instructions: [
|
|
478
|
+
'1. Download Ollama for Windows from the link above',
|
|
479
|
+
'2. Run the downloaded installer (.exe file)',
|
|
480
|
+
'3. Open Command Prompt or PowerShell',
|
|
481
|
+
'4. Test with: ollama run llama2:7b'
|
|
482
|
+
],
|
|
483
|
+
alternativeInstall: 'winget install Ollama.Ollama'
|
|
484
|
+
},
|
|
485
|
+
'linux': {
|
|
486
|
+
name: 'Linux',
|
|
487
|
+
downloadUrl: 'https://ollama.com/download/linux',
|
|
488
|
+
instructions: [
|
|
489
|
+
'1. Review official installation options:',
|
|
490
|
+
' https://github.com/ollama/ollama/blob/main/docs/linux.md',
|
|
491
|
+
'2. Prefer a package manager (apt/dnf/pacman) when available',
|
|
492
|
+
'3. Start service after install:',
|
|
493
|
+
' sudo systemctl start ollama',
|
|
494
|
+
'4. Test with: ollama run llama2:7b'
|
|
495
|
+
],
|
|
496
|
+
alternativeInstall: 'Manual install: https://github.com/ollama/ollama/blob/main/docs/linux.md'
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
return instructions[platform] || instructions['linux'];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function displayOllamaInstallHelp() {
|
|
504
|
+
const installInfo = getOllamaInstallInstructions();
|
|
505
|
+
|
|
506
|
+
console.log(chalk.red.bold('\nOllama is not installed or not running!'));
|
|
507
|
+
console.log(chalk.yellow('\nLLM Checker requires Ollama to function properly.'));
|
|
508
|
+
console.log(chalk.cyan.bold(`\nInstall Ollama for ${installInfo.name}:`));
|
|
509
|
+
console.log(chalk.blue(`\nDownload: ${installInfo.downloadUrl}`));
|
|
510
|
+
|
|
511
|
+
console.log(chalk.green.bold('\nInstallation Steps:'));
|
|
512
|
+
installInfo.instructions.forEach(step => {
|
|
513
|
+
console.log(chalk.gray(` ${step}`));
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
if (installInfo.alternativeInstall) {
|
|
517
|
+
console.log(chalk.magenta.bold('\nQuick Install (if available):'));
|
|
518
|
+
console.log(chalk.white(` ${installInfo.alternativeInstall}`));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
console.log(chalk.yellow.bold('\nAfter installation:'));
|
|
522
|
+
console.log(chalk.gray(' 1. Restart your terminal'));
|
|
523
|
+
console.log(chalk.gray(' 2. Run: llm-checker check'));
|
|
524
|
+
console.log(chalk.gray(' 3. Start using the AI model selector!'));
|
|
525
|
+
|
|
526
|
+
console.log(chalk.cyan('\nNeed help? Visit: https://github.com/ollama/ollama'));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function checkOllamaAndExit() {
|
|
530
|
+
const spinner = ora('Checking Ollama availability...').start();
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
// Quick check if ollama command exists
|
|
534
|
+
const checkCommand = os.platform() === 'win32' ? 'where' : 'which';
|
|
535
|
+
|
|
536
|
+
return new Promise((resolve) => {
|
|
537
|
+
const proc = spawn(checkCommand, ['ollama'], { stdio: 'pipe' });
|
|
538
|
+
|
|
539
|
+
proc.on('close', (code) => {
|
|
540
|
+
spinner.stop();
|
|
541
|
+
if (code !== 0) {
|
|
542
|
+
displayOllamaInstallHelp();
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
resolve(true);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
proc.on('error', () => {
|
|
549
|
+
spinner.stop();
|
|
550
|
+
displayOllamaInstallHelp();
|
|
551
|
+
process.exit(1);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
} catch (error) {
|
|
555
|
+
spinner.stop();
|
|
556
|
+
displayOllamaInstallHelp();
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function getStatusIcon(model, ollamaModels) {
|
|
562
|
+
const ollamaModel = ollamaModels?.find(om => om.matchedModel?.name === model.name);
|
|
563
|
+
|
|
564
|
+
if (ollamaModel?.isRunning) return 'R';
|
|
565
|
+
if (ollamaModel?.isInstalled) return 'I';
|
|
566
|
+
|
|
567
|
+
if (model.specialization === 'code') return 'C';
|
|
568
|
+
if (model.specialization === 'multimodal' || model.multimodal) return 'M';
|
|
569
|
+
if (model.specialization === 'embeddings') return 'E';
|
|
570
|
+
if (model.category === 'ultra_small') return 'XS';
|
|
571
|
+
if (model.category === 'small') return 'S';
|
|
572
|
+
if (model.category === 'medium') return 'M';
|
|
573
|
+
if (model.category === 'large') return 'L';
|
|
574
|
+
|
|
575
|
+
return '-';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function formatSize(size) {
|
|
579
|
+
if (!size) return 'Unknown';
|
|
580
|
+
|
|
581
|
+
const cleanSize = size.replace(/[^\d.BMK]/gi, '');
|
|
582
|
+
const numMatch = cleanSize.match(/(\d+\.?\d*)/);
|
|
583
|
+
const unitMatch = cleanSize.match(/[BMK]/i);
|
|
584
|
+
|
|
585
|
+
if (numMatch && unitMatch) {
|
|
586
|
+
const num = parseFloat(numMatch[1]);
|
|
587
|
+
const unit = unitMatch[0].toUpperCase();
|
|
588
|
+
return `${num}${unit}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return size;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Helper function to calculate model compatibility score
|
|
595
|
+
function calculateModelCompatibilityScore(model, hardware) {
|
|
596
|
+
let score = 50; // Base score
|
|
597
|
+
|
|
598
|
+
// Estimar tamaño del modelo
|
|
599
|
+
const sizeMatch = model.model_identifier.match(/(\d+\.?\d*)[bm]/i);
|
|
600
|
+
let modelSizeB = 1; // Default 1B
|
|
601
|
+
|
|
602
|
+
if (sizeMatch) {
|
|
603
|
+
const num = parseFloat(sizeMatch[1]);
|
|
604
|
+
const unit = sizeMatch[0].slice(-1).toLowerCase();
|
|
605
|
+
modelSizeB = unit === 'm' ? num / 1000 : num;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Calcular requerimientos estimados
|
|
609
|
+
const estimatedRAM = modelSizeB * 1.2; // 1.2x el tamaño del modelo
|
|
610
|
+
const ramRatio = hardware.memory.total / estimatedRAM;
|
|
611
|
+
|
|
612
|
+
// Puntuación por compatibilidad de RAM (40% del score)
|
|
613
|
+
if (ramRatio >= 3) score += 40;
|
|
614
|
+
else if (ramRatio >= 2) score += 30;
|
|
615
|
+
else if (ramRatio >= 1.5) score += 20;
|
|
616
|
+
else if (ramRatio >= 1.2) score += 10;
|
|
617
|
+
else if (ramRatio >= 1) score += 5;
|
|
618
|
+
else score -= 20; // Penalización por RAM insuficiente
|
|
619
|
+
|
|
620
|
+
// Puntuación por tamaño del modelo (30% del score)
|
|
621
|
+
if (modelSizeB <= 1) score += 30; // Modelos pequeños funcionan en cualquier lado
|
|
622
|
+
else if (modelSizeB <= 3) score += 25;
|
|
623
|
+
else if (modelSizeB <= 7) score += 20;
|
|
624
|
+
else if (modelSizeB <= 13) score += 15;
|
|
625
|
+
else if (modelSizeB <= 30) score += 10;
|
|
626
|
+
else score -= 10; // Modelos muy grandes
|
|
627
|
+
|
|
628
|
+
// Puntuación por CPU cores (20% del score)
|
|
629
|
+
if (hardware.cpu.cores >= 12) score += 20;
|
|
630
|
+
else if (hardware.cpu.cores >= 8) score += 15;
|
|
631
|
+
else if (hardware.cpu.cores >= 6) score += 10;
|
|
632
|
+
else if (hardware.cpu.cores >= 4) score += 5;
|
|
633
|
+
|
|
634
|
+
// Bonus por popularidad (10% del score)
|
|
635
|
+
const pulls = model.pulls || 0;
|
|
636
|
+
if (pulls > 1000000) score += 10;
|
|
637
|
+
else if (pulls > 100000) score += 7;
|
|
638
|
+
else if (pulls > 10000) score += 5;
|
|
639
|
+
else if (pulls > 1000) score += 3;
|
|
640
|
+
|
|
641
|
+
// Bonus especial para Apple Silicon
|
|
642
|
+
if (hardware.cpu.architecture === 'Apple Silicon') {
|
|
643
|
+
score += 5;
|
|
644
|
+
// Bonus extra para modelos optimizados
|
|
645
|
+
const modelName = model.model_identifier.toLowerCase();
|
|
646
|
+
if (modelName.includes('llama') || modelName.includes('mistral') ||
|
|
647
|
+
modelName.includes('phi') || modelName.includes('gemma')) {
|
|
648
|
+
score += 3;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Helper function to get hardware tier for display
|
|
656
|
+
function getHardwareTierForDisplay(hardware) {
|
|
657
|
+
const ram = hardware.memory.total;
|
|
658
|
+
const cores = hardware.cpu.cores;
|
|
659
|
+
const gpuModel = hardware.gpu?.model || '';
|
|
660
|
+
const vramGB = hardware.gpu?.vram || 0;
|
|
661
|
+
|
|
662
|
+
// Check if it's integrated GPU (should cap tier)
|
|
663
|
+
const isIntegratedGPU = /iris.*xe|iris.*graphics|uhd.*graphics|vega.*integrated|radeon.*graphics|intel.*integrated|integrated/i.test(gpuModel);
|
|
664
|
+
const hasDedicatedGPU = vramGB > 0 && !isIntegratedGPU;
|
|
665
|
+
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'));
|
|
666
|
+
|
|
667
|
+
// Base tier calculation
|
|
668
|
+
let tier;
|
|
669
|
+
if (ram >= 64 && cores >= 16) tier = 'EXTREME';
|
|
670
|
+
else if (ram >= 32 && cores >= 12) tier = 'VERY HIGH';
|
|
671
|
+
else if (ram >= 16 && cores >= 8) tier = 'HIGH';
|
|
672
|
+
else if (ram >= 8 && cores >= 4) tier = 'MEDIUM';
|
|
673
|
+
else if (ram >= 4 && cores >= 2) tier = 'LOW';
|
|
674
|
+
else tier = 'ULTRA LOW';
|
|
675
|
+
|
|
676
|
+
// Special cases for edge configurations
|
|
677
|
+
if (ram >= 16 && ram < 32 && cores >= 12) tier = 'HIGH';
|
|
678
|
+
if (ram >= 32 && ram < 64 && cores >= 8 && tier === 'ULTRA LOW') tier = 'VERY HIGH';
|
|
679
|
+
|
|
680
|
+
// Cap tier for integrated GPU systems (most important fix)
|
|
681
|
+
if (isIntegratedGPU && !isAppleSilicon) {
|
|
682
|
+
// Cap iGPU systems at HIGH maximum (Iris Xe, Intel UHD, AMD integrated, etc.)
|
|
683
|
+
const tierPriority = { 'ULTRA LOW': 0, 'LOW': 1, 'MEDIUM': 2, 'HIGH': 3, 'VERY HIGH': 4, 'EXTREME': 5 };
|
|
684
|
+
const currentPriority = tierPriority[tier] || 0;
|
|
685
|
+
if (currentPriority > 3) { // HIGH = 3
|
|
686
|
+
tier = 'HIGH';
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return tier;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function formatSpeed(speed) {
|
|
694
|
+
const speedMap = {
|
|
695
|
+
'very_fast': 'very_fast',
|
|
696
|
+
'fast': 'fast',
|
|
697
|
+
'medium': 'medium',
|
|
698
|
+
'slow': 'slow',
|
|
699
|
+
'very_slow': 'very_slow'
|
|
700
|
+
};
|
|
701
|
+
return speedMap[speed] || (speed || 'unknown');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function getScoreColor(score) {
|
|
705
|
+
if (score >= 90) return chalk.green;
|
|
706
|
+
if (score >= 75) return chalk.yellow;
|
|
707
|
+
if (score >= 60) return chalk.hex('#FFA500');
|
|
708
|
+
return chalk.red;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function getOllamaCommand(modelName) {
|
|
712
|
+
const mapping = {
|
|
713
|
+
'TinyLlama 1.1B': 'tinyllama:1.1b',
|
|
714
|
+
'Qwen 0.5B': 'qwen:0.5b',
|
|
715
|
+
'Gemma 2B': 'gemma2:2b',
|
|
716
|
+
'Phi-3 Mini 3.8B': 'phi3:mini',
|
|
717
|
+
'Llama 3.2 3B': 'llama3.2:3b',
|
|
718
|
+
'Llama 3.1 8B': 'llama3.1:8b',
|
|
719
|
+
'Mistral 7B v0.3': 'mistral:7b',
|
|
720
|
+
'CodeLlama 7B': 'codellama:7b',
|
|
721
|
+
'Qwen 2.5 7B': 'qwen2.5:7b'
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
return mapping[modelName] || '-';
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function displaySystemInfo(hardware, analysis) {
|
|
728
|
+
const cpuColor = hardware.cpu.cores >= 8 ? chalk.green : hardware.cpu.cores >= 4 ? chalk.yellow : chalk.red;
|
|
729
|
+
const ramColor = hardware.memory.total >= 32 ? chalk.green : hardware.memory.total >= 16 ? chalk.yellow : chalk.red;
|
|
730
|
+
const gpuColor = hardware.gpu.dedicated ? chalk.green : chalk.hex('#FFA500');
|
|
731
|
+
|
|
732
|
+
const lines = [
|
|
733
|
+
`${chalk.cyan('CPU:')} ${cpuColor(hardware.cpu.brand)} ${chalk.gray(`(${hardware.cpu.cores} cores, ${hardware.cpu.speed}GHz)`)}`,
|
|
734
|
+
`${chalk.cyan('Architecture:')} ${hardware.cpu.architecture}`,
|
|
735
|
+
`${chalk.cyan('RAM:')} ${ramColor(hardware.memory.total + 'GB')}`,
|
|
736
|
+
`${chalk.cyan('GPU:')} ${gpuColor(hardware.gpu.model || 'Not detected')}`,
|
|
737
|
+
`${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)')}`,
|
|
738
|
+
];
|
|
739
|
+
|
|
740
|
+
const tier = analysis.summary.hardwareTier?.replace('_', ' ').toUpperCase() || 'UNKNOWN';
|
|
741
|
+
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
|
|
742
|
+
|
|
743
|
+
lines.push(`${chalk.bold('Hardware Tier:')} ${tierColor.bold(tier)}`);
|
|
744
|
+
|
|
745
|
+
console.log('\n' + chalk.bgBlue.white.bold(' SYSTEM INFORMATION '));
|
|
746
|
+
console.log(chalk.blue('╭' + '─'.repeat(50)));
|
|
747
|
+
|
|
748
|
+
lines.forEach(line => {
|
|
749
|
+
console.log(chalk.blue('│') + ' ' + line);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
console.log(chalk.blue('╰'));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function displayOllamaIntegration(ollamaInfo, ollamaModels) {
|
|
756
|
+
const lines = [];
|
|
757
|
+
|
|
758
|
+
if (ollamaInfo.available) {
|
|
759
|
+
lines.push(`${chalk.green('✅ Status:')} Running ${chalk.gray(`(v${ollamaInfo.version || 'unknown'})`)}`);
|
|
760
|
+
|
|
761
|
+
if (ollamaModels && ollamaModels.length > 0) {
|
|
762
|
+
const compatibleCount = ollamaModels.filter(m => {
|
|
763
|
+
return m.canRun === true ||
|
|
764
|
+
m.compatibilityScore >= 60 ||
|
|
765
|
+
(m.matchedModel && true);
|
|
766
|
+
}).length;
|
|
767
|
+
|
|
768
|
+
const runningCount = ollamaModels.filter(m => m.isRunning).length;
|
|
769
|
+
|
|
770
|
+
lines.push(`${chalk.cyan('Installed:')} ${ollamaModels.length} total, ${chalk.green(compatibleCount)} compatible`);
|
|
771
|
+
if (runningCount > 0) {
|
|
772
|
+
lines.push(`${chalk.cyan('Running:')} ${chalk.green(runningCount)} models`);
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
lines.push(`${chalk.gray('No models installed yet')}`);
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
lines.push(`${chalk.red('Status:')} Not available`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log('\n' + chalk.bgMagenta.white.bold(' OLLAMA INTEGRATION '));
|
|
782
|
+
console.log(chalk.hex('#a259ff')('╭' + '─'.repeat(50)));
|
|
783
|
+
|
|
784
|
+
lines.forEach(line => {
|
|
785
|
+
console.log(chalk.hex('#a259ff')('│') + ' ' + line);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
console.log(chalk.hex('#a259ff')('╰'));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function displayEnhancedCompatibleModels(compatible, ollamaModels) {
|
|
792
|
+
if (compatible.length === 0) {
|
|
793
|
+
console.log('\n' + chalk.yellow('No compatible models found.'));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
console.log('\n' + chalk.green.bold(' ✅ Compatible Models (Score ≥ 75)'));
|
|
798
|
+
|
|
799
|
+
const data = [
|
|
800
|
+
[
|
|
801
|
+
chalk.bgGreen.white.bold(' Model '),
|
|
802
|
+
chalk.bgGreen.white.bold(' Size '),
|
|
803
|
+
chalk.bgGreen.white.bold(' Score '),
|
|
804
|
+
chalk.bgGreen.white.bold(' RAM '),
|
|
805
|
+
chalk.bgGreen.white.bold(' VRAM '),
|
|
806
|
+
chalk.bgGreen.white.bold(' Speed '),
|
|
807
|
+
chalk.bgGreen.white.bold(' Status ')
|
|
808
|
+
]
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
compatible.slice(0, 15).forEach(model => {
|
|
812
|
+
const tokensPerSec = model.performanceEstimate?.estimatedTokensPerSecond || 'N/A';
|
|
813
|
+
const ramReq = model.requirements?.ram || 1;
|
|
814
|
+
const vramReq = model.requirements?.vram || 0;
|
|
815
|
+
const speedFormatted = formatSpeed(model.performance?.speed || 'medium');
|
|
816
|
+
const scoreColor = getScoreColor(model.score || 0);
|
|
817
|
+
const scoreDisplay = scoreColor(`${model.score || 0}/100`);
|
|
818
|
+
|
|
819
|
+
let statusDisplay = `${tokensPerSec}t/s`;
|
|
820
|
+
if (model.isOllamaInstalled) {
|
|
821
|
+
const ollamaInfo = model.ollamaInfo || {};
|
|
822
|
+
if (ollamaInfo.isRunning) {
|
|
823
|
+
statusDisplay = 'Running';
|
|
824
|
+
} else {
|
|
825
|
+
statusDisplay = 'Installed';
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
let modelName = model.name;
|
|
830
|
+
if (model.isOllamaInstalled) {
|
|
831
|
+
modelName = `${model.name}`;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const row = [
|
|
835
|
+
modelName,
|
|
836
|
+
formatSize(model.size || 'Unknown'),
|
|
837
|
+
scoreDisplay,
|
|
838
|
+
`${ramReq}GB`,
|
|
839
|
+
`${vramReq}GB`,
|
|
840
|
+
speedFormatted,
|
|
841
|
+
statusDisplay
|
|
842
|
+
];
|
|
843
|
+
data.push(row);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
console.log(table(data));
|
|
847
|
+
|
|
848
|
+
if (compatible.length > 15) {
|
|
849
|
+
console.log(chalk.gray(`\n... and ${compatible.length - 15} more compatible models`));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
displayCompatibleModelsSummary(compatible.length);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function displayCompatibleModelsSummary(count) {
|
|
856
|
+
console.log('\n' + chalk.bgMagenta.white.bold(' COMPATIBLE MODELS '));
|
|
857
|
+
console.log(chalk.hex('#a259ff')('╭' + '─'.repeat(40)));
|
|
858
|
+
console.log(chalk.hex('#a259ff')('│') + ` Total compatible models: ${chalk.green.bold(count)}`);
|
|
859
|
+
console.log(chalk.hex('#a259ff')('╰'));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function displayMarginalModels(marginal) {
|
|
863
|
+
if (marginal.length === 0) return;
|
|
864
|
+
|
|
865
|
+
console.log('\n' + chalk.yellow.bold('Marginal Performance (Score 60-74)'));
|
|
866
|
+
|
|
867
|
+
const data = [
|
|
868
|
+
[
|
|
869
|
+
chalk.bgYellow.white.bold(' Model '),
|
|
870
|
+
chalk.bgYellow.white.bold(' Size '),
|
|
871
|
+
chalk.bgYellow.white.bold(' Score '),
|
|
872
|
+
chalk.bgYellow.white.bold(' RAM '),
|
|
873
|
+
chalk.bgYellow.white.bold(' VRAM '),
|
|
874
|
+
chalk.bgYellow.white.bold(' Issue ')
|
|
875
|
+
]
|
|
876
|
+
];
|
|
877
|
+
|
|
878
|
+
marginal.slice(0, 6).forEach(model => {
|
|
879
|
+
const mainIssue = model.issues?.[0] || 'Performance limitations';
|
|
880
|
+
const scoreColor = getScoreColor(model.score || 0);
|
|
881
|
+
const scoreDisplay = scoreColor(`${model.score || 0}/100`);
|
|
882
|
+
|
|
883
|
+
const ramReq = model.requirements?.ram || 1;
|
|
884
|
+
const vramReq = model.requirements?.vram || 0;
|
|
885
|
+
|
|
886
|
+
const truncatedIssue = mainIssue.length > 30 ? mainIssue.substring(0, 27) + '...' : mainIssue;
|
|
887
|
+
|
|
888
|
+
const row = [
|
|
889
|
+
model.name,
|
|
890
|
+
formatSize(model.size || 'Unknown'),
|
|
891
|
+
scoreDisplay,
|
|
892
|
+
`${ramReq}GB`,
|
|
893
|
+
`${vramReq}GB`,
|
|
894
|
+
truncatedIssue
|
|
895
|
+
];
|
|
896
|
+
data.push(row);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
console.log(table(data));
|
|
900
|
+
|
|
901
|
+
if (marginal.length > 6) {
|
|
902
|
+
console.log(chalk.gray(`\n... and ${marginal.length - 6} more marginal models`));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
function displayStructuredRecommendations(recommendations) {
|
|
908
|
+
if (!recommendations) return;
|
|
909
|
+
|
|
910
|
+
if (Array.isArray(recommendations)) {
|
|
911
|
+
displayLegacyRecommendations(recommendations);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
console.log('\n' + chalk.bgCyan.white.bold(' SMART RECOMMENDATIONS '));
|
|
916
|
+
console.log(chalk.cyan('╭' + '─'.repeat(50)));
|
|
917
|
+
|
|
918
|
+
if (recommendations.general && recommendations.general.length > 0) {
|
|
919
|
+
console.log(chalk.cyan('│') + ` ${chalk.bold.white('General Recommendations:')}`);
|
|
920
|
+
recommendations.general.slice(0, 4).forEach((rec, index) => {
|
|
921
|
+
console.log(chalk.cyan('│') + ` ${index + 1}. ${chalk.white(rec)}`);
|
|
922
|
+
});
|
|
923
|
+
console.log(chalk.cyan('│'));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (recommendations.installedModels && recommendations.installedModels.length > 0) {
|
|
927
|
+
console.log(chalk.cyan('│') + ` ${chalk.bold.green('Your Installed Ollama Models:')}`);
|
|
928
|
+
recommendations.installedModels.forEach(rec => {
|
|
929
|
+
console.log(chalk.cyan('│') + ` ${chalk.green(rec)}`);
|
|
930
|
+
});
|
|
931
|
+
console.log(chalk.cyan('│'));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (recommendations.cloudSuggestions && recommendations.cloudSuggestions.length > 0) {
|
|
935
|
+
console.log(chalk.cyan('│') + ` ${chalk.bold.blue('Recommended from Ollama Cloud:')}`);
|
|
936
|
+
recommendations.cloudSuggestions.forEach(rec => {
|
|
937
|
+
if (rec.includes('ollama pull')) {
|
|
938
|
+
console.log(chalk.cyan('│') + ` ${chalk.cyan.bold(rec)}`);
|
|
939
|
+
} else {
|
|
940
|
+
console.log(chalk.cyan('│') + ` ${chalk.blue(rec)}`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
console.log(chalk.cyan('│'));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (recommendations.quickCommands && recommendations.quickCommands.length > 0) {
|
|
947
|
+
console.log(chalk.cyan('│') + ` ${chalk.bold.yellow('⚡ Quick Commands:')}`);
|
|
948
|
+
const uniqueCommands = [...new Set(recommendations.quickCommands)];
|
|
949
|
+
uniqueCommands.slice(0, 3).forEach(cmd => {
|
|
950
|
+
console.log(chalk.cyan('│') + ` > ${chalk.yellow.bold(cmd)}`);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
console.log(chalk.cyan('╰'));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function displayLegacyRecommendations(recommendations) {
|
|
958
|
+
if (!recommendations || recommendations.length === 0) return;
|
|
959
|
+
|
|
960
|
+
const generalRecs = [];
|
|
961
|
+
const ollamaFoundRecs = [];
|
|
962
|
+
const quickInstallRecs = [];
|
|
963
|
+
|
|
964
|
+
recommendations.forEach(rec => {
|
|
965
|
+
if (rec.includes('Score:')) {
|
|
966
|
+
ollamaFoundRecs.push(rec);
|
|
967
|
+
} else if (rec.includes('ollama pull')) {
|
|
968
|
+
quickInstallRecs.push(rec);
|
|
969
|
+
} else if (rec.includes('ollama run')) {
|
|
970
|
+
quickInstallRecs.push(rec);
|
|
971
|
+
} else {
|
|
972
|
+
generalRecs.push(rec);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
console.log('\n' + chalk.bgCyan.white.bold(' SMART RECOMMENDATIONS '));
|
|
977
|
+
console.log(chalk.cyan('╭' + '─'.repeat(40)));
|
|
978
|
+
|
|
979
|
+
generalRecs.slice(0, 8).forEach((rec, index) => {
|
|
980
|
+
const number = chalk.green.bold(`${index + 1}.`);
|
|
981
|
+
console.log(chalk.cyan('│') + ` ${number} ${chalk.white(rec)}`);
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
if (ollamaFoundRecs.length > 0) {
|
|
985
|
+
console.log(chalk.cyan('│'));
|
|
986
|
+
console.log(chalk.cyan('│') + ` ${chalk.bold.green('Your Installed Ollama Models:')}`);
|
|
987
|
+
ollamaFoundRecs.forEach(rec => {
|
|
988
|
+
console.log(chalk.cyan('│') + ` ${chalk.green(rec)}`);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (quickInstallRecs.length > 0) {
|
|
993
|
+
console.log(chalk.cyan('│'));
|
|
994
|
+
console.log(chalk.cyan('│') + ` ${chalk.bold.blue('Quick Commands:')}`);
|
|
995
|
+
quickInstallRecs.slice(0, 3).forEach(cmd => {
|
|
996
|
+
console.log(chalk.cyan('│') + ` > ${chalk.cyan.bold(cmd)}`);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
console.log(chalk.cyan('╰'));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function displayIntelligentRecommendations(intelligentData) {
|
|
1004
|
+
if (!intelligentData || !intelligentData.summary) return;
|
|
1005
|
+
|
|
1006
|
+
const { summary, recommendations } = intelligentData;
|
|
1007
|
+
const tier = summary.hardware_tier.replace('_', ' ').toUpperCase();
|
|
1008
|
+
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
|
|
1009
|
+
|
|
1010
|
+
console.log('\n' + chalk.bgRed.white.bold(' INTELLIGENT RECOMMENDATIONS BY CATEGORY '));
|
|
1011
|
+
console.log(chalk.red('╭' + '─'.repeat(65)));
|
|
1012
|
+
console.log(chalk.red('│') + ` Hardware Tier: ${tierColor.bold(tier)} | Models Analyzed: ${chalk.cyan.bold(intelligentData.totalModelsAnalyzed)}`);
|
|
1013
|
+
console.log(chalk.red('│'));
|
|
1014
|
+
|
|
1015
|
+
// Mostrar mejor modelo general
|
|
1016
|
+
if (summary.best_overall) {
|
|
1017
|
+
const best = summary.best_overall;
|
|
1018
|
+
console.log(chalk.red('│') + ` ${chalk.bold.yellow('BEST OVERALL:')} ${chalk.green.bold(best.name)}`);
|
|
1019
|
+
console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(best.command)}`);
|
|
1020
|
+
console.log(chalk.red('│') + ` Score: ${chalk.yellow.bold(best.score)}/100 | Category: ${chalk.magenta(best.category)}`);
|
|
1021
|
+
console.log(chalk.red('│'));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Mostrar por categorías
|
|
1025
|
+
const categories = {
|
|
1026
|
+
coding: 'Coding',
|
|
1027
|
+
talking: 'Chat',
|
|
1028
|
+
reading: 'Reading',
|
|
1029
|
+
reasoning: 'Reasoning',
|
|
1030
|
+
multimodal: 'Multimodal',
|
|
1031
|
+
creative: 'Creative',
|
|
1032
|
+
general: 'General'
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
Object.entries(summary.by_category).forEach(([category, model]) => {
|
|
1036
|
+
const icon = categories[category] || 'Other';
|
|
1037
|
+
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
|
|
1038
|
+
const scoreColor = getScoreColor(model.score);
|
|
1039
|
+
|
|
1040
|
+
console.log(chalk.red('│') + ` ${chalk.bold.white(categoryName)} (${icon}):`);
|
|
1041
|
+
console.log(chalk.red('│') + ` ${chalk.green(model.name)} (${model.size})`);
|
|
1042
|
+
console.log(chalk.red('│') + ` Score: ${scoreColor.bold(model.score)}/100 | Pulls: ${chalk.gray(model.pulls?.toLocaleString() || 'N/A')}`);
|
|
1043
|
+
console.log(chalk.red('│') + ` Command: ${chalk.cyan.bold(model.command)}`);
|
|
1044
|
+
console.log(chalk.red('│'));
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
console.log(chalk.red('╰'));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function displayModelsStats(originalCount, filteredCount, options) {
|
|
1051
|
+
console.log('\n' + chalk.bgGreen.white.bold(' DATABASE STATS '));
|
|
1052
|
+
console.log(chalk.green('╭' + '─'.repeat(60)));
|
|
1053
|
+
console.log(chalk.green('│') + ` Total models in database: ${chalk.cyan.bold(originalCount)}`);
|
|
1054
|
+
console.log(chalk.green('│') + ` After filters: ${chalk.yellow.bold(filteredCount)}`);
|
|
1055
|
+
|
|
1056
|
+
if (options.category) {
|
|
1057
|
+
console.log(chalk.green('│') + ` Category filter: ${chalk.magenta.bold(options.category)}`);
|
|
1058
|
+
}
|
|
1059
|
+
if (options.size) {
|
|
1060
|
+
console.log(chalk.green('│') + ` Size filter: ${chalk.magenta.bold(options.size)}`);
|
|
1061
|
+
}
|
|
1062
|
+
if (options.popular) {
|
|
1063
|
+
console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Popular models only (>100k pulls)')}`);
|
|
1064
|
+
}
|
|
1065
|
+
if (options.recent) {
|
|
1066
|
+
console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Recent models only')}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
console.log(chalk.green('╰'));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function displayTopRecommended(models, categoryFilter) {
|
|
1073
|
+
console.log('\n' + chalk.bgGreen.white.bold(' TOP 3 RECOMMENDED FOR YOUR HARDWARE '));
|
|
1074
|
+
|
|
1075
|
+
try {
|
|
1076
|
+
const DeterministicModelSelector = require('../src/models/deterministic-selector.js');
|
|
1077
|
+
const selector = new DeterministicModelSelector();
|
|
1078
|
+
|
|
1079
|
+
// Use deterministic selector to get top 3 for this category
|
|
1080
|
+
const result = await selector.selectModels(categoryFilter || 'general', {
|
|
1081
|
+
topN: 3,
|
|
1082
|
+
enableProbe: false,
|
|
1083
|
+
silent: true
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
const top3 = result.candidates.map(candidate => selector.mapCandidateToLegacyFormat(candidate));
|
|
1087
|
+
|
|
1088
|
+
if (top3.length === 0) {
|
|
1089
|
+
console.log(chalk.green('│') + chalk.yellow(' No models found for this category with current hardware'));
|
|
1090
|
+
console.log(chalk.green('╰' + '─'.repeat(65)));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
top3.forEach((model, index) => {
|
|
1095
|
+
const rankEmoji = ['🥇', '🥈', '🥉'][index];
|
|
1096
|
+
const categoryColor = getCategoryColor(model.category || categoryFilter || 'general');
|
|
1097
|
+
const scoreColor = model.categoryScore >= 80 ? chalk.green.bold :
|
|
1098
|
+
model.categoryScore >= 60 ? chalk.yellow : chalk.red;
|
|
1099
|
+
const size = model.size ? `${model.size}B` : 'Unknown';
|
|
1100
|
+
|
|
1101
|
+
console.log(chalk.green('│'));
|
|
1102
|
+
console.log(chalk.green('│') + ` ${rankEmoji} ${chalk.cyan.bold(model.model_identifier)}`);
|
|
1103
|
+
console.log(chalk.green('│') + ` Size: ${chalk.green(size)} | Score: ${scoreColor(Math.round(model.categoryScore) + '%')} | Category: ${categoryColor(model.category || 'general')}`);
|
|
1104
|
+
console.log(chalk.green('│') + ` Command: ${chalk.yellow.bold('ollama pull ' + model.model_identifier)}`);
|
|
1105
|
+
console.log(chalk.green('│') + ` ${chalk.gray(`Hardware: ${Math.round(model.hardwareScore)}/100, Quality: ${Math.round(model.specializationScore)}/100, Speed: ${Math.round(model.efficiencyScore)}/100`)}`);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
console.log(chalk.green('╰' + '─'.repeat(65)));
|
|
1109
|
+
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
console.log(chalk.green('│') + chalk.red(' Error calculating intelligent recommendations: ' + error.message));
|
|
1112
|
+
console.log(chalk.green('╰' + '─'.repeat(65)));
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async function displayCompactModelsList(models, categoryFilter = null) {
|
|
1117
|
+
// Si hay modelos con compatibilityScore, mostrar top 3 recomendados primero
|
|
1118
|
+
const showCompatibility = models.length > 0 && models[0].compatibilityScore !== undefined;
|
|
1119
|
+
|
|
1120
|
+
if (showCompatibility && categoryFilter) {
|
|
1121
|
+
await displayTopRecommended(models, categoryFilter);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
console.log('\n' + chalk.bgBlue.white.bold(' 📋 MODELS LIST '));
|
|
1125
|
+
|
|
1126
|
+
const headers = [
|
|
1127
|
+
chalk.bgBlue.white.bold(' # '),
|
|
1128
|
+
chalk.bgBlue.white.bold(' Model '),
|
|
1129
|
+
chalk.bgBlue.white.bold(' Size ')
|
|
1130
|
+
];
|
|
1131
|
+
|
|
1132
|
+
if (showCompatibility) {
|
|
1133
|
+
headers.push(chalk.bgBlue.white.bold(' Score '));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
headers.push(
|
|
1137
|
+
chalk.bgBlue.white.bold(' Context '),
|
|
1138
|
+
chalk.bgBlue.white.bold(' Input '),
|
|
1139
|
+
chalk.bgBlue.white.bold(' Category ')
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1142
|
+
const data = [headers];
|
|
1143
|
+
|
|
1144
|
+
let rowIndex = 0;
|
|
1145
|
+
models.forEach((model) => {
|
|
1146
|
+
const category = model.category || 'general';
|
|
1147
|
+
const categoryColor = getCategoryColor(category);
|
|
1148
|
+
|
|
1149
|
+
// Context length
|
|
1150
|
+
const contextLength = model.context_length || 'Unknown';
|
|
1151
|
+
|
|
1152
|
+
// Input types
|
|
1153
|
+
const inputTypes = (model.input_types && model.input_types.length > 0) ?
|
|
1154
|
+
model.input_types.slice(0, 2).join(',') : 'text';
|
|
1155
|
+
|
|
1156
|
+
// Si el modelo tiene tags/variantes, crear una fila por cada tag
|
|
1157
|
+
if (model.tags && model.tags.length > 0) {
|
|
1158
|
+
model.tags.forEach((tag) => {
|
|
1159
|
+
rowIndex++;
|
|
1160
|
+
|
|
1161
|
+
// Extraer el tamaño del tag si está presente
|
|
1162
|
+
const tagSize = extractSizeFromIdentifier(tag) ||
|
|
1163
|
+
model.main_size ||
|
|
1164
|
+
(model.model_sizes && model.model_sizes[0]) ||
|
|
1165
|
+
'Unknown';
|
|
1166
|
+
|
|
1167
|
+
const row = [
|
|
1168
|
+
chalk.gray(`${rowIndex}`),
|
|
1169
|
+
tag, // Mostrar el tag completo como nombre del modelo
|
|
1170
|
+
chalk.green(tagSize)
|
|
1171
|
+
];
|
|
1172
|
+
|
|
1173
|
+
// Agregar score si está disponible
|
|
1174
|
+
if (showCompatibility) {
|
|
1175
|
+
const score = model.compatibilityScore || 0;
|
|
1176
|
+
const scoreColor = score >= 80 ? chalk.green.bold :
|
|
1177
|
+
score >= 60 ? chalk.yellow : chalk.red;
|
|
1178
|
+
row.push(scoreColor(`${score}%`));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
row.push(
|
|
1182
|
+
chalk.blue(contextLength),
|
|
1183
|
+
chalk.magenta(inputTypes),
|
|
1184
|
+
categoryColor(category)
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
data.push(row);
|
|
1188
|
+
});
|
|
1189
|
+
} else {
|
|
1190
|
+
// Si no tiene tags, mostrar el modelo base
|
|
1191
|
+
rowIndex++;
|
|
1192
|
+
|
|
1193
|
+
const mainSize = model.main_size ||
|
|
1194
|
+
(model.model_sizes && model.model_sizes[0]) ||
|
|
1195
|
+
extractSizeFromIdentifier(model.model_identifier) ||
|
|
1196
|
+
'Unknown';
|
|
1197
|
+
|
|
1198
|
+
const row = [
|
|
1199
|
+
chalk.gray(`${rowIndex}`),
|
|
1200
|
+
model.model_name || model.model_identifier || 'Unknown',
|
|
1201
|
+
chalk.green(mainSize)
|
|
1202
|
+
];
|
|
1203
|
+
|
|
1204
|
+
// Agregar score si está disponible
|
|
1205
|
+
if (showCompatibility) {
|
|
1206
|
+
const score = model.compatibilityScore || 0;
|
|
1207
|
+
const scoreColor = score >= 80 ? chalk.green.bold :
|
|
1208
|
+
score >= 60 ? chalk.yellow : chalk.red;
|
|
1209
|
+
row.push(scoreColor(`${score}%`));
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
row.push(
|
|
1213
|
+
chalk.blue(contextLength),
|
|
1214
|
+
chalk.magenta(inputTypes),
|
|
1215
|
+
categoryColor(category)
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
data.push(row);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
console.log(table(data));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function extractSizeFromIdentifier(identifier) {
|
|
1226
|
+
const sizeMatch = identifier.match(/(\d+\.?\d*[bg])/i);
|
|
1227
|
+
return sizeMatch ? sizeMatch[1].toLowerCase() : null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function displayFullModelsList(models) {
|
|
1231
|
+
console.log('\n' + chalk.bgBlue.white.bold(' 📋 DETAILED MODELS LIST '));
|
|
1232
|
+
|
|
1233
|
+
models.forEach((model, index) => {
|
|
1234
|
+
console.log(`\n${chalk.cyan.bold(`${index + 1}. ${model.model_name}`)}`);
|
|
1235
|
+
console.log(` ${chalk.gray('Identifier:')} ${chalk.yellow(model.model_identifier)}`);
|
|
1236
|
+
console.log(` ${chalk.gray('Size:')} ${chalk.green(model.main_size || 'Unknown')}`);
|
|
1237
|
+
console.log(` ${chalk.gray('Context:')} ${chalk.blue(model.context_length || 'Unknown')}`);
|
|
1238
|
+
console.log(` ${chalk.gray('Input types:')} ${chalk.magenta((model.input_types || ['text']).join(', '))}`);
|
|
1239
|
+
console.log(` ${chalk.gray('Category:')} ${getCategoryColor(model.category || 'general')(model.category || 'general')}`);
|
|
1240
|
+
console.log(` ${chalk.gray('Pulls:')} ${chalk.green((model.pulls || 0).toLocaleString())}`);
|
|
1241
|
+
console.log(` ${chalk.gray('Description:')} ${model.description || model.detailed_description || 'No description'}`);
|
|
1242
|
+
|
|
1243
|
+
if (model.use_cases && model.use_cases.length > 0) {
|
|
1244
|
+
console.log(` ${chalk.gray('Use cases:')} ${model.use_cases.map(uc => chalk.magenta(uc)).join(', ')}`);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (model.tags && model.tags.length > 0) {
|
|
1248
|
+
console.log(` ${chalk.gray(`Available variants (${model.tags.length}):`)} `);
|
|
1249
|
+
// Mostrar las primeras 10 variantes, agrupadas de 5 por línea
|
|
1250
|
+
const tagsToShow = model.tags.slice(0, 15);
|
|
1251
|
+
for (let i = 0; i < tagsToShow.length; i += 5) {
|
|
1252
|
+
const batch = tagsToShow.slice(i, i + 5);
|
|
1253
|
+
console.log(` ${batch.map(tag => chalk.blue(tag)).join(', ')}`);
|
|
1254
|
+
}
|
|
1255
|
+
if (model.tags.length > 15) {
|
|
1256
|
+
console.log(` ${chalk.gray(`... and ${model.tags.length - 15} more variants`)}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (model.quantizations && model.quantizations.length > 0) {
|
|
1261
|
+
console.log(` ${chalk.gray('Quantizations found:')} ${model.quantizations.map(q => chalk.green(q)).join(', ')}`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
console.log(` ${chalk.gray('Base command:')} ${chalk.cyan.bold(`ollama pull ${model.model_identifier}`)}`);
|
|
1265
|
+
console.log(` ${chalk.gray('Example variant:')} ${chalk.cyan.bold(`ollama pull ${model.tags && model.tags.length > 0 ? model.tags[0] : model.model_identifier}`)}`);
|
|
1266
|
+
console.log(` ${chalk.gray('Updated:')} ${model.last_updated || 'Unknown'}`);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function getCategoryColor(category) {
|
|
1271
|
+
const colors = {
|
|
1272
|
+
coding: chalk.blue,
|
|
1273
|
+
talking: chalk.green,
|
|
1274
|
+
reading: chalk.yellow,
|
|
1275
|
+
reasoning: chalk.red,
|
|
1276
|
+
multimodal: chalk.magenta,
|
|
1277
|
+
creative: chalk.cyan,
|
|
1278
|
+
general: chalk.gray,
|
|
1279
|
+
chat: chalk.green,
|
|
1280
|
+
embeddings: chalk.blue
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
return colors[category] || chalk.gray;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function displaySampleCommands(topModels) {
|
|
1287
|
+
console.log('\n' + chalk.bgYellow.black.bold(' ⚡ SAMPLE COMMANDS '));
|
|
1288
|
+
console.log(chalk.yellow('╭' + '─'.repeat(60)));
|
|
1289
|
+
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Try these popular models:')}`);
|
|
1290
|
+
|
|
1291
|
+
topModels.forEach((model, index) => {
|
|
1292
|
+
const command = `ollama pull ${model.model_identifier}`;
|
|
1293
|
+
console.log(chalk.yellow('│') + ` ${index + 1}. ${chalk.cyan.bold(command)}`);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
console.log(chalk.yellow('│'));
|
|
1297
|
+
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Browse models by category:')}`);
|
|
1298
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category coding')} ${chalk.gray('(Programming & development)')}`);
|
|
1299
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category reasoning')} ${chalk.gray('(Logic & math problems)')}`);
|
|
1300
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category talking')} ${chalk.gray('(Chat & conversations)')}`);
|
|
1301
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category reading')} ${chalk.gray('(Text analysis & comprehension)')}`);
|
|
1302
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category multimodal')} ${chalk.gray('(Image & vision tasks)')}`);
|
|
1303
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category creative')} ${chalk.gray('(Creative writing & stories)')}`);
|
|
1304
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker list-models --category general')} ${chalk.gray('(General purpose tasks)')}`);
|
|
1305
|
+
console.log(chalk.yellow('│'));
|
|
1306
|
+
console.log(chalk.yellow('│') + ` ${chalk.bold.white('AI-powered selection:')}`);
|
|
1307
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker ai-check --category coding --top 12')} ${chalk.gray('(AI meta-evaluation)')}`);
|
|
1308
|
+
console.log(chalk.yellow('│') + ` ${chalk.cyan('llm-checker ai-run')} ${chalk.gray('(Smart model selection & launch)')}`);
|
|
1309
|
+
console.log(chalk.yellow('│'));
|
|
1310
|
+
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Additional options:')}`);
|
|
1311
|
+
console.log(chalk.yellow('│') + ` ${chalk.gray('llm-checker list-models --popular --limit 10')}`);
|
|
1312
|
+
console.log(chalk.yellow('│') + ` ${chalk.gray('llm-checker list-models --json > models.json')}`);
|
|
1313
|
+
console.log(chalk.yellow('╰'));
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function checkIfModelInstalled(model, ollamaInfo) {
|
|
1317
|
+
try {
|
|
1318
|
+
// Si Ollama no está disponible, no hay modelos instalados
|
|
1319
|
+
if (!ollamaInfo || !ollamaInfo.available) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Ejecutar 'ollama list' para obtener modelos instalados
|
|
1324
|
+
const installedModels = await new Promise((resolve, reject) => {
|
|
1325
|
+
try {
|
|
1326
|
+
const ollama = spawn('ollama', ['list'], { stdio: 'pipe' });
|
|
1327
|
+
let output = '';
|
|
1328
|
+
|
|
1329
|
+
ollama.stdout.on('data', (data) => {
|
|
1330
|
+
output += data.toString();
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
ollama.on('close', (code) => {
|
|
1334
|
+
if (code === 0) {
|
|
1335
|
+
resolve(output);
|
|
1336
|
+
} else {
|
|
1337
|
+
resolve(''); // Si falla, asumimos que no hay modelos
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
ollama.on('error', (err) => {
|
|
1342
|
+
// Handle ENOENT and other spawn errors gracefully
|
|
1343
|
+
if (err.code === 'ENOENT') {
|
|
1344
|
+
resolve(''); // Ollama not found, no models installed
|
|
1345
|
+
} else {
|
|
1346
|
+
resolve(''); // Any other error, assume no models
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
} catch (spawnError) {
|
|
1350
|
+
// Handle synchronous spawn errors
|
|
1351
|
+
resolve(''); // If spawn itself fails, no models available
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// Parsear la salida de 'ollama list'
|
|
1356
|
+
const lines = installedModels.split('\n');
|
|
1357
|
+
const modelNames = [];
|
|
1358
|
+
|
|
1359
|
+
for (let i = 1; i < lines.length; i++) { // Skip header
|
|
1360
|
+
const line = lines[i].trim();
|
|
1361
|
+
if (line) {
|
|
1362
|
+
const parts = line.split(/\s+/);
|
|
1363
|
+
if (parts.length > 0) {
|
|
1364
|
+
modelNames.push(parts[0].toLowerCase());
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// Generar el comando de instalación esperado para el modelo
|
|
1370
|
+
const expectedCommand = getOllamaInstallCommand(model);
|
|
1371
|
+
if (!expectedCommand) return false;
|
|
1372
|
+
|
|
1373
|
+
// Extraer el nombre del modelo del comando (ej: "ollama pull mistral:7b" -> "mistral:7b")
|
|
1374
|
+
const modelNameMatch = expectedCommand.match(/ollama pull (.+)/);
|
|
1375
|
+
if (!modelNameMatch) return false;
|
|
1376
|
+
|
|
1377
|
+
const expectedModelName = modelNameMatch[1].toLowerCase();
|
|
1378
|
+
|
|
1379
|
+
// Verificar si el modelo está en la lista de instalados
|
|
1380
|
+
return modelNames.some(installedName =>
|
|
1381
|
+
installedName === expectedModelName ||
|
|
1382
|
+
installedName.startsWith(expectedModelName.split(':')[0])
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
// Si hay algún error, asumimos que no está instalado
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function displaySimplifiedSystemInfo(hardware) {
|
|
1392
|
+
console.log(chalk.cyan.bold('\nSYSTEM SUMMARY'));
|
|
1393
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
1394
|
+
|
|
1395
|
+
const cpuInfo = `${hardware.cpu.brand} (${hardware.cpu.cores} cores)`;
|
|
1396
|
+
const memInfo = `${hardware.memory.total}GB RAM`;
|
|
1397
|
+
const gpuInfo = hardware.gpu.model || 'Integrated GPU';
|
|
1398
|
+
|
|
1399
|
+
console.log(`CPU: ${chalk.white(cpuInfo)}`);
|
|
1400
|
+
console.log(`Memory: ${chalk.white(memInfo)}`);
|
|
1401
|
+
console.log(`GPU: ${chalk.white(gpuInfo)}`);
|
|
1402
|
+
console.log(`Architecture: ${chalk.white(hardware.cpu.architecture)}`);
|
|
1403
|
+
|
|
1404
|
+
const tier = getHardwareTierForDisplay(hardware);
|
|
1405
|
+
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
|
|
1406
|
+
console.log(`Hardware Tier: ${tierColor.bold(tier)}`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
async function displayModelRecommendations(analysis, hardware, useCase = 'general', limit = 1) {
|
|
1410
|
+
const title = limit === 1 ? 'RECOMMENDED MODEL' : `TOP ${limit} COMPATIBLE MODELS`;
|
|
1411
|
+
console.log(chalk.green.bold(`\n${title}`));
|
|
1412
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
1413
|
+
|
|
1414
|
+
// Find the best models from compatible models considering use case
|
|
1415
|
+
let selectedModels = [];
|
|
1416
|
+
let reason = '';
|
|
1417
|
+
|
|
1418
|
+
if (analysis.compatible && analysis.compatible.length > 0) {
|
|
1419
|
+
// First, try to find models that match the use case
|
|
1420
|
+
let candidateModels = analysis.compatible;
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
// Apply intelligent filtering based on use case
|
|
1424
|
+
if (useCase && useCase !== 'general') {
|
|
1425
|
+
// Specific use case filtering
|
|
1426
|
+
const useCaseModels = candidateModels.filter(model => {
|
|
1427
|
+
const specialization = model.specialization?.toLowerCase();
|
|
1428
|
+
const category = model.category?.toLowerCase();
|
|
1429
|
+
|
|
1430
|
+
const lowerUseCase = useCase.toLowerCase();
|
|
1431
|
+
switch (lowerUseCase) {
|
|
1432
|
+
case 'coding':
|
|
1433
|
+
case 'code':
|
|
1434
|
+
return model.primary_category === 'coding' ||
|
|
1435
|
+
model.categories?.includes('coding') ||
|
|
1436
|
+
specialization === 'code' || category === 'coding' ||
|
|
1437
|
+
model.name.toLowerCase().includes('code') ||
|
|
1438
|
+
model.name.toLowerCase().includes('coder');
|
|
1439
|
+
|
|
1440
|
+
case 'creative':
|
|
1441
|
+
case 'writing':
|
|
1442
|
+
return model.primary_category === 'creative' ||
|
|
1443
|
+
model.categories?.includes('creative') ||
|
|
1444
|
+
category === 'creative' || specialization === 'creative' ||
|
|
1445
|
+
model.name.toLowerCase().includes('dolphin') ||
|
|
1446
|
+
model.name.toLowerCase().includes('wizard') ||
|
|
1447
|
+
model.name.toLowerCase().includes('uncensored');
|
|
1448
|
+
|
|
1449
|
+
case 'chat':
|
|
1450
|
+
case 'conversation':
|
|
1451
|
+
case 'talking':
|
|
1452
|
+
// Prefer chat models, exclude coding models
|
|
1453
|
+
// First, hard exclude coding models
|
|
1454
|
+
if (model.primary_category === 'coding' ||
|
|
1455
|
+
specialization === 'code' ||
|
|
1456
|
+
model.name.toLowerCase().includes('code') ||
|
|
1457
|
+
model.name.toLowerCase().includes('coder')) {
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
// Then include chat models (coding exclusion above takes precedence)
|
|
1461
|
+
return model.primary_category === 'chat' ||
|
|
1462
|
+
model.categories?.includes('chat') ||
|
|
1463
|
+
category === 'talking' || specialization === 'chat' ||
|
|
1464
|
+
(model.name.toLowerCase().includes('llama') && !model.name.toLowerCase().includes('code')) ||
|
|
1465
|
+
(model.name.toLowerCase().includes('mistral') && !model.name.toLowerCase().includes('code')) ||
|
|
1466
|
+
(model.name.toLowerCase().includes('qwen') && !model.name.toLowerCase().includes('code')) ||
|
|
1467
|
+
(!model.name.toLowerCase().includes('llava') &&
|
|
1468
|
+
(specialization === 'general' || category === 'medium'));
|
|
1469
|
+
|
|
1470
|
+
case 'multimodal':
|
|
1471
|
+
case 'vision':
|
|
1472
|
+
return model.primary_category === 'multimodal' ||
|
|
1473
|
+
model.categories?.includes('multimodal') ||
|
|
1474
|
+
category === 'multimodal' ||
|
|
1475
|
+
model.name.toLowerCase().includes('llava') ||
|
|
1476
|
+
model.name.toLowerCase().includes('vision');
|
|
1477
|
+
|
|
1478
|
+
case 'embeddings':
|
|
1479
|
+
case 'embedings': // typo tolerance
|
|
1480
|
+
case 'embedding':
|
|
1481
|
+
case 'embeding': // typo tolerance
|
|
1482
|
+
return model.primary_category === 'embeddings' ||
|
|
1483
|
+
model.categories?.includes('embeddings') ||
|
|
1484
|
+
category === 'embeddings' ||
|
|
1485
|
+
model.name.toLowerCase().includes('embed') ||
|
|
1486
|
+
model.name.toLowerCase().includes('bge');
|
|
1487
|
+
|
|
1488
|
+
case 'reasoning':
|
|
1489
|
+
case 'reason':
|
|
1490
|
+
return model.primary_category === 'reasoning' ||
|
|
1491
|
+
model.categories?.includes('reasoning') ||
|
|
1492
|
+
category === 'reasoning' ||
|
|
1493
|
+
model.name.toLowerCase().includes('deepseek-r1') ||
|
|
1494
|
+
model.name.toLowerCase().includes('reasoning');
|
|
1495
|
+
|
|
1496
|
+
default:
|
|
1497
|
+
// Check for partial matches with typo tolerance
|
|
1498
|
+
if (lowerUseCase.includes('embed')) {
|
|
1499
|
+
return model.primary_category === 'embeddings' ||
|
|
1500
|
+
model.categories?.includes('embeddings') ||
|
|
1501
|
+
category === 'embeddings' ||
|
|
1502
|
+
model.name.toLowerCase().includes('embed');
|
|
1503
|
+
}
|
|
1504
|
+
if (lowerUseCase.includes('code')) {
|
|
1505
|
+
return model.primary_category === 'coding' ||
|
|
1506
|
+
model.categories?.includes('coding');
|
|
1507
|
+
}
|
|
1508
|
+
if (lowerUseCase.includes('creat')) {
|
|
1509
|
+
return model.primary_category === 'creative' ||
|
|
1510
|
+
model.categories?.includes('creative');
|
|
1511
|
+
}
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
// If we found use case specific models, use those, otherwise search Ollama database
|
|
1517
|
+
if (useCaseModels.length > 0) {
|
|
1518
|
+
candidateModels = useCaseModels;
|
|
1519
|
+
reason = `Best ${useCase} model for your hardware`;
|
|
1520
|
+
} else {
|
|
1521
|
+
// Search directly in Ollama database for use case specific models
|
|
1522
|
+
const ollamaModels = searchOllamaModelsForUseCase(useCase, hardware);
|
|
1523
|
+
if (ollamaModels.length > 0) {
|
|
1524
|
+
candidateModels = ollamaModels;
|
|
1525
|
+
reason = `Best ${useCase} model from Ollama database`;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
// No specific use case - apply intelligent general filtering
|
|
1530
|
+
// First, infer categories for static models that don't have them
|
|
1531
|
+
const modelsWithCategories = candidateModels.map(model => {
|
|
1532
|
+
if (!model.primary_category) {
|
|
1533
|
+
const modelName = model.name.toLowerCase();
|
|
1534
|
+
let inferredCategory = 'general';
|
|
1535
|
+
|
|
1536
|
+
if (modelName.includes('code') || modelName.includes('coder')) {
|
|
1537
|
+
inferredCategory = 'coding';
|
|
1538
|
+
} else if (modelName.includes('llava') || modelName.includes('vision')) {
|
|
1539
|
+
inferredCategory = 'multimodal';
|
|
1540
|
+
} else if (modelName.includes('embed')) {
|
|
1541
|
+
inferredCategory = 'embeddings';
|
|
1542
|
+
} else if (modelName.includes('llama') || modelName.includes('mistral') ||
|
|
1543
|
+
modelName.includes('qwen') || modelName.includes('gemma')) {
|
|
1544
|
+
inferredCategory = 'chat';
|
|
1545
|
+
} else if (modelName.includes('phi') && modelName.includes('mini')) {
|
|
1546
|
+
inferredCategory = 'reasoning';
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
return { ...model, primary_category: inferredCategory };
|
|
1550
|
+
}
|
|
1551
|
+
return model;
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
// Prefer versatile models, exclude highly specialized ones
|
|
1555
|
+
const generalModels = modelsWithCategories.filter(model => {
|
|
1556
|
+
// Exclude very specialized models
|
|
1557
|
+
if (model.primary_category === 'embeddings' ||
|
|
1558
|
+
model.primary_category === 'safety' ||
|
|
1559
|
+
model.primary_category === 'multimodal') {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Include chat, coding, reasoning, creative, and general models
|
|
1564
|
+
return model.primary_category === 'chat' ||
|
|
1565
|
+
model.primary_category === 'coding' ||
|
|
1566
|
+
model.primary_category === 'reasoning' ||
|
|
1567
|
+
model.primary_category === 'creative' ||
|
|
1568
|
+
model.primary_category === 'general' ||
|
|
1569
|
+
model.specialization === 'general' ||
|
|
1570
|
+
model.category === 'medium' ||
|
|
1571
|
+
model.category === 'small';
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
if (generalModels.length > 0) {
|
|
1575
|
+
// Re-score general models with category bonus
|
|
1576
|
+
const scoredModels = generalModels.map(model => {
|
|
1577
|
+
let adjustedScore = model.score || 0;
|
|
1578
|
+
|
|
1579
|
+
// Apply category bonuses for general use
|
|
1580
|
+
if (model.primary_category === 'chat') {
|
|
1581
|
+
adjustedScore += 5; // Chat models are great for general use
|
|
1582
|
+
} else if (model.primary_category === 'coding') {
|
|
1583
|
+
adjustedScore += 3; // Coding models are versatile
|
|
1584
|
+
} else if (model.primary_category === 'reasoning') {
|
|
1585
|
+
adjustedScore += 4; // Reasoning models are smart
|
|
1586
|
+
} else if (model.primary_category === 'creative') {
|
|
1587
|
+
adjustedScore += 2; // Creative models are fun
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
return { ...model, adjustedScore };
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
candidateModels = scoredModels.sort((a, b) => b.adjustedScore - a.adjustedScore);
|
|
1594
|
+
reason = 'Best general-purpose model for your hardware';
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Filter out unreasonably large models before final selection
|
|
1599
|
+
const reasonableSizedModels = candidateModels.filter(model => {
|
|
1600
|
+
const realSize = getRealSizeFromOllamaCache(model);
|
|
1601
|
+
const sizeGB = parseFloat(realSize?.replace(/GB|gb/gi, '') || '0');
|
|
1602
|
+
|
|
1603
|
+
// For hardware with 24GB RAM, models >25GB are not practical
|
|
1604
|
+
const maxReasonableSize = hardware.memory.total > 32 ? 50 : 25;
|
|
1605
|
+
return sizeGB === 0 || sizeGB <= maxReasonableSize; // 0 means unknown/fallback
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
// Sort by score and get the top models (use adjustedScore if available)
|
|
1609
|
+
const sortedModels = reasonableSizedModels.sort((a, b) =>
|
|
1610
|
+
(b.adjustedScore || b.score || 0) - (a.adjustedScore || a.score || 0)
|
|
1611
|
+
);
|
|
1612
|
+
selectedModels = sortedModels.slice(0, limit);
|
|
1613
|
+
|
|
1614
|
+
if (!reason) {
|
|
1615
|
+
reason = 'Highest compatibility score for your hardware';
|
|
1616
|
+
}
|
|
1617
|
+
} else if (analysis.marginal && analysis.marginal.length > 0) {
|
|
1618
|
+
let marginalCandidates = analysis.marginal;
|
|
1619
|
+
|
|
1620
|
+
// Apply same use case filtering to marginal models
|
|
1621
|
+
if (useCase && useCase !== 'general') {
|
|
1622
|
+
const useCaseMarginal = marginalCandidates.filter(model => {
|
|
1623
|
+
const specialization = model.specialization?.toLowerCase();
|
|
1624
|
+
const category = model.category?.toLowerCase();
|
|
1625
|
+
|
|
1626
|
+
const lowerUseCase = useCase.toLowerCase();
|
|
1627
|
+
switch (lowerUseCase) {
|
|
1628
|
+
case 'coding':
|
|
1629
|
+
case 'code':
|
|
1630
|
+
return model.primary_category === 'coding' ||
|
|
1631
|
+
model.categories?.includes('coding') ||
|
|
1632
|
+
specialization === 'code' || category === 'coding' ||
|
|
1633
|
+
model.name.toLowerCase().includes('code') ||
|
|
1634
|
+
model.name.toLowerCase().includes('coder');
|
|
1635
|
+
|
|
1636
|
+
case 'creative':
|
|
1637
|
+
case 'writing':
|
|
1638
|
+
return model.primary_category === 'creative' ||
|
|
1639
|
+
model.categories?.includes('creative') ||
|
|
1640
|
+
category === 'creative' || specialization === 'creative' ||
|
|
1641
|
+
model.name.toLowerCase().includes('dolphin') ||
|
|
1642
|
+
model.name.toLowerCase().includes('wizard') ||
|
|
1643
|
+
model.name.toLowerCase().includes('uncensored');
|
|
1644
|
+
|
|
1645
|
+
case 'chat':
|
|
1646
|
+
case 'conversation':
|
|
1647
|
+
case 'talking':
|
|
1648
|
+
// First, hard exclude coding models
|
|
1649
|
+
if (model.primary_category === 'coding' ||
|
|
1650
|
+
specialization === 'code' ||
|
|
1651
|
+
model.name.toLowerCase().includes('code') ||
|
|
1652
|
+
model.name.toLowerCase().includes('coder')) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
// Then include chat models
|
|
1656
|
+
return model.primary_category === 'chat' ||
|
|
1657
|
+
model.categories?.includes('chat') ||
|
|
1658
|
+
category === 'talking' || specialization === 'chat' ||
|
|
1659
|
+
(model.name.toLowerCase().includes('llama') && !model.name.toLowerCase().includes('code')) ||
|
|
1660
|
+
(model.name.toLowerCase().includes('mistral') && !model.name.toLowerCase().includes('code')) ||
|
|
1661
|
+
(model.name.toLowerCase().includes('qwen') && !model.name.toLowerCase().includes('code')) ||
|
|
1662
|
+
(!model.name.toLowerCase().includes('llava') &&
|
|
1663
|
+
(specialization === 'general' || category === 'medium'));
|
|
1664
|
+
|
|
1665
|
+
case 'multimodal':
|
|
1666
|
+
case 'vision':
|
|
1667
|
+
return model.primary_category === 'multimodal' ||
|
|
1668
|
+
model.categories?.includes('multimodal') ||
|
|
1669
|
+
category === 'multimodal' ||
|
|
1670
|
+
model.name.toLowerCase().includes('llava') ||
|
|
1671
|
+
model.name.toLowerCase().includes('vision');
|
|
1672
|
+
|
|
1673
|
+
case 'embeddings':
|
|
1674
|
+
case 'embedings': // typo tolerance
|
|
1675
|
+
case 'embedding':
|
|
1676
|
+
case 'embeding': // typo tolerance
|
|
1677
|
+
return model.primary_category === 'embeddings' ||
|
|
1678
|
+
model.categories?.includes('embeddings') ||
|
|
1679
|
+
category === 'embeddings' ||
|
|
1680
|
+
model.name.toLowerCase().includes('embed') ||
|
|
1681
|
+
model.name.toLowerCase().includes('bge');
|
|
1682
|
+
|
|
1683
|
+
case 'reasoning':
|
|
1684
|
+
case 'reason':
|
|
1685
|
+
return model.primary_category === 'reasoning' ||
|
|
1686
|
+
model.categories?.includes('reasoning') ||
|
|
1687
|
+
category === 'reasoning' ||
|
|
1688
|
+
model.name.toLowerCase().includes('deepseek-r1') ||
|
|
1689
|
+
model.name.toLowerCase().includes('reasoning');
|
|
1690
|
+
|
|
1691
|
+
default:
|
|
1692
|
+
// Check for partial matches with typo tolerance
|
|
1693
|
+
if (lowerUseCase.includes('embed')) {
|
|
1694
|
+
return model.primary_category === 'embeddings' ||
|
|
1695
|
+
model.categories?.includes('embeddings') ||
|
|
1696
|
+
category === 'embeddings' ||
|
|
1697
|
+
model.name.toLowerCase().includes('embed');
|
|
1698
|
+
}
|
|
1699
|
+
if (lowerUseCase.includes('code')) {
|
|
1700
|
+
return model.primary_category === 'coding' ||
|
|
1701
|
+
model.categories?.includes('coding') ||
|
|
1702
|
+
category === 'coding' ||
|
|
1703
|
+
model.name.toLowerCase().includes('code');
|
|
1704
|
+
}
|
|
1705
|
+
if (lowerUseCase.includes('creat')) {
|
|
1706
|
+
return model.primary_category === 'creative' ||
|
|
1707
|
+
model.categories?.includes('creative') ||
|
|
1708
|
+
category === 'creative';
|
|
1709
|
+
}
|
|
1710
|
+
if (lowerUseCase.includes('chat') || lowerUseCase.includes('talk')) {
|
|
1711
|
+
return model.primary_category === 'chat' ||
|
|
1712
|
+
model.categories?.includes('chat') ||
|
|
1713
|
+
category === 'chat';
|
|
1714
|
+
}
|
|
1715
|
+
if (lowerUseCase.includes('vision') || lowerUseCase.includes('image')) {
|
|
1716
|
+
return model.primary_category === 'multimodal' ||
|
|
1717
|
+
model.categories?.includes('multimodal') ||
|
|
1718
|
+
model.name.toLowerCase().includes('llava');
|
|
1719
|
+
}
|
|
1720
|
+
return true; // Include if no specific pattern matches
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
if (useCaseMarginal.length > 0) {
|
|
1725
|
+
marginalCandidates = useCaseMarginal;
|
|
1726
|
+
reason = `Best ${useCase} model for your hardware`;
|
|
1727
|
+
} else {
|
|
1728
|
+
reason = 'Best available option (marginal performance)';
|
|
1729
|
+
}
|
|
1730
|
+
} else {
|
|
1731
|
+
reason = 'Best available option (marginal performance)';
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const sortedMarginal = marginalCandidates.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1735
|
+
selectedModels = sortedMarginal.slice(0, limit);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (selectedModels && selectedModels.length > 0) {
|
|
1739
|
+
for (let index = 0; index < selectedModels.length; index++) {
|
|
1740
|
+
const model = selectedModels[index];
|
|
1741
|
+
|
|
1742
|
+
if (limit > 1) {
|
|
1743
|
+
const rank = index + 1;
|
|
1744
|
+
const rankColor = rank === 1 ? chalk.yellow : chalk.gray;
|
|
1745
|
+
console.log(`\n${rankColor.bold(`#${rank} - ${model.name}`)}`);
|
|
1746
|
+
} else {
|
|
1747
|
+
console.log(`Model: ${chalk.cyan.bold(model.name)}`);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// Get real size from Ollama cache or estimate
|
|
1751
|
+
const realSize = getRealSizeFromOllamaCache(model) || estimateModelSize(model);
|
|
1752
|
+
console.log(`Size: ${chalk.white(realSize)}`);
|
|
1753
|
+
console.log(`Compatibility Score: ${chalk.green.bold(model.adjustedScore || model.score || 'N/A')}/100`);
|
|
1754
|
+
|
|
1755
|
+
if (index === 0) {
|
|
1756
|
+
console.log(`Reason: ${chalk.gray(reason)}`);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// Show performance if available
|
|
1760
|
+
if (model.performanceEstimate) {
|
|
1761
|
+
console.log(`Estimated Speed: ${chalk.yellow(model.performanceEstimate.estimatedTokensPerSecond || 'N/A')} tokens/sec`);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Check if it's already installed by comparing with Ollama integration
|
|
1765
|
+
let isInstalled = false;
|
|
1766
|
+
try {
|
|
1767
|
+
isInstalled = await checkIfModelInstalled(model, analysis.ollamaInfo);
|
|
1768
|
+
if (isInstalled) {
|
|
1769
|
+
console.log(`Status: ${chalk.green('Already installed in Ollama')}`);
|
|
1770
|
+
} else if (analysis.ollamaInfo && analysis.ollamaInfo.available) {
|
|
1771
|
+
console.log(`Status: ${chalk.gray('Available for installation')}`);
|
|
1772
|
+
} else {
|
|
1773
|
+
console.log(`Status: ${chalk.yellow('Requires Ollama (not detected)')}`);
|
|
1774
|
+
}
|
|
1775
|
+
} catch (installCheckError) {
|
|
1776
|
+
// If checking installation status fails, show based on Ollama availability
|
|
1777
|
+
if (analysis.ollamaInfo && analysis.ollamaInfo.available) {
|
|
1778
|
+
console.log(`Status: ${chalk.gray('Available for installation')}`);
|
|
1779
|
+
} else {
|
|
1780
|
+
console.log(`Status: ${chalk.yellow('Requires Ollama (not detected)')}`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Show pull/run command directly in each model block (Issue #3)
|
|
1785
|
+
const ollamaCommand = getOllamaInstallCommand(model);
|
|
1786
|
+
if (ollamaCommand) {
|
|
1787
|
+
const modelName = extractModelName(ollamaCommand);
|
|
1788
|
+
if (isInstalled) {
|
|
1789
|
+
console.log(`\nCommand: ${chalk.cyan.bold(`ollama run ${modelName}`)}`);
|
|
1790
|
+
} else {
|
|
1791
|
+
console.log(`\nCommand: ${chalk.cyan.bold(ollamaCommand)}`);
|
|
1792
|
+
}
|
|
1793
|
+
} else if (model.ollamaTag || model.ollamaId) {
|
|
1794
|
+
const tag = model.ollamaTag || model.ollamaId;
|
|
1795
|
+
if (isInstalled) {
|
|
1796
|
+
console.log(`\nCommand: ${chalk.cyan.bold(`ollama run ${tag}`)}`);
|
|
1797
|
+
} else {
|
|
1798
|
+
console.log(`\nCommand: ${chalk.cyan.bold(`ollama pull ${tag}`)}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
} else {
|
|
1803
|
+
console.log(chalk.yellow('No compatible models found for your hardware'));
|
|
1804
|
+
console.log(chalk.gray('Try running with --include-cloud to see more options'));
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
return selectedModels;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async function displayQuickStartCommands(analysis, recommendedModel = null, allRecommended = null) {
|
|
1811
|
+
console.log(chalk.yellow.bold('\nQUICK START'));
|
|
1812
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
1813
|
+
|
|
1814
|
+
// Use the first model from allRecommended if available, otherwise fallback to recommendedModel
|
|
1815
|
+
let bestModel = (allRecommended && allRecommended.length > 0) ? allRecommended[0] : recommendedModel;
|
|
1816
|
+
|
|
1817
|
+
if (!bestModel) {
|
|
1818
|
+
if (analysis.compatible && analysis.compatible.length > 0) {
|
|
1819
|
+
const sortedModels = analysis.compatible.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1820
|
+
bestModel = sortedModels[0];
|
|
1821
|
+
} else if (analysis.marginal && analysis.marginal.length > 0) {
|
|
1822
|
+
const sortedMarginal = analysis.marginal.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
1823
|
+
bestModel = sortedMarginal[0];
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
if (analysis.ollamaInfo && !analysis.ollamaInfo.available) {
|
|
1828
|
+
console.log(`1. Install Ollama: ${chalk.underline('https://ollama.ai')}`);
|
|
1829
|
+
console.log(`2. Come back and run this command again`);
|
|
1830
|
+
} else if (bestModel) {
|
|
1831
|
+
let isInstalled = false;
|
|
1832
|
+
try {
|
|
1833
|
+
isInstalled = await checkIfModelInstalled(bestModel, analysis.ollamaInfo);
|
|
1834
|
+
} catch (installCheckError) {
|
|
1835
|
+
// If checking installation status fails, assume not installed
|
|
1836
|
+
isInstalled = false;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (isInstalled) {
|
|
1840
|
+
const ollamaCommand = getOllamaInstallCommand(bestModel);
|
|
1841
|
+
const modelName = ollamaCommand ? extractModelName(ollamaCommand) : bestModel.name.toLowerCase();
|
|
1842
|
+
console.log(`1. Start using your installed model:`);
|
|
1843
|
+
console.log(` ${chalk.cyan.bold(`ollama run ${modelName}`)}`);
|
|
1844
|
+
} else {
|
|
1845
|
+
// Try to find Ollama command
|
|
1846
|
+
const ollamaCommand = getOllamaInstallCommand(bestModel);
|
|
1847
|
+
if (ollamaCommand) {
|
|
1848
|
+
console.log(`1. Install the recommended model:`);
|
|
1849
|
+
console.log(` ${chalk.cyan.bold(ollamaCommand)}`);
|
|
1850
|
+
console.log(`2. Start using it:`);
|
|
1851
|
+
console.log(` ${chalk.cyan.bold(`ollama run ${extractModelName(ollamaCommand)}`)}`);
|
|
1852
|
+
} else {
|
|
1853
|
+
console.log(`1. Search for ${bestModel.name} on Ollama Hub`);
|
|
1854
|
+
console.log(`2. Install and run the model`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// If multiple models were shown, suggest trying alternatives (only reasonable ones)
|
|
1859
|
+
if (allRecommended && allRecommended.length > 1) {
|
|
1860
|
+
console.log(`\n${chalk.gray('Alternative options:')}`);
|
|
1861
|
+
|
|
1862
|
+
// Filter out unreasonable alternatives (>50GB, no ollama command)
|
|
1863
|
+
const reasonableAlternatives = allRecommended.slice(1).filter(model => {
|
|
1864
|
+
const realSize = getRealSizeFromOllamaCache(model);
|
|
1865
|
+
const sizeGB = parseFloat(realSize?.replace(/GB|gb/gi, '') || '0');
|
|
1866
|
+
const ollamaCommand = getOllamaInstallCommand(model);
|
|
1867
|
+
|
|
1868
|
+
// Only show if size is reasonable (<50GB) and has ollama command
|
|
1869
|
+
return sizeGB < 50 && ollamaCommand;
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
// Show max 2 alternatives, avoid duplicating commands
|
|
1873
|
+
const seenCommands = new Set();
|
|
1874
|
+
const bestModelCommand = getOllamaInstallCommand(bestModel);
|
|
1875
|
+
if (bestModelCommand) seenCommands.add(bestModelCommand);
|
|
1876
|
+
|
|
1877
|
+
let alternativeCount = 0;
|
|
1878
|
+
reasonableAlternatives.forEach((model) => {
|
|
1879
|
+
if (alternativeCount >= 2) return; // Max 2 alternatives
|
|
1880
|
+
|
|
1881
|
+
const ollamaCommand = getOllamaInstallCommand(model);
|
|
1882
|
+
if (ollamaCommand && !seenCommands.has(ollamaCommand)) {
|
|
1883
|
+
console.log(` ${chalk.gray(`${alternativeCount + 2}. ${ollamaCommand}`)}`);
|
|
1884
|
+
seenCommands.add(ollamaCommand);
|
|
1885
|
+
alternativeCount++;
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
// If no reasonable alternatives, don't show the section
|
|
1890
|
+
if (reasonableAlternatives.length === 0) {
|
|
1891
|
+
console.log(` ${chalk.gray('No other reasonable alternatives found for your hardware')}`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
} else {
|
|
1895
|
+
console.log(`1. Try expanding search: ${chalk.cyan('llm-checker check --include-cloud')}`);
|
|
1896
|
+
console.log(`2. Or see all available models: ${chalk.cyan('llm-checker list-models')}`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function getOllamaInstallCommand(model) {
|
|
1901
|
+
// Special handling for specific models that need corrected commands
|
|
1902
|
+
const modelName = model.name.toLowerCase();
|
|
1903
|
+
|
|
1904
|
+
if (modelName.includes('codellama') && modelName.includes('7b')) {
|
|
1905
|
+
return 'ollama pull codellama:7b';
|
|
1906
|
+
}
|
|
1907
|
+
if (modelName.includes('mistral') && modelName.includes('7b')) {
|
|
1908
|
+
return 'ollama pull mistral:7b';
|
|
1909
|
+
}
|
|
1910
|
+
if (modelName.includes('llama 3.1') && modelName.includes('8b')) {
|
|
1911
|
+
return 'ollama pull llama3.1:8b';
|
|
1912
|
+
}
|
|
1913
|
+
if (modelName.includes('llama3.1') && !modelName.includes('8b')) {
|
|
1914
|
+
return 'ollama pull llama3.1:8b'; // Default to 8b variant
|
|
1915
|
+
}
|
|
1916
|
+
if (modelName.includes('llama3.2-vision')) {
|
|
1917
|
+
return 'ollama pull llama3.2-vision:latest';
|
|
1918
|
+
}
|
|
1919
|
+
if (modelName.includes('llama3.2')) {
|
|
1920
|
+
return 'ollama pull llama3.2:3b'; // Most common variant
|
|
1921
|
+
}
|
|
1922
|
+
if (modelName.includes('llama3.3')) {
|
|
1923
|
+
return 'ollama pull llama3.3:70b'; // This is the actual size
|
|
1924
|
+
}
|
|
1925
|
+
if (modelName.includes('qwen') && modelName.includes('7b')) {
|
|
1926
|
+
return 'ollama pull qwen2.5:7b';
|
|
1927
|
+
}
|
|
1928
|
+
if (modelName.includes('phi4-reasoning')) {
|
|
1929
|
+
return 'ollama pull phi4-reasoning:latest';
|
|
1930
|
+
}
|
|
1931
|
+
if (modelName.includes('deepseek-r1')) {
|
|
1932
|
+
return 'ollama pull deepseek-r1:8b';
|
|
1933
|
+
}
|
|
1934
|
+
if (modelName.includes('dolphin3')) {
|
|
1935
|
+
return 'ollama pull dolphin3:latest';
|
|
1936
|
+
}
|
|
1937
|
+
if (modelName === 'phi' || modelName.includes('phi ')) {
|
|
1938
|
+
return 'ollama pull phi:latest';
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// First priority: use ollamaTag if available (from Ollama database)
|
|
1942
|
+
if (model.ollamaTag) {
|
|
1943
|
+
return `ollama pull ${model.ollamaTag}`;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Second priority: use installation.ollama if available
|
|
1947
|
+
if (model.installation && model.installation.ollama) {
|
|
1948
|
+
return model.installation.ollama;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// Third priority: try to generate from model name
|
|
1952
|
+
|
|
1953
|
+
const mapping = {
|
|
1954
|
+
'tinyllama 1.1b': 'ollama pull tinyllama:1.1b',
|
|
1955
|
+
'phi-3 mini 3.8b': 'ollama pull phi3:mini',
|
|
1956
|
+
'llama 3.2 3b': 'ollama pull llama3.2:3b',
|
|
1957
|
+
'llama 3.1 8b': 'ollama pull llama3.1:8b',
|
|
1958
|
+
'mistral 7b': 'ollama pull mistral:7b',
|
|
1959
|
+
'mistral 7b v0.3': 'ollama pull mistral:7b',
|
|
1960
|
+
'qwen 2.5 7b': 'ollama pull qwen2.5:7b',
|
|
1961
|
+
'codellama 7b': 'ollama pull codellama:7b',
|
|
1962
|
+
'codellama': 'ollama pull codellama:7b'
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
for (const [key, command] of Object.entries(mapping)) {
|
|
1966
|
+
if (modelName.includes(key) || key.includes(modelName)) {
|
|
1967
|
+
return command;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Last resort: use ollamaId if available
|
|
1972
|
+
if (model.ollamaId) {
|
|
1973
|
+
return `ollama pull ${model.ollamaId}`;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
return null;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function extractModelName(command) {
|
|
1980
|
+
const match = command.match(/ollama pull (.+)/);
|
|
1981
|
+
return match ? match[1] : 'model';
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
program
|
|
1985
|
+
.command('check')
|
|
1986
|
+
.description('Analyze your system and show compatible LLM models')
|
|
1987
|
+
.option('-d, --detailed', 'Show detailed hardware information')
|
|
1988
|
+
.option('-f, --filter <type>', 'Filter by model type')
|
|
1989
|
+
.option('-u, --use-case <case>', 'Specify use case', 'general')
|
|
1990
|
+
.option('-l, --limit <number>', 'Number of compatible models to show (default: 1)', '1')
|
|
1991
|
+
.option('--max-size <size>', 'Maximum model size to consider (e.g., "30B" or "30GB")')
|
|
1992
|
+
.option('--min-size <size>', 'Minimum model size to consider (e.g., "7B" or "7GB")')
|
|
1993
|
+
.option('--include-cloud', 'Include cloud models in analysis')
|
|
1994
|
+
.option('--ollama-only', 'Only show models available in Ollama')
|
|
1995
|
+
.option('--performance-test', 'Run performance benchmarks')
|
|
1996
|
+
.option('--show-ollama-analysis', 'Show detailed Ollama model analysis')
|
|
1997
|
+
.option('--no-verbose', 'Disable step-by-step progress display')
|
|
1998
|
+
.action(async (options) => {
|
|
1999
|
+
showAsciiArt('check');
|
|
2000
|
+
try {
|
|
2001
|
+
// Use verbose progress unless explicitly disabled
|
|
2002
|
+
const verboseEnabled = options.verbose !== false;
|
|
2003
|
+
const checker = new (getLLMChecker())({ verbose: verboseEnabled });
|
|
2004
|
+
|
|
2005
|
+
// If verbose is disabled, show simple loading message
|
|
2006
|
+
if (!verboseEnabled) {
|
|
2007
|
+
process.stdout.write(chalk.gray('Analyzing your system...'));
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const hardware = await checker.getSystemInfo();
|
|
2011
|
+
|
|
2012
|
+
// Normalize and fix use-case typos
|
|
2013
|
+
const normalizeUseCase = (useCase = '') => {
|
|
2014
|
+
const alias = useCase.toLowerCase().trim();
|
|
2015
|
+
const useCaseMap = {
|
|
2016
|
+
'embed': 'embeddings',
|
|
2017
|
+
'embedding': 'embeddings',
|
|
2018
|
+
'embeddings': 'embeddings',
|
|
2019
|
+
'embedings': 'embeddings', // common typo
|
|
2020
|
+
'talk': 'chat',
|
|
2021
|
+
'chat': 'chat',
|
|
2022
|
+
'talking': 'chat'
|
|
2023
|
+
};
|
|
2024
|
+
return useCaseMap[alias] || alias || 'general';
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
const normalizedUseCase = normalizeUseCase(options.useCase);
|
|
2028
|
+
|
|
2029
|
+
// Parse size filters
|
|
2030
|
+
const parseSizeFilter = (sizeStr) => {
|
|
2031
|
+
if (!sizeStr) return null;
|
|
2032
|
+
const match = sizeStr.toUpperCase().match(/^(\d+\.?\d*)\s*(B|GB)?$/);
|
|
2033
|
+
if (match) {
|
|
2034
|
+
const num = parseFloat(match[1]);
|
|
2035
|
+
const unit = match[2] || 'B';
|
|
2036
|
+
// Return size in billions of parameters (B)
|
|
2037
|
+
return unit === 'GB' ? num / 0.5 : num; // Approximate: 0.5GB per 1B params (Q4)
|
|
2038
|
+
}
|
|
2039
|
+
return null;
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
const maxSize = parseSizeFilter(options.maxSize);
|
|
2043
|
+
const minSize = parseSizeFilter(options.minSize);
|
|
2044
|
+
|
|
2045
|
+
const analysis = await checker.analyze({
|
|
2046
|
+
filter: options.filter,
|
|
2047
|
+
useCase: normalizedUseCase,
|
|
2048
|
+
includeCloud: options.includeCloud,
|
|
2049
|
+
performanceTest: options.performanceTest,
|
|
2050
|
+
limit: parseInt(options.limit) || 10,
|
|
2051
|
+
maxSize: maxSize,
|
|
2052
|
+
minSize: minSize
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
if (!verboseEnabled) {
|
|
2056
|
+
console.log(chalk.green(' done'));
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Simplified output - show only essential information
|
|
2060
|
+
displaySimplifiedSystemInfo(hardware);
|
|
2061
|
+
const recommendedModels = await displayModelRecommendations(analysis, hardware, normalizedUseCase, parseInt(options.limit) || 1);
|
|
2062
|
+
await displayQuickStartCommands(analysis, recommendedModels[0], recommendedModels);
|
|
2063
|
+
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
2066
|
+
if (process.env.DEBUG) {
|
|
2067
|
+
console.error(error.stack);
|
|
2068
|
+
}
|
|
2069
|
+
process.exit(1);
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
program
|
|
2074
|
+
.command('ollama')
|
|
2075
|
+
.description('Manage Ollama integration with hardware compatibility')
|
|
2076
|
+
.option('-l, --list', 'List installed models with compatibility scores')
|
|
2077
|
+
.option('-r, --running', 'Show running models with performance data')
|
|
2078
|
+
.option('-c, --compatible', 'Show only hardware-compatible installed models')
|
|
2079
|
+
.option('--recommendations', 'Show installation recommendations')
|
|
2080
|
+
.action(async (options) => {
|
|
2081
|
+
showAsciiArt('ollama');
|
|
2082
|
+
const spinner = ora('Checking Ollama integration...').start();
|
|
2083
|
+
|
|
2084
|
+
try {
|
|
2085
|
+
const checker = new (getLLMChecker())();
|
|
2086
|
+
const analysis = await checker.analyze();
|
|
2087
|
+
|
|
2088
|
+
if (!analysis.ollamaInfo.available) {
|
|
2089
|
+
spinner.fail(`Ollama not available`);
|
|
2090
|
+
console.log('\nTo install Ollama:');
|
|
2091
|
+
console.log('Visit: https://ollama.ai');
|
|
2092
|
+
if (analysis.ollamaInfo.hint) {
|
|
2093
|
+
console.log(chalk.yellow('Hint: ' + analysis.ollamaInfo.hint));
|
|
2094
|
+
}
|
|
2095
|
+
if (analysis.ollamaInfo.attemptedURL) {
|
|
2096
|
+
console.log(chalk.gray('Attempted URL: ' + analysis.ollamaInfo.attemptedURL));
|
|
2097
|
+
console.log(chalk.gray('Set OLLAMA_HOST environment variable to use a different URL'));
|
|
2098
|
+
}
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
spinner.succeed(`Ollama integration active`);
|
|
2103
|
+
|
|
2104
|
+
if (options.list) {
|
|
2105
|
+
console.log('Ollama models list feature coming soon...');
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
spinner.fail('Error with Ollama integration');
|
|
2110
|
+
console.error(chalk.red('Error:'), error.message);
|
|
2111
|
+
}
|
|
2112
|
+
});
|
|
2113
|
+
|
|
2114
|
+
// New command: installed - Show ranking of installed Ollama models
|
|
2115
|
+
program
|
|
2116
|
+
.command('installed')
|
|
2117
|
+
.description('Show ranking of installed Ollama models by compatibility and use-case')
|
|
2118
|
+
.option('--sort <by>', 'Sort by: score, size, name (default: score)', 'score')
|
|
2119
|
+
.option('--json', 'Output in JSON format')
|
|
2120
|
+
.action(async (options) => {
|
|
2121
|
+
if (!options.json) showAsciiArt('installed');
|
|
2122
|
+
const spinner = ora('Analyzing installed models...').start();
|
|
2123
|
+
|
|
2124
|
+
try {
|
|
2125
|
+
const checker = new (getLLMChecker())({ verbose: false });
|
|
2126
|
+
const OllamaClient = require('../src/ollama/client');
|
|
2127
|
+
const ollamaClient = new OllamaClient();
|
|
2128
|
+
|
|
2129
|
+
// Check Ollama availability
|
|
2130
|
+
const availability = await ollamaClient.checkOllamaAvailability();
|
|
2131
|
+
if (!availability.available) {
|
|
2132
|
+
spinner.fail('Ollama not available');
|
|
2133
|
+
console.log(chalk.red('\n' + availability.error));
|
|
2134
|
+
if (availability.hint) {
|
|
2135
|
+
console.log(chalk.yellow('Hint: ' + availability.hint));
|
|
2136
|
+
}
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// Get installed models
|
|
2141
|
+
const installedModels = await ollamaClient.getLocalModels();
|
|
2142
|
+
if (!installedModels || installedModels.length === 0) {
|
|
2143
|
+
spinner.fail('No models installed');
|
|
2144
|
+
console.log(chalk.yellow('\nNo Ollama models found. Install one with:'));
|
|
2145
|
+
console.log(chalk.cyan(' ollama pull llama3.2:3b'));
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// Get hardware info for scoring
|
|
2150
|
+
const hardware = await checker.getSystemInfo();
|
|
2151
|
+
const analysis = await checker.analyze({ limit: 100 });
|
|
2152
|
+
|
|
2153
|
+
spinner.succeed(`Found ${installedModels.length} installed models`);
|
|
2154
|
+
|
|
2155
|
+
// Score and categorize each installed model
|
|
2156
|
+
const scoredModels = installedModels.map(model => {
|
|
2157
|
+
// Find matching model in analysis
|
|
2158
|
+
const matchingModel = [...(analysis.compatible || []), ...(analysis.marginal || [])].find(m =>
|
|
2159
|
+
m.name && model.name && (
|
|
2160
|
+
m.name.toLowerCase().includes(model.family) ||
|
|
2161
|
+
model.name.toLowerCase().includes(m.name.toLowerCase().split(' ')[0])
|
|
2162
|
+
)
|
|
2163
|
+
);
|
|
2164
|
+
|
|
2165
|
+
// Determine use-case from model name
|
|
2166
|
+
const nameLower = model.name.toLowerCase();
|
|
2167
|
+
let useCase = 'general';
|
|
2168
|
+
if (nameLower.includes('code') || nameLower.includes('coder') || nameLower.includes('deepseek-coder')) {
|
|
2169
|
+
useCase = 'coding';
|
|
2170
|
+
} else if (nameLower.includes('embed') || nameLower.includes('nomic') || nameLower.includes('bge')) {
|
|
2171
|
+
useCase = 'embeddings';
|
|
2172
|
+
} else if (nameLower.includes('llava') || nameLower.includes('vision') || nameLower.includes('bakllava')) {
|
|
2173
|
+
useCase = 'multimodal';
|
|
2174
|
+
} else if (nameLower.includes('r1') || nameLower.includes('qwq') || nameLower.includes('reasoning')) {
|
|
2175
|
+
useCase = 'reasoning';
|
|
2176
|
+
} else if (nameLower.includes('wizard') || nameLower.includes('creative')) {
|
|
2177
|
+
useCase = 'creative';
|
|
2178
|
+
} else if (nameLower.includes('chat') || nameLower.includes('instruct')) {
|
|
2179
|
+
useCase = 'chat';
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Calculate compatibility score
|
|
2183
|
+
const fileSizeGB = model.fileSizeGB || 0;
|
|
2184
|
+
const availableRAM = hardware.memory.total * 0.8;
|
|
2185
|
+
let score = 50;
|
|
2186
|
+
|
|
2187
|
+
// RAM fit score
|
|
2188
|
+
if (fileSizeGB <= availableRAM * 0.3) score += 30;
|
|
2189
|
+
else if (fileSizeGB <= availableRAM * 0.5) score += 20;
|
|
2190
|
+
else if (fileSizeGB <= availableRAM * 0.7) score += 10;
|
|
2191
|
+
else score -= 10;
|
|
2192
|
+
|
|
2193
|
+
// Size efficiency for hardware tier
|
|
2194
|
+
const sizeMatch = (model.size || '').match(/(\d+)/);
|
|
2195
|
+
const paramSize = sizeMatch ? parseInt(sizeMatch[1]) : 7;
|
|
2196
|
+
if (hardware.memory.total >= 32 && paramSize >= 13) score += 10;
|
|
2197
|
+
else if (hardware.memory.total >= 16 && paramSize >= 7) score += 10;
|
|
2198
|
+
else if (hardware.memory.total >= 8 && paramSize <= 7) score += 10;
|
|
2199
|
+
|
|
2200
|
+
// Use matched model score if available
|
|
2201
|
+
if (matchingModel && matchingModel.score) {
|
|
2202
|
+
score = Math.round((score + matchingModel.score) / 2);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
return {
|
|
2206
|
+
name: model.name,
|
|
2207
|
+
displayName: model.displayName,
|
|
2208
|
+
size: model.size,
|
|
2209
|
+
fileSizeGB: model.fileSizeGB,
|
|
2210
|
+
quantization: model.quantization,
|
|
2211
|
+
useCase: useCase,
|
|
2212
|
+
score: Math.min(100, Math.max(0, score)),
|
|
2213
|
+
command: `ollama run ${model.name}`
|
|
2214
|
+
};
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
// Sort models
|
|
2218
|
+
scoredModels.sort((a, b) => {
|
|
2219
|
+
switch (options.sort) {
|
|
2220
|
+
case 'size':
|
|
2221
|
+
return b.fileSizeGB - a.fileSizeGB;
|
|
2222
|
+
case 'name':
|
|
2223
|
+
return a.name.localeCompare(b.name);
|
|
2224
|
+
case 'score':
|
|
2225
|
+
default:
|
|
2226
|
+
return b.score - a.score;
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
// Output
|
|
2231
|
+
if (options.json) {
|
|
2232
|
+
console.log(JSON.stringify(scoredModels, null, 2));
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
console.log('\n' + chalk.bgGreen.white.bold(' INSTALLED MODELS RANKING '));
|
|
2237
|
+
console.log(chalk.green('╭' + '─'.repeat(75)));
|
|
2238
|
+
console.log(chalk.green('│') + ` Sorted by: ${chalk.cyan(options.sort)} | Hardware: ${chalk.yellow(hardware.memory.total + 'GB RAM')}`);
|
|
2239
|
+
console.log(chalk.green('├' + '─'.repeat(75)));
|
|
2240
|
+
|
|
2241
|
+
const headers = [
|
|
2242
|
+
chalk.bold(' # '),
|
|
2243
|
+
chalk.bold(' Model '),
|
|
2244
|
+
chalk.bold(' Size '),
|
|
2245
|
+
chalk.bold(' Score '),
|
|
2246
|
+
chalk.bold(' Use Case '),
|
|
2247
|
+
chalk.bold(' Command ')
|
|
2248
|
+
];
|
|
2249
|
+
const data = [headers];
|
|
2250
|
+
|
|
2251
|
+
scoredModels.forEach((model, index) => {
|
|
2252
|
+
const rank = index + 1;
|
|
2253
|
+
const rankIcon = rank <= 3 ? ['🥇', '🥈', '🥉'][rank - 1] : `${rank}.`;
|
|
2254
|
+
const scoreColor = model.score >= 75 ? chalk.green : model.score >= 50 ? chalk.yellow : chalk.red;
|
|
2255
|
+
|
|
2256
|
+
data.push([
|
|
2257
|
+
rankIcon,
|
|
2258
|
+
model.name.length > 25 ? model.name.substring(0, 22) + '...' : model.name,
|
|
2259
|
+
`${model.fileSizeGB}GB`,
|
|
2260
|
+
scoreColor(`${model.score}/100`),
|
|
2261
|
+
model.useCase,
|
|
2262
|
+
chalk.cyan(`ollama run ${model.name.split(':')[0]}`)
|
|
2263
|
+
]);
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
console.log(table(data));
|
|
2267
|
+
|
|
2268
|
+
// Show suggestions for low-ranking models
|
|
2269
|
+
const lowRankingModels = scoredModels.filter(m => m.score < 50);
|
|
2270
|
+
if (lowRankingModels.length > 0) {
|
|
2271
|
+
console.log(chalk.yellow('\nConsider removing these low-ranking models to free up space:'));
|
|
2272
|
+
lowRankingModels.forEach(m => {
|
|
2273
|
+
console.log(chalk.gray(` ollama rm ${m.name} # Score: ${m.score}/100, Size: ${m.fileSizeGB}GB`));
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
console.log(chalk.green('╰' + '─'.repeat(75)));
|
|
2278
|
+
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
spinner.fail('Error analyzing installed models');
|
|
2281
|
+
console.error(chalk.red('Error:'), error.message);
|
|
2282
|
+
if (process.env.DEBUG) {
|
|
2283
|
+
console.error(error.stack);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
program
|
|
2289
|
+
.command('recommend')
|
|
2290
|
+
.description('Get intelligent model recommendations for your hardware')
|
|
2291
|
+
.option('-c, --category <category>', 'Get recommendations for specific category (coding, talking, reading, etc.)')
|
|
2292
|
+
.option('--no-verbose', 'Disable step-by-step progress display')
|
|
2293
|
+
.action(async (options) => {
|
|
2294
|
+
showAsciiArt('recommend');
|
|
2295
|
+
try {
|
|
2296
|
+
const verboseEnabled = options.verbose !== false;
|
|
2297
|
+
const checker = new (getLLMChecker())({ verbose: verboseEnabled });
|
|
2298
|
+
|
|
2299
|
+
if (!verboseEnabled) {
|
|
2300
|
+
process.stdout.write(chalk.gray('Generating recommendations...'));
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const hardware = await checker.getSystemInfo();
|
|
2304
|
+
const intelligentRecommendations = await checker.generateIntelligentRecommendations(hardware);
|
|
2305
|
+
|
|
2306
|
+
if (!intelligentRecommendations) {
|
|
2307
|
+
console.error(chalk.red('\nFailed to generate recommendations'));
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (!verboseEnabled) {
|
|
2312
|
+
console.log(chalk.green(' done'));
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// Mostrar información del sistema
|
|
2316
|
+
displaySystemInfo(hardware, { summary: { hardwareTier: intelligentRecommendations.summary.hardware_tier } });
|
|
2317
|
+
|
|
2318
|
+
// Mostrar recomendaciones
|
|
2319
|
+
displayIntelligentRecommendations(intelligentRecommendations);
|
|
2320
|
+
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
console.error(chalk.red('\nError:'), error.message);
|
|
2323
|
+
if (process.env.DEBUG) {
|
|
2324
|
+
console.error(error.stack);
|
|
2325
|
+
}
|
|
2326
|
+
process.exit(1);
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
program
|
|
2331
|
+
.command('list-models')
|
|
2332
|
+
.description('List all models from Ollama database')
|
|
2333
|
+
.option('-c, --category <category>', 'Filter by category (coding, talking, reading, reasoning, multimodal, creative, general)')
|
|
2334
|
+
.option('-s, --size <size>', 'Filter by size (small, medium, large, e.g., "7b", "13b")')
|
|
2335
|
+
.option('-p, --popular', 'Show only popular models (>100k pulls)')
|
|
2336
|
+
.option('-r, --recent', 'Show only recent models (updated in last 30 days)')
|
|
2337
|
+
.option('--limit <number>', 'Limit number of results (default: 50)', '50')
|
|
2338
|
+
.option('--full', 'Show full details including variants and tags')
|
|
2339
|
+
.option('--json', 'Output in JSON format')
|
|
2340
|
+
.action(async (options) => {
|
|
2341
|
+
if (!options.json) showAsciiArt('list-models');
|
|
2342
|
+
const spinner = ora('📋 Loading models database...').start();
|
|
2343
|
+
|
|
2344
|
+
try {
|
|
2345
|
+
const checker = new (getLLMChecker())();
|
|
2346
|
+
const data = await checker.ollamaScraper.scrapeAllModels(false);
|
|
2347
|
+
|
|
2348
|
+
if (!data || !data.models) {
|
|
2349
|
+
spinner.fail('No models found in database');
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
let models = data.models;
|
|
2354
|
+
let originalCount = models.length;
|
|
2355
|
+
|
|
2356
|
+
// Aplicar filtros
|
|
2357
|
+
if (options.category) {
|
|
2358
|
+
const categoryFilter = options.category.toLowerCase();
|
|
2359
|
+
models = models.filter(model => {
|
|
2360
|
+
// Buscar en categoría principal
|
|
2361
|
+
if (model.category === categoryFilter) return true;
|
|
2362
|
+
|
|
2363
|
+
// Buscar en use_cases
|
|
2364
|
+
if (model.use_cases && model.use_cases.includes(categoryFilter)) return true;
|
|
2365
|
+
|
|
2366
|
+
// Buscar por palabras clave en el nombre/identificador
|
|
2367
|
+
const modelText = `${model.model_name} ${model.model_identifier}`.toLowerCase();
|
|
2368
|
+
|
|
2369
|
+
switch(categoryFilter) {
|
|
2370
|
+
case 'coding':
|
|
2371
|
+
case 'code':
|
|
2372
|
+
return modelText.includes('code') || modelText.includes('coder') ||
|
|
2373
|
+
modelText.includes('programming') || modelText.includes('deepseek') ||
|
|
2374
|
+
modelText.includes('starcoder');
|
|
2375
|
+
case 'talking':
|
|
2376
|
+
case 'chat':
|
|
2377
|
+
return modelText.includes('chat') || modelText.includes('llama') ||
|
|
2378
|
+
modelText.includes('mistral') || modelText.includes('gemma') ||
|
|
2379
|
+
modelText.includes('phi');
|
|
2380
|
+
case 'reasoning':
|
|
2381
|
+
return modelText.includes('reasoning') || modelText.includes('deepseek-r1') ||
|
|
2382
|
+
modelText.includes('qwq') || modelText.includes('r1');
|
|
2383
|
+
case 'multimodal':
|
|
2384
|
+
case 'vision':
|
|
2385
|
+
return modelText.includes('vision') || modelText.includes('llava') ||
|
|
2386
|
+
modelText.includes('minicpm-v');
|
|
2387
|
+
case 'creative':
|
|
2388
|
+
case 'writing':
|
|
2389
|
+
return modelText.includes('wizard') || modelText.includes('creative') ||
|
|
2390
|
+
modelText.includes('uncensored');
|
|
2391
|
+
case 'embeddings':
|
|
2392
|
+
case 'embed':
|
|
2393
|
+
return modelText.includes('embed') || modelText.includes('bge') ||
|
|
2394
|
+
modelText.includes('nomic');
|
|
2395
|
+
default:
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
if (options.size) {
|
|
2402
|
+
const sizeFilter = options.size.toLowerCase();
|
|
2403
|
+
models = models.filter(model =>
|
|
2404
|
+
model.model_identifier.toLowerCase().includes(sizeFilter) ||
|
|
2405
|
+
(model.model_sizes && model.model_sizes.some(size => size.includes(sizeFilter)))
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (options.popular) {
|
|
2410
|
+
models = models.filter(model => (model.pulls || 0) > 100000);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (options.recent) {
|
|
2414
|
+
models = models.filter(model =>
|
|
2415
|
+
model.last_updated && model.last_updated.includes('day')
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// Si hay filtro de categoría, ordenar por compatibilidad con hardware
|
|
2420
|
+
if (options.category) {
|
|
2421
|
+
try {
|
|
2422
|
+
const LLMChecker = require('../src/index.js');
|
|
2423
|
+
const hardwareDetector = new (require('../src/hardware/detector.js'))();
|
|
2424
|
+
const hardware = await hardwareDetector.getSystemInfo();
|
|
2425
|
+
|
|
2426
|
+
// Calcular puntuación de compatibilidad para cada modelo
|
|
2427
|
+
models = models.map(model => {
|
|
2428
|
+
const compatibilityScore = calculateModelCompatibilityScore(model, hardware);
|
|
2429
|
+
return { ...model, compatibilityScore };
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
// Ordenar por compatibilidad primero, luego por popularidad
|
|
2433
|
+
models.sort((a, b) => {
|
|
2434
|
+
if (b.compatibilityScore !== a.compatibilityScore) {
|
|
2435
|
+
return b.compatibilityScore - a.compatibilityScore;
|
|
2436
|
+
}
|
|
2437
|
+
return (b.pulls || 0) - (a.pulls || 0);
|
|
2438
|
+
});
|
|
2439
|
+
|
|
2440
|
+
spinner.text = `Sorted by hardware compatibility (${getHardwareTierForDisplay(hardware)})`;
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
console.warn('Could not sort by hardware compatibility:', error.message);
|
|
2443
|
+
// Fallback a ordenar por popularidad
|
|
2444
|
+
models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
|
|
2445
|
+
}
|
|
2446
|
+
} else {
|
|
2447
|
+
// Sin filtro de categoría, ordenar solo por popularidad
|
|
2448
|
+
models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// Limitar resultados
|
|
2452
|
+
const limit = parseInt(options.limit) || 50;
|
|
2453
|
+
const displayModels = models.slice(0, limit);
|
|
2454
|
+
|
|
2455
|
+
spinner.succeed(`✅ Found ${models.length} models (showing ${displayModels.length})`);
|
|
2456
|
+
|
|
2457
|
+
if (options.json) {
|
|
2458
|
+
console.log(JSON.stringify(displayModels, null, 2));
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// Mostrar estadísticas
|
|
2463
|
+
displayModelsStats(originalCount, models.length, options);
|
|
2464
|
+
|
|
2465
|
+
// Mostrar modelos
|
|
2466
|
+
if (options.full) {
|
|
2467
|
+
displayFullModelsList(displayModels);
|
|
2468
|
+
} else {
|
|
2469
|
+
await displayCompactModelsList(displayModels, options.category);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// Mostrar comandos de ejemplo
|
|
2473
|
+
if (displayModels.length > 0) {
|
|
2474
|
+
displaySampleCommands(displayModels.slice(0, 3));
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
spinner.fail('Failed to load models');
|
|
2479
|
+
console.error(chalk.red('Error:'), error.message);
|
|
2480
|
+
if (process.env.DEBUG) {
|
|
2481
|
+
console.error(error.stack);
|
|
2482
|
+
}
|
|
2483
|
+
process.exit(1);
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
function getStatusColor(status) {
|
|
2489
|
+
const colors = {
|
|
2490
|
+
'TRAINED': chalk.green,
|
|
2491
|
+
'NOT TRAINED': chalk.yellow,
|
|
2492
|
+
'CORRUPTED': chalk.red
|
|
2493
|
+
};
|
|
2494
|
+
return colors[status] || chalk.gray;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
function getConfidenceColor(confidence) {
|
|
2498
|
+
if (confidence >= 0.8) return chalk.green.bold;
|
|
2499
|
+
if (confidence >= 0.6) return chalk.yellow.bold;
|
|
2500
|
+
if (confidence >= 0.4) return chalk.red.bold; // orange doesn't exist, use red
|
|
2501
|
+
return chalk.red.bold;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function getScoreColor(score) {
|
|
2505
|
+
if (score >= 85) return chalk.green.bold;
|
|
2506
|
+
if (score >= 70) return chalk.cyan.bold;
|
|
2507
|
+
if (score >= 55) return chalk.yellow.bold;
|
|
2508
|
+
if (score >= 40) return chalk.red.bold;
|
|
2509
|
+
return chalk.gray;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function getTierColor(tier) {
|
|
2513
|
+
const colors = {
|
|
2514
|
+
'extreme': chalk.magenta.bold,
|
|
2515
|
+
'very_high': chalk.green.bold,
|
|
2516
|
+
'high': chalk.cyan.bold,
|
|
2517
|
+
'medium': chalk.yellow,
|
|
2518
|
+
'low': chalk.red,
|
|
2519
|
+
'ultra_low': chalk.gray
|
|
2520
|
+
};
|
|
2521
|
+
return colors[tier] || chalk.white;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
program
|
|
2525
|
+
.command('ai-check')
|
|
2526
|
+
.description('AI-powered model evaluation with meta-analysis')
|
|
2527
|
+
.option('-c, --category <category>', 'Category: coding, reasoning, multimodal, general', 'general')
|
|
2528
|
+
.option('-t, --top <number>', 'Number of top models to show', '12')
|
|
2529
|
+
.option('--ctx <number>', 'Target context length', '8192')
|
|
2530
|
+
.option('-e, --evaluator <model>', 'Evaluator model (auto for best available)', 'auto')
|
|
2531
|
+
.option('-w, --weight <number>', 'AI weight (0.0-1.0, default 0.3)', '0.3')
|
|
2532
|
+
.action(async (options) => {
|
|
2533
|
+
showAsciiArt('ai-check');
|
|
2534
|
+
// Check if Ollama is installed first
|
|
2535
|
+
await checkOllamaAndExit();
|
|
2536
|
+
|
|
2537
|
+
const AICheckSelector = require('../src/models/ai-check-selector');
|
|
2538
|
+
|
|
2539
|
+
try {
|
|
2540
|
+
const spinner = ora('AI-Check Mode: Meta-evaluation in progress...').start();
|
|
2541
|
+
|
|
2542
|
+
const aiCheckSelector = new AICheckSelector();
|
|
2543
|
+
|
|
2544
|
+
const checkOptions = {
|
|
2545
|
+
category: options.category,
|
|
2546
|
+
top: parseInt(options.top),
|
|
2547
|
+
ctx: options.ctx ? parseInt(options.ctx) : undefined,
|
|
2548
|
+
evaluator: options.evaluator,
|
|
2549
|
+
weight: parseFloat(options.weight)
|
|
2550
|
+
};
|
|
2551
|
+
|
|
2552
|
+
spinner.stop();
|
|
2553
|
+
|
|
2554
|
+
const result = await aiCheckSelector.aiCheck(checkOptions);
|
|
2555
|
+
|
|
2556
|
+
// Format and display results
|
|
2557
|
+
aiCheckSelector.formatResults(result);
|
|
2558
|
+
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
console.error(chalk.red('❌ AI-Check failed:'), error.message);
|
|
2561
|
+
if (process.argv.includes('--verbose')) {
|
|
2562
|
+
console.error(error.stack);
|
|
2563
|
+
}
|
|
2564
|
+
process.exit(1);
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
program
|
|
2569
|
+
.command('ai-run')
|
|
2570
|
+
.description('AI-powered model selection and execution')
|
|
2571
|
+
.option('-m, --models <models...>', 'Specific models to choose from')
|
|
2572
|
+
.option('--prompt <prompt>', 'Prompt to run with selected model')
|
|
2573
|
+
.action(async (options) => {
|
|
2574
|
+
showAsciiArt('ai-run');
|
|
2575
|
+
// Check if Ollama is installed first
|
|
2576
|
+
await checkOllamaAndExit();
|
|
2577
|
+
|
|
2578
|
+
const AIModelSelector = require('../src/ai/model-selector');
|
|
2579
|
+
|
|
2580
|
+
try {
|
|
2581
|
+
const spinner = ora('Selecting best model and launching...').start();
|
|
2582
|
+
|
|
2583
|
+
const aiSelector = new AIModelSelector();
|
|
2584
|
+
const checker = new (getLLMChecker())();
|
|
2585
|
+
const systemInfo = await checker.getSystemInfo();
|
|
2586
|
+
|
|
2587
|
+
// Get available models or use provided ones
|
|
2588
|
+
let candidateModels = options.models;
|
|
2589
|
+
|
|
2590
|
+
if (!candidateModels) {
|
|
2591
|
+
spinner.text = '📋 Getting available Ollama models...';
|
|
2592
|
+
const OllamaClient = require('../src/ollama/client');
|
|
2593
|
+
const client = new OllamaClient();
|
|
2594
|
+
|
|
2595
|
+
try {
|
|
2596
|
+
const models = await client.getLocalModels();
|
|
2597
|
+
candidateModels = models.map(m => m.name || m.model);
|
|
2598
|
+
|
|
2599
|
+
if (candidateModels.length === 0) {
|
|
2600
|
+
spinner.fail('❌ No Ollama models found');
|
|
2601
|
+
console.log('\nInstall some models first:');
|
|
2602
|
+
console.log(' ollama pull llama2:7b');
|
|
2603
|
+
console.log(' ollama pull mistral:7b');
|
|
2604
|
+
console.log(' ollama pull phi3:mini');
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
} catch (error) {
|
|
2608
|
+
spinner.fail('❌ Failed to get Ollama models');
|
|
2609
|
+
console.error(chalk.red('Error:'), error.message);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// AI selection
|
|
2615
|
+
const systemSpecs = {
|
|
2616
|
+
cpu_cores: systemInfo.cpu?.cores || 4,
|
|
2617
|
+
cpu_freq_max: systemInfo.cpu?.speed || 3.0,
|
|
2618
|
+
total_ram_gb: systemInfo.memory?.total || 8,
|
|
2619
|
+
gpu_vram_gb: systemInfo.gpu?.vram || 0,
|
|
2620
|
+
gpu_model_normalized: systemInfo.gpu?.model ||
|
|
2621
|
+
(systemInfo.cpu?.manufacturer === 'Apple' ? 'apple_silicon' : 'cpu_only')
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
const result = await aiSelector.selectBestModel(candidateModels, systemSpecs);
|
|
2625
|
+
|
|
2626
|
+
spinner.succeed(`Selected ${chalk.green.bold(result.bestModel)} (${result.method}, ${Math.round(result.confidence * 100)}% confidence)`);
|
|
2627
|
+
|
|
2628
|
+
// Execute the selected model
|
|
2629
|
+
console.log(chalk.magenta.bold(`\nLaunching ${result.bestModel}...`));
|
|
2630
|
+
console.log(chalk.gray(`Tip: Type ${chalk.cyan('/bye')} to exit the chat when finished\n`));
|
|
2631
|
+
|
|
2632
|
+
const args = ['run', result.bestModel];
|
|
2633
|
+
if (options.prompt) {
|
|
2634
|
+
args.push(options.prompt);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
const ollamaProcess = spawn('ollama', args, {
|
|
2638
|
+
stdio: 'inherit'
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
ollamaProcess.on('error', (error) => {
|
|
2642
|
+
console.error(chalk.red('Failed to launch Ollama:'), error.message);
|
|
2643
|
+
});
|
|
2644
|
+
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
console.error(chalk.red('❌ AI-powered execution failed:'), error.message);
|
|
2647
|
+
process.exit(1);
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
// Comando especial para demostrar el nuevo estilo de verbosity
|
|
2652
|
+
program
|
|
2653
|
+
.command('demo')
|
|
2654
|
+
.description('Demo of the enhanced verbose progress with progress bars')
|
|
2655
|
+
.action(async () => {
|
|
2656
|
+
showAsciiArt('demo');
|
|
2657
|
+
console.log(chalk.cyan.bold('\nLLM Checker - Enhanced Progress Demo'));
|
|
2658
|
+
console.log(chalk.gray('This demonstrates the new step-by-step progress display with visual indicators'));
|
|
2659
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
2660
|
+
|
|
2661
|
+
// Simular el proceso de análisis con verbosity
|
|
2662
|
+
const VerboseProgress = require('../src/utils/verbose-progress');
|
|
2663
|
+
const progress = VerboseProgress.create(true);
|
|
2664
|
+
|
|
2665
|
+
progress.startOperation('LLM Model Analysis & Compatibility Demo', 5);
|
|
2666
|
+
|
|
2667
|
+
// Simular paso 1
|
|
2668
|
+
progress.step('System Detection', 'Scanning hardware specifications...');
|
|
2669
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
2670
|
+
progress.substep('CPU detected: Apple M4 Pro (12 cores)');
|
|
2671
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2672
|
+
progress.substep('Memory detected: 24GB unified memory', true);
|
|
2673
|
+
progress.stepComplete('Apple M4 Pro, 24GB RAM, Apple Silicon GPU');
|
|
2674
|
+
|
|
2675
|
+
// Simular paso 2
|
|
2676
|
+
progress.step('Database Sync', 'Updating model database...');
|
|
2677
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
2678
|
+
progress.found('3,247 models in database');
|
|
2679
|
+
progress.stepComplete('Database synchronized');
|
|
2680
|
+
|
|
2681
|
+
// Simular paso 3
|
|
2682
|
+
progress.step('Compatibility Analysis', 'Running mathematical heuristics...');
|
|
2683
|
+
await new Promise(resolve => setTimeout(resolve, 1200));
|
|
2684
|
+
progress.substep('Analyzing hardware requirements...');
|
|
2685
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
2686
|
+
progress.substep('Calculating performance scores...', true);
|
|
2687
|
+
progress.found('127 compatible models found');
|
|
2688
|
+
progress.stepComplete('Compatibility analysis complete');
|
|
2689
|
+
|
|
2690
|
+
// Simular paso 4
|
|
2691
|
+
progress.step('AI Evaluation', 'Running intelligent model selection...');
|
|
2692
|
+
await new Promise(resolve => setTimeout(resolve, 900));
|
|
2693
|
+
progress.substep('Mathematical heuristics applied');
|
|
2694
|
+
progress.found('Top 15 models selected by AI');
|
|
2695
|
+
progress.stepComplete('AI evaluation complete');
|
|
2696
|
+
|
|
2697
|
+
// Simular paso 5
|
|
2698
|
+
progress.step('Smart Recommendations', 'Generating personalized suggestions...');
|
|
2699
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
2700
|
+
progress.substep('Analyzing use case: general');
|
|
2701
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
2702
|
+
progress.substep('Generating Ollama commands...', true);
|
|
2703
|
+
progress.stepComplete('23 recommendations generated');
|
|
2704
|
+
|
|
2705
|
+
// Completar
|
|
2706
|
+
progress.complete('Analysis complete! Found optimal models for your hardware');
|
|
2707
|
+
|
|
2708
|
+
console.log(chalk.green.bold('Demo completed successfully!'));
|
|
2709
|
+
console.log(chalk.gray('\\nNow try running: ') + chalk.cyan.bold('llm-checker check'));
|
|
2710
|
+
console.log(chalk.gray('For silent mode: ') + chalk.cyan.bold('llm-checker check --no-verbose'));
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2713
|
+
// ============================================================
|
|
2714
|
+
// NEW ENHANCED COMMANDS (v3.0 - Intelligent Model Selection)
|
|
2715
|
+
// ============================================================
|
|
2716
|
+
|
|
2717
|
+
program
|
|
2718
|
+
.command('sync')
|
|
2719
|
+
.description('Sync the model database from Ollama registry (scrapes all models)')
|
|
2720
|
+
.option('-f, --force', 'Force full sync even if recent data exists')
|
|
2721
|
+
.option('--incremental', 'Only sync new and updated models')
|
|
2722
|
+
.option('-q, --quiet', 'Suppress progress output')
|
|
2723
|
+
.action(async (options) => {
|
|
2724
|
+
if (!options.quiet) showAsciiArt('sync');
|
|
2725
|
+
const SyncManager = require('../src/data/sync-manager');
|
|
2726
|
+
|
|
2727
|
+
const spinner = options.quiet ? null : ora('Initializing sync...').start();
|
|
2728
|
+
|
|
2729
|
+
try {
|
|
2730
|
+
const syncManager = new SyncManager({
|
|
2731
|
+
onProgress: (info) => {
|
|
2732
|
+
if (!options.quiet && spinner) {
|
|
2733
|
+
if (info.phase === 'complete') {
|
|
2734
|
+
spinner.succeed(info.message);
|
|
2735
|
+
} else {
|
|
2736
|
+
spinner.text = info.message;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
},
|
|
2740
|
+
onError: (err) => {
|
|
2741
|
+
if (!options.quiet) console.error(chalk.yellow('Warning:'), err);
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
|
|
2745
|
+
let result;
|
|
2746
|
+
if (options.incremental) {
|
|
2747
|
+
result = await syncManager.incrementalSync();
|
|
2748
|
+
} else {
|
|
2749
|
+
result = await syncManager.sync({ force: options.force });
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
if (!options.quiet) {
|
|
2753
|
+
console.log(chalk.green('\n[OK] Sync complete!'));
|
|
2754
|
+
console.log(chalk.gray(` Models: ${result.stats?.models || result.models || 0}`));
|
|
2755
|
+
console.log(chalk.gray(` Variants: ${result.stats?.variants || result.variants || 0}`));
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
syncManager.close();
|
|
2759
|
+
|
|
2760
|
+
} catch (error) {
|
|
2761
|
+
if (spinner) spinner.fail('Sync failed');
|
|
2762
|
+
console.error(chalk.red('Error:'), error.message);
|
|
2763
|
+
if (process.env.DEBUG) console.error(error.stack);
|
|
2764
|
+
process.exit(1);
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
program
|
|
2769
|
+
.command('search <query>')
|
|
2770
|
+
.description('Search models in the database with intelligent scoring')
|
|
2771
|
+
.option('-u, --use-case <case>', 'Optimize for use case (general, coding, chat, reasoning, creative)', 'general')
|
|
2772
|
+
.option('-l, --limit <n>', 'Maximum number of results', '10')
|
|
2773
|
+
.option('--max-size <gb>', 'Maximum model size in GB')
|
|
2774
|
+
.option('--min-size <gb>', 'Minimum model size in GB')
|
|
2775
|
+
.option('--quant <type>', 'Filter by quantization (Q4_K_M, Q5_K_M, Q8_0, etc.)')
|
|
2776
|
+
.option('--family <name>', 'Filter by model family (llama, qwen, mistral, etc.)')
|
|
2777
|
+
.option('-j, --json', 'Output as JSON')
|
|
2778
|
+
.action(async (query, options) => {
|
|
2779
|
+
if (!options.json) showAsciiArt('search');
|
|
2780
|
+
const SyncManager = require('../src/data/sync-manager');
|
|
2781
|
+
const IntelligentSelector = require('../src/models/intelligent-selector');
|
|
2782
|
+
const UnifiedDetector = require('../src/hardware/unified-detector');
|
|
2783
|
+
|
|
2784
|
+
const spinner = options.json ? null : ora('Searching models...').start();
|
|
2785
|
+
|
|
2786
|
+
try {
|
|
2787
|
+
// Detect hardware first to determine max size
|
|
2788
|
+
const detector = new UnifiedDetector();
|
|
2789
|
+
const hardware = await detector.detect();
|
|
2790
|
+
const hardwareMaxSize = detector.getMaxModelSize();
|
|
2791
|
+
|
|
2792
|
+
const syncManager = new SyncManager({ onProgress: () => {} });
|
|
2793
|
+
await syncManager.init();
|
|
2794
|
+
|
|
2795
|
+
// Check if we need to sync first
|
|
2796
|
+
const syncStatus = await syncManager.needsSync();
|
|
2797
|
+
if (syncStatus.needed && !options.json) {
|
|
2798
|
+
spinner.text = 'Database needs sync, running quick check...';
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// Use user-provided maxSize or hardware-detected max
|
|
2802
|
+
const effectiveMaxSize = options.maxSize
|
|
2803
|
+
? parseFloat(options.maxSize)
|
|
2804
|
+
: hardwareMaxSize + 2; // Add some headroom
|
|
2805
|
+
|
|
2806
|
+
// Search for variants in database
|
|
2807
|
+
const searchResults = await syncManager.searchVariants(query, {
|
|
2808
|
+
maxSize: effectiveMaxSize,
|
|
2809
|
+
minSize: options.minSize ? parseFloat(options.minSize) : null,
|
|
2810
|
+
quant: options.quant,
|
|
2811
|
+
family: options.family,
|
|
2812
|
+
limit: parseInt(options.limit) * 5 // Get more for scoring
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
if (searchResults.length === 0) {
|
|
2816
|
+
if (spinner) spinner.info('No models found matching your query');
|
|
2817
|
+
syncManager.close();
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Score with intelligent selector (reuse detector from above)
|
|
2822
|
+
const selector = new IntelligentSelector({ detector });
|
|
2823
|
+
|
|
2824
|
+
const recommendations = await selector.recommend(searchResults, {
|
|
2825
|
+
useCase: options.useCase,
|
|
2826
|
+
limit: parseInt(options.limit)
|
|
2827
|
+
});
|
|
2828
|
+
|
|
2829
|
+
syncManager.close();
|
|
2830
|
+
|
|
2831
|
+
if (options.json) {
|
|
2832
|
+
console.log(JSON.stringify(recommendations, null, 2));
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
if (spinner) spinner.succeed(`Found ${recommendations.meta.afterFiltering} matching models`);
|
|
2837
|
+
|
|
2838
|
+
// Display results
|
|
2839
|
+
console.log(chalk.blue.bold('\nSearch Results for: ') + chalk.white(query));
|
|
2840
|
+
console.log(chalk.gray(`Hardware: ${recommendations.hardware.description}`));
|
|
2841
|
+
console.log(chalk.gray(`Max model size: ${recommendations.hardware.maxSize}GB`));
|
|
2842
|
+
console.log('');
|
|
2843
|
+
|
|
2844
|
+
for (const item of recommendations.all) {
|
|
2845
|
+
const v = item.variant;
|
|
2846
|
+
const s = item.score;
|
|
2847
|
+
|
|
2848
|
+
// Format model name (tag already contains model:variant format)
|
|
2849
|
+
const fullTag = v.tag || 'latest';
|
|
2850
|
+
const displayName = fullTag.includes(':') ? fullTag : `${v.model_id || v.modelId}:${fullTag}`;
|
|
2851
|
+
|
|
2852
|
+
const scoreColor = s.final >= 80 ? chalk.green : s.final >= 60 ? chalk.yellow : chalk.red;
|
|
2853
|
+
|
|
2854
|
+
console.log(
|
|
2855
|
+
scoreColor(`[${s.final}]`) + ' ' +
|
|
2856
|
+
chalk.white.bold(displayName)
|
|
2857
|
+
);
|
|
2858
|
+
console.log(
|
|
2859
|
+
chalk.gray(` ${v.params_b || v.paramsB || '?'}B params, `) +
|
|
2860
|
+
chalk.gray(`${v.size_gb || v.sizeGB || '?'}GB, `) +
|
|
2861
|
+
chalk.gray(`${v.quant || 'Q4_K_M'}, `) +
|
|
2862
|
+
chalk.cyan(`~${s.meta.estimatedTPS} tok/s`)
|
|
2863
|
+
);
|
|
2864
|
+
console.log(
|
|
2865
|
+
chalk.gray(` Q:${s.components.quality} S:${s.components.speed} F:${s.components.fit} C:${s.components.context}`)
|
|
2866
|
+
);
|
|
2867
|
+
console.log(chalk.cyan(` ollama pull ${displayName}`));
|
|
2868
|
+
console.log('');
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// Show insights
|
|
2872
|
+
if (recommendations.insights.length > 0) {
|
|
2873
|
+
console.log(chalk.blue.bold('Insights:'));
|
|
2874
|
+
for (const insight of recommendations.insights) {
|
|
2875
|
+
const icon = insight.type === 'success' ? '[OK]' : insight.type === 'warning' ? '[!]' : '[i]';
|
|
2876
|
+
const color = insight.type === 'success' ? chalk.green : insight.type === 'warning' ? chalk.yellow : chalk.cyan;
|
|
2877
|
+
console.log(color(` ${icon} ${insight.message}`));
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
if (spinner) spinner.fail('Search failed');
|
|
2883
|
+
console.error(chalk.red('Error:'), error.message);
|
|
2884
|
+
if (process.env.DEBUG) console.error(error.stack);
|
|
2885
|
+
process.exit(1);
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
|
|
2889
|
+
program
|
|
2890
|
+
.command('smart-recommend')
|
|
2891
|
+
.description('Get intelligent model recommendations using the new scoring engine')
|
|
2892
|
+
.option('-u, --use-case <case>', 'Optimize for use case', 'general')
|
|
2893
|
+
.option('-l, --limit <n>', 'Maximum number of recommendations', '5')
|
|
2894
|
+
.option('--target-tps <n>', 'Target tokens per second', '20')
|
|
2895
|
+
.option('--target-context <n>', 'Target context length', '8192')
|
|
2896
|
+
.option('--include-vision', 'Include vision/multimodal models')
|
|
2897
|
+
.option('--include-embeddings', 'Include embedding models')
|
|
2898
|
+
.option('-j, --json', 'Output as JSON')
|
|
2899
|
+
.action(async (options) => {
|
|
2900
|
+
if (!options.json) showAsciiArt('smart-recommend');
|
|
2901
|
+
const SyncManager = require('../src/data/sync-manager');
|
|
2902
|
+
const IntelligentSelector = require('../src/models/intelligent-selector');
|
|
2903
|
+
const UnifiedDetector = require('../src/hardware/unified-detector');
|
|
2904
|
+
|
|
2905
|
+
const spinner = options.json ? null : ora('Analyzing hardware and models...').start();
|
|
2906
|
+
|
|
2907
|
+
try {
|
|
2908
|
+
// Detect hardware
|
|
2909
|
+
const detector = new UnifiedDetector();
|
|
2910
|
+
const hardware = await detector.detect();
|
|
2911
|
+
|
|
2912
|
+
if (spinner) spinner.text = 'Loading model database...';
|
|
2913
|
+
|
|
2914
|
+
// Load models from database
|
|
2915
|
+
const syncManager = new SyncManager({ onProgress: () => {} });
|
|
2916
|
+
await syncManager.init();
|
|
2917
|
+
|
|
2918
|
+
const syncStatus = await syncManager.needsSync();
|
|
2919
|
+
if (syncStatus.needed) {
|
|
2920
|
+
if (spinner) spinner.text = 'Syncing model database (first time takes a few minutes)...';
|
|
2921
|
+
await syncManager.sync();
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// Get all variants that might fit
|
|
2925
|
+
const maxSize = detector.getMaxModelSize() + 2;
|
|
2926
|
+
const variants = await syncManager.getCompatibleVariants(maxSize, {});
|
|
2927
|
+
|
|
2928
|
+
if (spinner) spinner.text = `Scoring ${variants.length} model variants...`;
|
|
2929
|
+
|
|
2930
|
+
// Get intelligent recommendations
|
|
2931
|
+
const selector = new IntelligentSelector({ detector });
|
|
2932
|
+
const recommendations = await selector.recommend(variants, {
|
|
2933
|
+
useCase: options.useCase,
|
|
2934
|
+
targetTPS: parseInt(options.targetTps) || 20,
|
|
2935
|
+
targetContext: parseInt(options.targetContext) || 8192,
|
|
2936
|
+
includeVision: options.includeVision,
|
|
2937
|
+
includeEmbeddings: options.includeEmbeddings,
|
|
2938
|
+
limit: parseInt(options.limit)
|
|
2939
|
+
});
|
|
2940
|
+
|
|
2941
|
+
syncManager.close();
|
|
2942
|
+
|
|
2943
|
+
if (options.json) {
|
|
2944
|
+
console.log(JSON.stringify(recommendations, null, 2));
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
if (spinner) spinner.succeed('Analysis complete!');
|
|
2949
|
+
|
|
2950
|
+
// Display hardware info
|
|
2951
|
+
console.log(chalk.blue.bold('\n=== Hardware Analysis ==='));
|
|
2952
|
+
console.log(chalk.white(` ${recommendations.hardware.description}`));
|
|
2953
|
+
console.log(chalk.gray(` Tier: ${recommendations.hardware.tier.replace('_', ' ').toUpperCase()}`));
|
|
2954
|
+
console.log(chalk.gray(` Backend: ${recommendations.hardware.backend}`));
|
|
2955
|
+
console.log(chalk.gray(` Max model size: ${recommendations.hardware.maxSize}GB`));
|
|
2956
|
+
|
|
2957
|
+
// Display top picks
|
|
2958
|
+
console.log(chalk.blue.bold('\n=== Top Recommendations ==='));
|
|
2959
|
+
|
|
2960
|
+
// Helper to format model name (tag already contains model:variant)
|
|
2961
|
+
const formatModelName = (v) => {
|
|
2962
|
+
const fullTag = v.tag || 'latest';
|
|
2963
|
+
return fullTag.includes(':') ? fullTag : `${v.model_id}:${fullTag}`;
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
const picks = recommendations.topPicks;
|
|
2967
|
+
if (picks.best) {
|
|
2968
|
+
const v = picks.best.variant;
|
|
2969
|
+
const s = picks.best.score;
|
|
2970
|
+
const name = formatModelName(v);
|
|
2971
|
+
console.log(chalk.green.bold('\n[BEST] Best Overall:'));
|
|
2972
|
+
console.log(chalk.white.bold(` ${name}`));
|
|
2973
|
+
console.log(chalk.gray(` ${v.params_b || '?'}B params | ${v.size_gb || '?'}GB | ${v.quant || 'Q4_K_M'}`));
|
|
2974
|
+
console.log(chalk.cyan(` Score: ${s.final}/100 (Q:${s.components.quality} S:${s.components.speed} F:${s.components.fit})`));
|
|
2975
|
+
console.log(chalk.yellow(` ~${s.meta.estimatedTPS} tokens/sec`));
|
|
2976
|
+
console.log(chalk.cyan(` ollama pull ${name}`));
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
if (picks.fast && picks.fast !== picks.best) {
|
|
2980
|
+
const v = picks.fast.variant;
|
|
2981
|
+
const s = picks.fast.score;
|
|
2982
|
+
const name = formatModelName(v);
|
|
2983
|
+
console.log(chalk.blue.bold('\n⚡ Fastest:'));
|
|
2984
|
+
console.log(chalk.white(` ${name}`));
|
|
2985
|
+
console.log(chalk.gray(` ${v.params_b || '?'}B | ${v.size_gb || '?'}GB | ~${s.meta.estimatedTPS} tok/s`));
|
|
2986
|
+
console.log(chalk.cyan(` ollama pull ${name}`));
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
if (picks.quality && picks.quality !== picks.best) {
|
|
2990
|
+
const v = picks.quality.variant;
|
|
2991
|
+
const s = picks.quality.score;
|
|
2992
|
+
const name = formatModelName(v);
|
|
2993
|
+
console.log(chalk.magenta.bold('\nHighest Quality:'));
|
|
2994
|
+
console.log(chalk.white(` ${name}`));
|
|
2995
|
+
console.log(chalk.gray(` ${v.params_b || '?'}B | ${v.size_gb || '?'}GB | Quality: ${s.components.quality}/100`));
|
|
2996
|
+
console.log(chalk.cyan(` ollama pull ${name}`));
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// Show other recommendations
|
|
3000
|
+
if (recommendations.all.length > 1) {
|
|
3001
|
+
console.log(chalk.blue.bold('\n=== Other Good Options ==='));
|
|
3002
|
+
for (const item of recommendations.all.slice(1, parseInt(options.limit))) {
|
|
3003
|
+
const v = item.variant;
|
|
3004
|
+
const s = item.score;
|
|
3005
|
+
const name = formatModelName(v);
|
|
3006
|
+
console.log(
|
|
3007
|
+
chalk.gray(`[${s.final}] `) +
|
|
3008
|
+
chalk.white(name) +
|
|
3009
|
+
chalk.gray(` - ${v.params_b || '?'}B, ${v.size_gb || '?'}GB`)
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// Show insights
|
|
3015
|
+
if (recommendations.insights.length > 0) {
|
|
3016
|
+
console.log(chalk.blue.bold('\n=== Insights ==='));
|
|
3017
|
+
for (const insight of recommendations.insights) {
|
|
3018
|
+
const icon = insight.type === 'success' ? chalk.green('[OK]') :
|
|
3019
|
+
insight.type === 'warning' ? chalk.yellow('[!]') :
|
|
3020
|
+
insight.type === 'tip' ? chalk.cyan('[TIP]') : chalk.blue('[i]');
|
|
3021
|
+
console.log(` ${icon} ${insight.message}`);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
console.log('');
|
|
3026
|
+
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
if (spinner) spinner.fail('Recommendation failed');
|
|
3029
|
+
console.error(chalk.red('Error:'), error.message);
|
|
3030
|
+
if (process.env.DEBUG) console.error(error.stack);
|
|
3031
|
+
process.exit(1);
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
|
|
3035
|
+
program
|
|
3036
|
+
.command('hw-detect')
|
|
3037
|
+
.description('Detect and display detailed hardware capabilities')
|
|
3038
|
+
.option('-j, --json', 'Output as JSON')
|
|
3039
|
+
.action(async (options) => {
|
|
3040
|
+
if (!options.json) showAsciiArt('hw-detect');
|
|
3041
|
+
const UnifiedDetector = require('../src/hardware/unified-detector');
|
|
3042
|
+
|
|
3043
|
+
const spinner = options.json ? null : ora('Detecting hardware...').start();
|
|
3044
|
+
|
|
3045
|
+
try {
|
|
3046
|
+
const detector = new UnifiedDetector();
|
|
3047
|
+
const hardware = await detector.detect();
|
|
3048
|
+
|
|
3049
|
+
if (options.json) {
|
|
3050
|
+
console.log(JSON.stringify(hardware, null, 2));
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
if (spinner) spinner.succeed('Hardware detected!');
|
|
3055
|
+
|
|
3056
|
+
console.log(chalk.blue.bold('\n=== Hardware Detection ===\n'));
|
|
3057
|
+
|
|
3058
|
+
// Summary
|
|
3059
|
+
console.log(chalk.white.bold('Summary:'));
|
|
3060
|
+
console.log(` ${detector.getHardwareDescription()}`);
|
|
3061
|
+
console.log(` Tier: ${chalk.cyan(detector.getHardwareTier().replace('_', ' ').toUpperCase())}`);
|
|
3062
|
+
console.log(` Max model size: ${chalk.green(detector.getMaxModelSize() + 'GB')}`);
|
|
3063
|
+
console.log(` Best backend: ${chalk.cyan(hardware.summary.bestBackend)}`);
|
|
3064
|
+
|
|
3065
|
+
// CPU
|
|
3066
|
+
if (hardware.cpu) {
|
|
3067
|
+
console.log(chalk.blue.bold('\nCPU:'));
|
|
3068
|
+
console.log(` ${hardware.cpu.brand}`);
|
|
3069
|
+
console.log(` Cores: ${hardware.cpu.cores.logical} (${hardware.cpu.cores.physical} physical)`);
|
|
3070
|
+
console.log(` SIMD: ${hardware.cpu.capabilities.bestSimd}`);
|
|
3071
|
+
if (hardware.cpu.capabilities.avx512) console.log(chalk.green(' [OK] AVX-512'));
|
|
3072
|
+
if (hardware.cpu.capabilities.avx2) console.log(chalk.green(' [OK] AVX2'));
|
|
3073
|
+
if (hardware.cpu.capabilities.neon) console.log(chalk.green(' [OK] ARM NEON'));
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// GPU backends
|
|
3077
|
+
for (const [backend, info] of Object.entries(hardware.backends)) {
|
|
3078
|
+
if (!info.available || backend === 'cpu') continue;
|
|
3079
|
+
|
|
3080
|
+
console.log(chalk.blue.bold(`\n${backend.toUpperCase()}:`));
|
|
3081
|
+
|
|
3082
|
+
if (backend === 'metal' && info.info) {
|
|
3083
|
+
console.log(` ${info.info.chip}`);
|
|
3084
|
+
console.log(` GPU Cores: ${info.info.gpu.cores}`);
|
|
3085
|
+
console.log(` Unified Memory: ${info.info.memory.unified}GB`);
|
|
3086
|
+
console.log(` Memory Bandwidth: ${info.info.memory.bandwidth}GB/s`);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
if (backend === 'cuda' && info.info) {
|
|
3090
|
+
console.log(` Driver: ${info.info.driver}`);
|
|
3091
|
+
console.log(` CUDA: ${info.info.cuda}`);
|
|
3092
|
+
console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
|
|
3093
|
+
for (const gpu of info.info.gpus) {
|
|
3094
|
+
console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
if (backend === 'rocm' && info.info) {
|
|
3099
|
+
console.log(` ROCm: ${info.info.rocmVersion}`);
|
|
3100
|
+
console.log(` Total VRAM: ${info.info.totalVRAM}GB`);
|
|
3101
|
+
for (const gpu of info.info.gpus) {
|
|
3102
|
+
console.log(` ${gpu.name}: ${gpu.memory.total}GB`);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
console.log(chalk.gray(`\nFingerprint: ${hardware.fingerprint}`));
|
|
3108
|
+
console.log('');
|
|
3109
|
+
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
if (spinner) spinner.fail('Detection failed');
|
|
3112
|
+
console.error(chalk.red('Error:'), error.message);
|
|
3113
|
+
if (process.env.DEBUG) console.error(error.stack);
|
|
3114
|
+
process.exit(1);
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
|
|
3118
|
+
program.parse();
|