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,279 @@
1
+ /**
2
+ * Sync Manager - Coordinates scraping and database updates
3
+ * Handles initial sync and incremental updates
4
+ */
5
+
6
+ const ModelDatabase = require('./model-database');
7
+ const EnhancedOllamaScraper = require('../ollama/enhanced-scraper');
8
+
9
+ class SyncManager {
10
+ constructor(options = {}) {
11
+ this.db = options.database || new ModelDatabase();
12
+ this.scraper = options.scraper || new EnhancedOllamaScraper({
13
+ concurrency: options.concurrency || 5,
14
+ rateLimitMs: options.rateLimitMs || 200,
15
+ onProgress: options.onProgress || this.defaultOnProgress.bind(this),
16
+ onError: options.onError || console.error
17
+ });
18
+
19
+ this.onProgress = options.onProgress || this.defaultOnProgress.bind(this);
20
+ this.onError = options.onError || console.error;
21
+ }
22
+
23
+ /**
24
+ * Default progress handler
25
+ */
26
+ defaultOnProgress(info) {
27
+ if (info.phase === 'details' && info.current && info.total) {
28
+ const pct = Math.round((info.current / info.total) * 100);
29
+ process.stdout.write(`\r[${pct}%] ${info.message} `);
30
+ } else {
31
+ console.log(info.message);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Initialize database
37
+ */
38
+ async init() {
39
+ await this.db.initialize();
40
+ }
41
+
42
+ /**
43
+ * Perform full sync from scratch
44
+ */
45
+ async fullSync() {
46
+ await this.init();
47
+
48
+ this.onProgress({ phase: 'start', message: 'Starting full sync...' });
49
+
50
+ // Clear existing data
51
+ this.db.clear();
52
+
53
+ // Scrape all models
54
+ const result = await this.scraper.scrapeAll((model, variants) => {
55
+ // Save model as we go
56
+ this.db.upsertModel(model);
57
+
58
+ // Save variants
59
+ for (const variant of variants) {
60
+ this.db.upsertVariant(variant);
61
+ }
62
+ });
63
+
64
+ // Update sync timestamp
65
+ this.db.setLastSync(new Date().toISOString());
66
+
67
+ const stats = this.db.getStats();
68
+
69
+ this.onProgress({
70
+ phase: 'complete',
71
+ message: `Sync complete: ${stats.models} models, ${stats.variants} variants`,
72
+ stats
73
+ });
74
+
75
+ return stats;
76
+ }
77
+
78
+ /**
79
+ * Perform incremental sync (update only changed models)
80
+ */
81
+ async incrementalSync() {
82
+ await this.init();
83
+
84
+ const lastSync = this.db.getLastSync();
85
+
86
+ if (!lastSync) {
87
+ // No previous sync, do full sync
88
+ return this.fullSync();
89
+ }
90
+
91
+ this.onProgress({ phase: 'start', message: 'Starting incremental sync...' });
92
+
93
+ // Get current model list
94
+ const modelList = await this.scraper.scrapeModelList();
95
+
96
+ // Get existing models from DB
97
+ const existingModels = new Set(
98
+ this.db.all(`SELECT id FROM models`).map(m => m.id)
99
+ );
100
+
101
+ // Find new and potentially updated models
102
+ const newModels = modelList.filter(m => !existingModels.has(m.id));
103
+ const toUpdate = modelList.filter(m => existingModels.has(m.id));
104
+
105
+ this.onProgress({
106
+ phase: 'incremental',
107
+ message: `Found ${newModels.length} new models, checking ${toUpdate.length} for updates...`
108
+ });
109
+
110
+ let updated = 0;
111
+ let added = 0;
112
+
113
+ // Process new models
114
+ for (const { id } of newModels) {
115
+ try {
116
+ const model = await this.scraper.scrapeModelDetails(id);
117
+ if (model) {
118
+ this.db.upsertModel(model);
119
+ added++;
120
+
121
+ const variants = await this.scraper.scrapeModelTags(id);
122
+ for (const variant of variants) {
123
+ this.db.upsertVariant(variant);
124
+ }
125
+ }
126
+
127
+ await this.sleep(200);
128
+ } catch (error) {
129
+ this.onError(`Error syncing ${id}: ${error.message}`);
130
+ }
131
+ }
132
+
133
+ // Check for updates in existing models (sample top 50 by pulls)
134
+ const topModels = toUpdate.sort((a, b) => (b.pulls || 0) - (a.pulls || 0)).slice(0, 50);
135
+
136
+ for (const { id } of topModels) {
137
+ try {
138
+ const model = await this.scraper.scrapeModelDetails(id);
139
+ if (model) {
140
+ const existing = this.db.get(`SELECT pulls, tags_count FROM models WHERE id = ?`, [id]);
141
+
142
+ // Update if pulls or tags changed significantly
143
+ if (!existing ||
144
+ Math.abs((existing.pulls || 0) - (model.pulls || 0)) > 1000 ||
145
+ (existing.tags_count || 0) !== (model.tags_count || 0)) {
146
+
147
+ this.db.upsertModel(model);
148
+
149
+ const variants = await this.scraper.scrapeModelTags(id);
150
+ for (const variant of variants) {
151
+ this.db.upsertVariant(variant);
152
+ }
153
+
154
+ updated++;
155
+ }
156
+ }
157
+
158
+ await this.sleep(100);
159
+ } catch (error) {
160
+ // Ignore errors during incremental update
161
+ }
162
+ }
163
+
164
+ // Update sync timestamp
165
+ this.db.setLastSync(new Date().toISOString());
166
+
167
+ const stats = this.db.getStats();
168
+
169
+ this.onProgress({
170
+ phase: 'complete',
171
+ message: `Incremental sync complete: ${added} added, ${updated} updated`,
172
+ stats
173
+ });
174
+
175
+ return { added, updated, stats };
176
+ }
177
+
178
+ /**
179
+ * Smart sync - chooses full or incremental based on conditions
180
+ */
181
+ async sync(options = {}) {
182
+ await this.init();
183
+
184
+ const force = options.force || false;
185
+ const lastSync = this.db.getLastSync();
186
+ const modelCount = this.db.getModelCount();
187
+
188
+ // Full sync if:
189
+ // - Force flag is set
190
+ // - No previous sync
191
+ // - Less than 100 models (probably incomplete)
192
+ // - Last sync was more than 7 days ago
193
+ if (force || !lastSync || modelCount < 100) {
194
+ return this.fullSync();
195
+ }
196
+
197
+ const lastSyncDate = new Date(lastSync);
198
+ const daysSinceSync = (Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60 * 24);
199
+
200
+ if (daysSinceSync > 7) {
201
+ return this.fullSync();
202
+ }
203
+
204
+ return this.incrementalSync();
205
+ }
206
+
207
+ /**
208
+ * Check if sync is needed
209
+ */
210
+ async needsSync() {
211
+ await this.init();
212
+
213
+ const lastSync = this.db.getLastSync();
214
+ const modelCount = this.db.getModelCount();
215
+
216
+ if (!lastSync || modelCount < 100) {
217
+ return { needed: true, reason: 'No previous sync or incomplete data' };
218
+ }
219
+
220
+ const lastSyncDate = new Date(lastSync);
221
+ const hoursSinceSync = (Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60);
222
+
223
+ if (hoursSinceSync > 24) {
224
+ return { needed: true, reason: `Last sync was ${Math.round(hoursSinceSync)} hours ago` };
225
+ }
226
+
227
+ return { needed: false, stats: this.db.getStats() };
228
+ }
229
+
230
+ /**
231
+ * Get database stats
232
+ */
233
+ async getStats() {
234
+ await this.init();
235
+ return this.db.getStats();
236
+ }
237
+
238
+ /**
239
+ * Search models
240
+ */
241
+ async search(query, filters = {}) {
242
+ await this.init();
243
+ return this.db.searchModels(query, filters);
244
+ }
245
+
246
+ /**
247
+ * Search for variants (not just models)
248
+ */
249
+ async searchVariants(query, filters = {}) {
250
+ await this.init();
251
+ return this.db.searchVariants(query, filters);
252
+ }
253
+
254
+ /**
255
+ * Get variants for hardware constraints
256
+ */
257
+ async getCompatibleVariants(maxSizeGB, filters = {}) {
258
+ await this.init();
259
+ return this.db.getVariantsForHardware(maxSizeGB, filters);
260
+ }
261
+
262
+ /**
263
+ * Sleep utility
264
+ */
265
+ sleep(ms) {
266
+ return new Promise(resolve => setTimeout(resolve, ms));
267
+ }
268
+
269
+ /**
270
+ * Close database connection
271
+ */
272
+ close() {
273
+ if (this.db) {
274
+ this.db.close();
275
+ }
276
+ }
277
+ }
278
+
279
+ module.exports = SyncManager;
@@ -0,0 +1,12 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ ### Feb 12, 2026
7
+
8
+ | ID | Time | T | Title | Read |
9
+ |----|------|---|-------|------|
10
+ | #3490 | 10:24 PM | 🔵 | Hardware Detector Cache Implementation - 5-Minute TTL Without Force Refresh Option | ~536 |
11
+ | #3440 | 9:58 PM | 🔵 | Hardware Detection System - Multi-GPU Support with Intelligent Selection | ~611 |
12
+ </claude-mem-context>
@@ -0,0 +1,11 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ ### Feb 12, 2026
7
+
8
+ | ID | Time | T | Title | Read |
9
+ |----|------|---|-------|------|
10
+ | #3453 | 10:01 PM | 🔵 | CUDA Detector Implementation - NVIDIA GPU Detection via nvidia-smi | ~497 |
11
+ </claude-mem-context>
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Apple Silicon Detector
3
+ * Detects M1, M2, M3, M4 chips and their capabilities
4
+ * Uses sysctl and system_profiler for accurate detection
5
+ */
6
+
7
+ const { execSync } = require('child_process');
8
+
9
+ class AppleSiliconDetector {
10
+ constructor() {
11
+ this.cache = null;
12
+ this.isSupported = process.platform === 'darwin' && process.arch === 'arm64';
13
+ }
14
+
15
+ /**
16
+ * Detect if running on Apple Silicon
17
+ */
18
+ detect() {
19
+ if (!this.isSupported) {
20
+ return null;
21
+ }
22
+
23
+ if (this.cache) {
24
+ return this.cache;
25
+ }
26
+
27
+ try {
28
+ const info = this.getChipInfo();
29
+ this.cache = info;
30
+ return info;
31
+ } catch (error) {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get detailed chip information using sysctl
38
+ */
39
+ getChipInfo() {
40
+ const result = {
41
+ chip: null,
42
+ variant: null, // base, pro, max, ultra
43
+ generation: null, // 1, 2, 3, 4
44
+ cores: {
45
+ performance: 0,
46
+ efficiency: 0,
47
+ total: 0
48
+ },
49
+ gpu: {
50
+ cores: 0,
51
+ model: null
52
+ },
53
+ neuralEngine: {
54
+ cores: 0
55
+ },
56
+ memory: {
57
+ unified: 0,
58
+ bandwidth: null
59
+ },
60
+ capabilities: {
61
+ metal: true,
62
+ metalVersion: null,
63
+ fp16: true,
64
+ int8: true,
65
+ amx: true
66
+ },
67
+ backend: 'metal',
68
+ speedCoefficient: 0
69
+ };
70
+
71
+ // Get chip brand
72
+ try {
73
+ const brand = execSync('sysctl -n machdep.cpu.brand_string', { encoding: 'utf8', timeout: 5000 }).trim();
74
+ result.chip = brand;
75
+
76
+ // Parse chip variant and generation
77
+ const parsed = this.parseChipBrand(brand);
78
+ result.variant = parsed.variant;
79
+ result.generation = parsed.generation;
80
+ } catch (e) {
81
+ // Fallback
82
+ }
83
+
84
+ // Get CPU cores
85
+ try {
86
+ result.cores.total = parseInt(execSync('sysctl -n hw.ncpu', { encoding: 'utf8', timeout: 5000 }).trim());
87
+ result.cores.performance = parseInt(execSync('sysctl -n hw.perflevel0.logicalcpu', { encoding: 'utf8', timeout: 5000 }).trim()) || Math.ceil(result.cores.total / 2);
88
+ result.cores.efficiency = parseInt(execSync('sysctl -n hw.perflevel1.logicalcpu', { encoding: 'utf8', timeout: 5000 }).trim()) || Math.floor(result.cores.total / 2);
89
+ } catch (e) {
90
+ result.cores.total = require('os').cpus().length;
91
+ }
92
+
93
+ // Get memory (unified memory on Apple Silicon)
94
+ try {
95
+ const memBytes = parseInt(execSync('sysctl -n hw.memsize', { encoding: 'utf8', timeout: 5000 }).trim());
96
+ result.memory.unified = Math.round(memBytes / (1024 ** 3));
97
+ } catch (e) {
98
+ result.memory.unified = Math.round(require('os').totalmem() / (1024 ** 3));
99
+ }
100
+
101
+ // Get GPU cores from system_profiler
102
+ try {
103
+ const gpuInfo = execSync('system_profiler SPDisplaysDataType -json', {
104
+ encoding: 'utf8',
105
+ timeout: 5000
106
+ });
107
+ const parsed = JSON.parse(gpuInfo);
108
+ const displays = parsed.SPDisplaysDataType || [];
109
+
110
+ if (displays.length > 0) {
111
+ const gpu = displays[0];
112
+ result.gpu.model = gpu.sppci_model || result.chip;
113
+
114
+ // Parse GPU cores from model name or estimate
115
+ const coreMatch = gpu.sppci_cores?.match(/(\d+)/);
116
+ if (coreMatch) {
117
+ result.gpu.cores = parseInt(coreMatch[1]);
118
+ } else {
119
+ result.gpu.cores = this.estimateGPUCores(result.variant, result.generation);
120
+ }
121
+ }
122
+ } catch (e) {
123
+ result.gpu.cores = this.estimateGPUCores(result.variant, result.generation);
124
+ result.gpu.model = result.chip;
125
+ }
126
+
127
+ // Get Metal version
128
+ try {
129
+ const metalVersion = execSync('system_profiler SPDisplaysDataType | grep "Metal Support"', {
130
+ encoding: 'utf8',
131
+ timeout: 5000
132
+ });
133
+ const match = metalVersion.match(/Metal\s*([\d.]+|Family)/i);
134
+ if (match) {
135
+ result.capabilities.metalVersion = match[1];
136
+ }
137
+ } catch (e) {
138
+ result.capabilities.metalVersion = '3'; // Apple Silicon supports Metal 3
139
+ }
140
+
141
+ // Calculate speed coefficient for LLM inference
142
+ result.speedCoefficient = this.calculateSpeedCoefficient(result);
143
+ result.memory.bandwidth = this.estimateMemoryBandwidth(result.variant, result.generation);
144
+
145
+ return result;
146
+ }
147
+
148
+ /**
149
+ * Parse chip brand string to extract variant and generation
150
+ */
151
+ parseChipBrand(brand) {
152
+ const result = { variant: 'base', generation: 1 };
153
+
154
+ const brandLower = brand.toLowerCase();
155
+
156
+ // Detect generation
157
+ if (brandLower.includes('m4')) result.generation = 4;
158
+ else if (brandLower.includes('m3')) result.generation = 3;
159
+ else if (brandLower.includes('m2')) result.generation = 2;
160
+ else if (brandLower.includes('m1')) result.generation = 1;
161
+
162
+ // Detect variant
163
+ if (brandLower.includes('ultra')) result.variant = 'ultra';
164
+ else if (brandLower.includes('max')) result.variant = 'max';
165
+ else if (brandLower.includes('pro')) result.variant = 'pro';
166
+ else result.variant = 'base';
167
+
168
+ return result;
169
+ }
170
+
171
+ /**
172
+ * Estimate GPU cores based on variant and generation
173
+ */
174
+ estimateGPUCores(variant, generation) {
175
+ const coreMap = {
176
+ // M1 series
177
+ '1-base': 8,
178
+ '1-pro': 16,
179
+ '1-max': 32,
180
+ '1-ultra': 64,
181
+ // M2 series
182
+ '2-base': 10,
183
+ '2-pro': 19,
184
+ '2-max': 38,
185
+ '2-ultra': 76,
186
+ // M3 series
187
+ '3-base': 10,
188
+ '3-pro': 18,
189
+ '3-max': 40,
190
+ '3-ultra': 80,
191
+ // M4 series
192
+ '4-base': 10,
193
+ '4-pro': 20,
194
+ '4-max': 40,
195
+ '4-ultra': 80
196
+ };
197
+
198
+ return coreMap[`${generation}-${variant}`] || 10;
199
+ }
200
+
201
+ /**
202
+ * Estimate memory bandwidth in GB/s
203
+ */
204
+ estimateMemoryBandwidth(variant, generation) {
205
+ const bandwidthMap = {
206
+ // M1 series
207
+ '1-base': 68,
208
+ '1-pro': 200,
209
+ '1-max': 400,
210
+ '1-ultra': 800,
211
+ // M2 series
212
+ '2-base': 100,
213
+ '2-pro': 200,
214
+ '2-max': 400,
215
+ '2-ultra': 800,
216
+ // M3 series
217
+ '3-base': 100,
218
+ '3-pro': 150,
219
+ '3-max': 400,
220
+ '3-ultra': 800,
221
+ // M4 series
222
+ '4-base': 120,
223
+ '4-pro': 273,
224
+ '4-max': 546,
225
+ '4-ultra': 800
226
+ };
227
+
228
+ return bandwidthMap[`${generation}-${variant}`] || 100;
229
+ }
230
+
231
+ /**
232
+ * Calculate speed coefficient for LLM inference (tokens/sec per B params)
233
+ */
234
+ calculateSpeedCoefficient(info) {
235
+ // Base coefficient by generation and variant
236
+ const baseCoefficients = {
237
+ '1-base': 180,
238
+ '1-pro': 200,
239
+ '1-max': 220,
240
+ '1-ultra': 240,
241
+ '2-base': 200,
242
+ '2-pro': 220,
243
+ '2-max': 240,
244
+ '2-ultra': 260,
245
+ '3-base': 220,
246
+ '3-pro': 240,
247
+ '3-max': 260,
248
+ '3-ultra': 280,
249
+ '4-base': 240,
250
+ '4-pro': 270,
251
+ '4-max': 300,
252
+ '4-ultra': 320
253
+ };
254
+
255
+ const key = `${info.generation}-${info.variant}`;
256
+ return baseCoefficients[key] || 180;
257
+ }
258
+
259
+ /**
260
+ * Check if Ollama is using Metal backend
261
+ */
262
+ async checkMetalSupport() {
263
+ if (!this.isSupported) {
264
+ return false;
265
+ }
266
+
267
+ try {
268
+ // Check if Metal framework is available
269
+ const metalCheck = execSync('ls /System/Library/Frameworks/Metal.framework', {
270
+ encoding: 'utf8',
271
+ timeout: 2000
272
+ });
273
+ return metalCheck.includes('Metal');
274
+ } catch (e) {
275
+ return true; // Assume Metal is available on Apple Silicon
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Get hardware fingerprint for benchmarks
281
+ */
282
+ getFingerprint() {
283
+ const info = this.detect();
284
+ if (!info) return null;
285
+
286
+ return `apple-${info.chip.toLowerCase().replace(/\s+/g, '-')}-${info.memory.unified}gb`;
287
+ }
288
+
289
+ /**
290
+ * Estimate inference speed for a model size
291
+ */
292
+ estimateTokensPerSecond(paramsB, quantization = 'Q4_K_M') {
293
+ const info = this.detect();
294
+ if (!info) return 0;
295
+
296
+ // Quantization multipliers (how much faster vs FP16)
297
+ const quantMult = {
298
+ 'FP16': 1.0,
299
+ 'Q8_0': 1.5,
300
+ 'Q6_K': 1.8,
301
+ 'Q5_K_M': 2.0,
302
+ 'Q5_0': 2.0,
303
+ 'Q4_K_M': 2.5,
304
+ 'Q4_0': 2.8,
305
+ 'Q3_K_M': 3.0,
306
+ 'Q2_K': 3.5,
307
+ 'IQ4_XS': 2.6,
308
+ 'IQ3_XXS': 3.2
309
+ };
310
+
311
+ const mult = quantMult[quantization] || 2.0;
312
+ const baseSpeed = info.speedCoefficient / paramsB * mult;
313
+
314
+ return Math.round(baseSpeed);
315
+ }
316
+ }
317
+
318
+ module.exports = AppleSiliconDetector;