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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/analyzer/compatibility.js +584 -0
  4. package/analyzer/performance.js +505 -0
  5. package/bin/CLAUDE.md +12 -0
  6. package/bin/enhanced_cli.js +3118 -0
  7. package/bin/test-deterministic.js +41 -0
  8. package/package.json +96 -0
  9. package/src/CLAUDE.md +12 -0
  10. package/src/ai/intelligent-selector.js +615 -0
  11. package/src/ai/model-selector.js +312 -0
  12. package/src/ai/multi-objective-selector.js +820 -0
  13. package/src/commands/check.js +58 -0
  14. package/src/data/CLAUDE.md +11 -0
  15. package/src/data/model-database.js +637 -0
  16. package/src/data/sync-manager.js +279 -0
  17. package/src/hardware/CLAUDE.md +12 -0
  18. package/src/hardware/backends/CLAUDE.md +11 -0
  19. package/src/hardware/backends/apple-silicon.js +318 -0
  20. package/src/hardware/backends/cpu-detector.js +490 -0
  21. package/src/hardware/backends/cuda-detector.js +417 -0
  22. package/src/hardware/backends/intel-detector.js +436 -0
  23. package/src/hardware/backends/rocm-detector.js +440 -0
  24. package/src/hardware/detector.js +573 -0
  25. package/src/hardware/pc-optimizer.js +635 -0
  26. package/src/hardware/specs.js +286 -0
  27. package/src/hardware/unified-detector.js +442 -0
  28. package/src/index.js +2289 -0
  29. package/src/models/CLAUDE.md +17 -0
  30. package/src/models/ai-check-selector.js +806 -0
  31. package/src/models/catalog.json +426 -0
  32. package/src/models/deterministic-selector.js +1145 -0
  33. package/src/models/expanded_database.js +1142 -0
  34. package/src/models/intelligent-selector.js +532 -0
  35. package/src/models/requirements.js +310 -0
  36. package/src/models/scoring-config.js +57 -0
  37. package/src/models/scoring-engine.js +715 -0
  38. package/src/ollama/.cache/README.md +33 -0
  39. package/src/ollama/CLAUDE.md +24 -0
  40. package/src/ollama/client.js +438 -0
  41. package/src/ollama/enhanced-client.js +113 -0
  42. package/src/ollama/enhanced-scraper.js +634 -0
  43. package/src/ollama/manager.js +357 -0
  44. package/src/ollama/native-scraper.js +776 -0
  45. package/src/plugins/CLAUDE.md +11 -0
  46. package/src/plugins/examples/custom_model_plugin.js +87 -0
  47. package/src/plugins/index.js +295 -0
  48. package/src/utils/CLAUDE.md +11 -0
  49. package/src/utils/config.js +359 -0
  50. package/src/utils/formatter.js +315 -0
  51. package/src/utils/logger.js +272 -0
  52. package/src/utils/model-classifier.js +167 -0
  53. 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;