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,357 @@
|
|
|
1
|
+
const OllamaClient = require('./client');
|
|
2
|
+
const { EventEmitter } = require('events');
|
|
3
|
+
|
|
4
|
+
class OllamaManager extends EventEmitter {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super();
|
|
7
|
+
this.client = new OllamaClient(options.baseURL);
|
|
8
|
+
this.modelQueue = [];
|
|
9
|
+
this.isProcessing = false;
|
|
10
|
+
this.maxConcurrent = options.maxConcurrent || 1;
|
|
11
|
+
this.autoCleanup = options.autoCleanup || true;
|
|
12
|
+
this.cleanupInterval = options.cleanupInterval || 30 * 60 * 1000; // 30 minutes
|
|
13
|
+
|
|
14
|
+
if (this.autoCleanup) {
|
|
15
|
+
this.startCleanupTimer();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async initializeManager() {
|
|
20
|
+
try {
|
|
21
|
+
const status = await this.client.checkOllamaAvailability();
|
|
22
|
+
if (!status.available) {
|
|
23
|
+
throw new Error(`Ollama not available: ${status.error}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.emit('initialized', status);
|
|
27
|
+
return status;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
this.emit('error', error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async installModel(modelName, options = {}) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const task = {
|
|
37
|
+
type: 'install',
|
|
38
|
+
modelName,
|
|
39
|
+
options,
|
|
40
|
+
resolve,
|
|
41
|
+
reject,
|
|
42
|
+
progress: options.onProgress || (() => {})
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.modelQueue.push(task);
|
|
46
|
+
this.processQueue();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async removeModel(modelName) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const task = {
|
|
53
|
+
type: 'remove',
|
|
54
|
+
modelName,
|
|
55
|
+
resolve,
|
|
56
|
+
reject
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.modelQueue.push(task);
|
|
60
|
+
this.processQueue();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async updateModel(modelName) {
|
|
65
|
+
// Update is essentially a re-pull
|
|
66
|
+
return this.installModel(modelName, { force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async processQueue() {
|
|
70
|
+
if (this.isProcessing || this.modelQueue.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.isProcessing = true;
|
|
75
|
+
const task = this.modelQueue.shift();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
this.emit('taskStarted', { type: task.type, model: task.modelName });
|
|
79
|
+
|
|
80
|
+
let result;
|
|
81
|
+
if (task.type === 'install') {
|
|
82
|
+
result = await this.client.pullModel(task.modelName, task.progress);
|
|
83
|
+
} else if (task.type === 'remove') {
|
|
84
|
+
result = await this.client.deleteModel(task.modelName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.emit('taskCompleted', { type: task.type, model: task.modelName, result });
|
|
88
|
+
task.resolve(result);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.emit('taskFailed', { type: task.type, model: task.modelName, error });
|
|
91
|
+
task.reject(error);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.isProcessing = false;
|
|
95
|
+
|
|
96
|
+
// Process next task
|
|
97
|
+
if (this.modelQueue.length > 0) {
|
|
98
|
+
setTimeout(() => this.processQueue(), 1000);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getModelStatus(modelName) {
|
|
103
|
+
try {
|
|
104
|
+
const localModels = await this.client.getLocalModels();
|
|
105
|
+
const runningModels = await this.client.getRunningModels();
|
|
106
|
+
|
|
107
|
+
const localModel = localModels.find(m => m.name === modelName);
|
|
108
|
+
const runningModel = runningModels.find(m => m.name === modelName);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
installed: !!localModel,
|
|
112
|
+
running: !!runningModel,
|
|
113
|
+
details: localModel || null,
|
|
114
|
+
runtime: runningModel || null
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new Error(`Failed to get model status: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getAllModelsStatus() {
|
|
122
|
+
try {
|
|
123
|
+
const [localModels, runningModels] = await Promise.all([
|
|
124
|
+
this.client.getLocalModels(),
|
|
125
|
+
this.client.getRunningModels()
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const runningSet = new Set(runningModels.map(m => m.name));
|
|
129
|
+
|
|
130
|
+
return localModels.map(model => ({
|
|
131
|
+
...model,
|
|
132
|
+
running: runningSet.has(model.name),
|
|
133
|
+
runtime: runningModels.find(r => r.name === model.name) || null
|
|
134
|
+
}));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error(`Failed to get models status: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async optimizeModels(hardware) {
|
|
141
|
+
try {
|
|
142
|
+
const modelsStatus = await this.getAllModelsStatus();
|
|
143
|
+
const recommendations = [];
|
|
144
|
+
|
|
145
|
+
// Find models that could be optimized
|
|
146
|
+
for (const model of modelsStatus) {
|
|
147
|
+
const analysis = await this.analyzeModelOptimization(model, hardware);
|
|
148
|
+
if (analysis.canOptimize) {
|
|
149
|
+
recommendations.push(analysis);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
totalModels: modelsStatus.length,
|
|
155
|
+
optimizable: recommendations.length,
|
|
156
|
+
recommendations,
|
|
157
|
+
estimatedSavings: this.calculateSavings(recommendations)
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(`Failed to optimize models: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async analyzeModelOptimization(model, hardware) {
|
|
165
|
+
// Analyze if a model can be optimized (re-quantized, etc.)
|
|
166
|
+
const currentQuant = model.quantization || 'Unknown';
|
|
167
|
+
const modelSizeGB = model.fileSizeGB;
|
|
168
|
+
|
|
169
|
+
// Suggest better quantization
|
|
170
|
+
let suggestedQuant = currentQuant;
|
|
171
|
+
let canOptimize = false;
|
|
172
|
+
|
|
173
|
+
if (hardware.memory.total >= 32 && currentQuant === 'Q4_0') {
|
|
174
|
+
suggestedQuant = 'Q5_K_M';
|
|
175
|
+
canOptimize = true;
|
|
176
|
+
} else if (hardware.memory.total <= 8 && currentQuant === 'Q8_0') {
|
|
177
|
+
suggestedQuant = 'Q4_K_M';
|
|
178
|
+
canOptimize = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
model: model.name,
|
|
183
|
+
currentQuantization: currentQuant,
|
|
184
|
+
suggestedQuantization: suggestedQuant,
|
|
185
|
+
currentSize: modelSizeGB,
|
|
186
|
+
estimatedNewSize: canOptimize ? modelSizeGB * 0.8 : modelSizeGB,
|
|
187
|
+
canOptimize,
|
|
188
|
+
reason: canOptimize ?
|
|
189
|
+
`Better quantization available for your hardware` :
|
|
190
|
+
'Current quantization is optimal'
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
calculateSavings(recommendations) {
|
|
195
|
+
const totalCurrentSize = recommendations.reduce((sum, r) => sum + r.currentSize, 0);
|
|
196
|
+
const totalNewSize = recommendations.reduce((sum, r) => sum + r.estimatedNewSize, 0);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
currentSize: Math.round(totalCurrentSize * 10) / 10,
|
|
200
|
+
newSize: Math.round(totalNewSize * 10) / 10,
|
|
201
|
+
saved: Math.round((totalCurrentSize - totalNewSize) * 10) / 10,
|
|
202
|
+
percentage: Math.round(((totalCurrentSize - totalNewSize) / totalCurrentSize) * 100)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async cleanupUnusedModels() {
|
|
207
|
+
try {
|
|
208
|
+
const runningModels = await this.client.getRunningModels();
|
|
209
|
+
const allModels = await this.client.getLocalModels();
|
|
210
|
+
|
|
211
|
+
// Find models not running for extended period
|
|
212
|
+
const candidates = allModels.filter(model => {
|
|
213
|
+
const isRunning = runningModels.some(r => r.name === model.name);
|
|
214
|
+
const lastModified = new Date(model.modified);
|
|
215
|
+
const daysSinceModified = (Date.now() - lastModified.getTime()) / (1000 * 60 * 60 * 24);
|
|
216
|
+
|
|
217
|
+
return !isRunning && daysSinceModified > 30; // Not used in 30 days
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this.emit('cleanupCandidatesFound', candidates);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
totalModels: allModels.length,
|
|
224
|
+
runningModels: runningModels.length,
|
|
225
|
+
cleanupCandidates: candidates.length,
|
|
226
|
+
candidates: candidates.map(m => ({
|
|
227
|
+
name: m.name,
|
|
228
|
+
size: m.fileSizeGB,
|
|
229
|
+
lastUsed: m.modified
|
|
230
|
+
}))
|
|
231
|
+
};
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new Error(`Failed to cleanup analysis: ${error.message}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async performCleanup(modelNames) {
|
|
238
|
+
const results = [];
|
|
239
|
+
|
|
240
|
+
for (const modelName of modelNames) {
|
|
241
|
+
try {
|
|
242
|
+
await this.removeModel(modelName);
|
|
243
|
+
results.push({ model: modelName, success: true });
|
|
244
|
+
this.emit('modelCleaned', modelName);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
results.push({ model: modelName, success: false, error: error.message });
|
|
247
|
+
this.emit('cleanupError', { model: modelName, error });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async benchmarkModel(modelName, options = {}) {
|
|
255
|
+
const testPrompts = options.prompts || [
|
|
256
|
+
"Hello, how are you?",
|
|
257
|
+
"Explain quantum computing in simple terms.",
|
|
258
|
+
"Write a short Python function to sort a list."
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
const results = [];
|
|
262
|
+
|
|
263
|
+
for (const prompt of testPrompts) {
|
|
264
|
+
try {
|
|
265
|
+
const result = await this.client.testModelPerformance(modelName, prompt);
|
|
266
|
+
results.push({
|
|
267
|
+
prompt: prompt.substring(0, 50) + (prompt.length > 50 ? '...' : ''),
|
|
268
|
+
...result
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
results.push({
|
|
272
|
+
prompt: prompt.substring(0, 50) + (prompt.length > 50 ? '...' : ''),
|
|
273
|
+
success: false,
|
|
274
|
+
error: error.message
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Calculate averages
|
|
280
|
+
const successful = results.filter(r => r.success);
|
|
281
|
+
const avgTokensPerSecond = successful.length > 0 ?
|
|
282
|
+
successful.reduce((sum, r) => sum + r.tokensPerSecond, 0) / successful.length : 0;
|
|
283
|
+
const avgResponseTime = successful.length > 0 ?
|
|
284
|
+
successful.reduce((sum, r) => sum + r.responseTime, 0) / successful.length : 0;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
model: modelName,
|
|
288
|
+
testCount: testPrompts.length,
|
|
289
|
+
successfulTests: successful.length,
|
|
290
|
+
failedTests: results.length - successful.length,
|
|
291
|
+
averageTokensPerSecond: Math.round(avgTokensPerSecond * 10) / 10,
|
|
292
|
+
averageResponseTime: Math.round(avgResponseTime),
|
|
293
|
+
detailedResults: results
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
startCleanupTimer() {
|
|
298
|
+
setInterval(async () => {
|
|
299
|
+
try {
|
|
300
|
+
const analysis = await this.cleanupUnusedModels();
|
|
301
|
+
if (analysis.cleanupCandidates > 0) {
|
|
302
|
+
this.emit('cleanupSuggested', analysis);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
this.emit('error', error);
|
|
306
|
+
}
|
|
307
|
+
}, this.cleanupInterval);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async getStatistics() {
|
|
311
|
+
try {
|
|
312
|
+
const [localModels, runningModels] = await Promise.all([
|
|
313
|
+
this.client.getLocalModels(),
|
|
314
|
+
this.client.getRunningModels()
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const totalSize = localModels.reduce((sum, m) => sum + m.fileSizeGB, 0);
|
|
318
|
+
const avgSize = localModels.length > 0 ? totalSize / localModels.length : 0;
|
|
319
|
+
|
|
320
|
+
// Group by quantization
|
|
321
|
+
const quantizationStats = {};
|
|
322
|
+
localModels.forEach(model => {
|
|
323
|
+
const quant = model.quantization || 'Unknown';
|
|
324
|
+
quantizationStats[quant] = (quantizationStats[quant] || 0) + 1;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Group by family
|
|
328
|
+
const familyStats = {};
|
|
329
|
+
localModels.forEach(model => {
|
|
330
|
+
const family = model.family || 'Unknown';
|
|
331
|
+
familyStats[family] = (familyStats[family] || 0) + 1;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
total: localModels.length,
|
|
336
|
+
running: runningModels.length,
|
|
337
|
+
totalSizeGB: Math.round(totalSize * 10) / 10,
|
|
338
|
+
averageSizeGB: Math.round(avgSize * 10) / 10,
|
|
339
|
+
quantizationBreakdown: quantizationStats,
|
|
340
|
+
familyBreakdown: familyStats,
|
|
341
|
+
queueLength: this.modelQueue.length,
|
|
342
|
+
isProcessing: this.isProcessing
|
|
343
|
+
};
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw new Error(`Failed to get statistics: ${error.message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
destroy() {
|
|
350
|
+
// Clean up resources
|
|
351
|
+
this.removeAllListeners();
|
|
352
|
+
this.modelQueue = [];
|
|
353
|
+
this.isProcessing = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = OllamaManager;
|