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,58 @@
1
+ const EnhancedOllamaClient = require('../ollama/enhanced-client');
2
+
3
+ async function checkOllamaIntegration() {
4
+ const ollama = new EnhancedOllamaClient();
5
+
6
+ try {
7
+ // Verificar si Ollama está corriendo
8
+ const status = await ollama.getStatus();
9
+
10
+ if (!status.running) {
11
+ console.log('Ollama Status: Not running');
12
+ console.log(' Start with: ollama serve');
13
+ return;
14
+ }
15
+
16
+ console.log(`Ollama Status: Running (v${status.version})`);
17
+
18
+ // 🆕 Nueva funcionalidad mejorada
19
+ console.log('Analyzing compatibility with enhanced model database...');
20
+ const enhanced = await ollama.getEnhancedCompatibleModels();
21
+
22
+ console.log(`Installed: ${enhanced.installed} total, ${enhanced.compatible} compatible`);
23
+ console.log(`Available: ${enhanced.total_available} models in Ollama library`);
24
+
25
+ // Mostrar modelos compatibles encontrados
26
+ if (enhanced.compatible > 0) {
27
+ console.log('\nCompatible Models Found:');
28
+ enhanced.compatible_models.forEach(model => {
29
+ const matchType = model.match_type === 'exact' ? 'EXACT' : 'PARTIAL';
30
+ console.log(` ${matchType} ${model.name} (Score: ${model.score}/100)`);
31
+ console.log(` Local: ${model.local_name}`);
32
+ console.log(` Pulls: ${model.pulls?.toLocaleString() || 'Unknown'}`);
33
+ console.log(` Install: ${model.installation.ollama}`);
34
+ });
35
+ } else {
36
+ console.log('\nNo compatible models found in current database');
37
+ }
38
+
39
+ // Mostrar recomendaciones
40
+ if (enhanced.recommendations.length > 0) {
41
+ console.log('\nRecommended popular models:');
42
+ enhanced.recommendations.slice(0, 5).forEach(model => {
43
+ console.log(` ${model.name} (${model.pulls?.toLocaleString() || 'Unknown'} pulls)`);
44
+ console.log(` ${model.description}`);
45
+ console.log(` Install: ${model.installation.ollama}`);
46
+ });
47
+ }
48
+
49
+ // Info del cache
50
+ if (enhanced.cache_info) {
51
+ const cached = new Date(enhanced.cache_info.cached_at).toLocaleString();
52
+ console.log(`\nModel data cached at: ${cached}`);
53
+ }
54
+
55
+ } catch (error) {
56
+ console.error('Error checking Ollama integration:', error.message);
57
+ }
58
+ }
@@ -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
+ | #3464 | 10:03 PM | 🔵 | SQL Database Schema - Indexed Model Repository with Benchmarks | ~555 |
11
+ </claude-mem-context>
@@ -0,0 +1,637 @@
1
+ /**
2
+ * Model Database - SQLite storage for Ollama models
3
+ * Provides fast indexed searches across 4000+ models
4
+ */
5
+
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const fs = require('fs');
9
+
10
+ class ModelDatabase {
11
+ constructor(options = {}) {
12
+ this.dbPath = options.dbPath || path.join(os.homedir(), '.llm-checker', 'models.db');
13
+ this.db = null;
14
+ this.initialized = false;
15
+ }
16
+
17
+ /**
18
+ * Initialize database with schema
19
+ */
20
+ async initialize() {
21
+ if (this.initialized) return;
22
+
23
+ // Ensure directory exists
24
+ const dbDir = path.dirname(this.dbPath);
25
+ if (!fs.existsSync(dbDir)) {
26
+ fs.mkdirSync(dbDir, { recursive: true });
27
+ }
28
+
29
+ // Use sql.js (optional dependency)
30
+ let initSqlJs;
31
+ try {
32
+ initSqlJs = require('sql.js');
33
+ } catch (e) {
34
+ throw new Error('sql.js is not installed. Install it with: npm install sql.js');
35
+ }
36
+ const SQL = await initSqlJs();
37
+
38
+ // Load existing database or create new
39
+ if (fs.existsSync(this.dbPath)) {
40
+ const buffer = fs.readFileSync(this.dbPath);
41
+ this.db = new SQL.Database(buffer);
42
+ } else {
43
+ this.db = new SQL.Database();
44
+ }
45
+ this.useBetterSqlite = false;
46
+
47
+ this.createSchema();
48
+ this.initialized = true;
49
+ }
50
+
51
+ /**
52
+ * Create database schema
53
+ */
54
+ createSchema() {
55
+ const schema = `
56
+ -- Main models table
57
+ CREATE TABLE IF NOT EXISTS models (
58
+ id TEXT PRIMARY KEY,
59
+ name TEXT NOT NULL,
60
+ family TEXT,
61
+ type TEXT DEFAULT 'official',
62
+ description TEXT,
63
+ capabilities TEXT,
64
+ pulls INTEGER DEFAULT 0,
65
+ tags_count INTEGER DEFAULT 0,
66
+ namespace TEXT,
67
+ url TEXT,
68
+ last_updated TEXT,
69
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
70
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
71
+ );
72
+
73
+ -- Variants table (each quantization/size combination)
74
+ CREATE TABLE IF NOT EXISTS variants (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ model_id TEXT NOT NULL,
77
+ tag TEXT NOT NULL,
78
+ params_b REAL,
79
+ quant TEXT,
80
+ size_gb REAL,
81
+ context_length INTEGER DEFAULT 4096,
82
+ input_types TEXT DEFAULT '["text"]',
83
+ is_moe INTEGER DEFAULT 0,
84
+ expert_count INTEGER,
85
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
86
+ FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE,
87
+ UNIQUE(model_id, tag)
88
+ );
89
+
90
+ -- Benchmarks table (real performance data per hardware)
91
+ CREATE TABLE IF NOT EXISTS benchmarks (
92
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
93
+ variant_id INTEGER NOT NULL,
94
+ hardware_fingerprint TEXT NOT NULL,
95
+ tokens_per_second REAL,
96
+ time_to_first_token REAL,
97
+ memory_used_gb REAL,
98
+ backend TEXT,
99
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
100
+ FOREIGN KEY (variant_id) REFERENCES variants(id) ON DELETE CASCADE
101
+ );
102
+
103
+ -- Sync metadata table
104
+ CREATE TABLE IF NOT EXISTS sync_meta (
105
+ key TEXT PRIMARY KEY,
106
+ value TEXT,
107
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
108
+ );
109
+
110
+ -- Create indexes for fast searches
111
+ CREATE INDEX IF NOT EXISTS idx_models_family ON models(family);
112
+ CREATE INDEX IF NOT EXISTS idx_models_pulls ON models(pulls DESC);
113
+ CREATE INDEX IF NOT EXISTS idx_models_type ON models(type);
114
+ CREATE INDEX IF NOT EXISTS idx_variants_params ON variants(params_b);
115
+ CREATE INDEX IF NOT EXISTS idx_variants_size ON variants(size_gb);
116
+ CREATE INDEX IF NOT EXISTS idx_variants_quant ON variants(quant);
117
+ CREATE INDEX IF NOT EXISTS idx_variants_model ON variants(model_id);
118
+ CREATE INDEX IF NOT EXISTS idx_benchmarks_hardware ON benchmarks(hardware_fingerprint);
119
+ CREATE INDEX IF NOT EXISTS idx_benchmarks_variant ON benchmarks(variant_id);
120
+ `;
121
+
122
+ if (this.useBetterSqlite) {
123
+ this.db.exec(schema);
124
+ } else {
125
+ this.db.run(schema);
126
+ this.saveToFile();
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Save sql.js database to file
132
+ */
133
+ saveToFile() {
134
+ if (!this.useBetterSqlite && this.db) {
135
+ const data = this.db.export();
136
+ const buffer = Buffer.from(data);
137
+ fs.writeFileSync(this.dbPath, buffer);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Execute a query (handles both sqlite implementations)
143
+ */
144
+ run(sql, params = []) {
145
+ if (this.useBetterSqlite) {
146
+ return this.db.prepare(sql).run(...params);
147
+ } else {
148
+ this.db.run(sql, params);
149
+ this.saveToFile();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get all results from a query
155
+ */
156
+ all(sql, params = []) {
157
+ if (this.useBetterSqlite) {
158
+ return this.db.prepare(sql).all(...params);
159
+ } else {
160
+ const stmt = this.db.prepare(sql);
161
+ stmt.bind(params);
162
+ const results = [];
163
+ while (stmt.step()) {
164
+ results.push(stmt.getAsObject());
165
+ }
166
+ stmt.free();
167
+ return results;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get single result from a query
173
+ */
174
+ get(sql, params = []) {
175
+ if (this.useBetterSqlite) {
176
+ return this.db.prepare(sql).get(...params);
177
+ } else {
178
+ const results = this.all(sql, params);
179
+ return results.length > 0 ? results[0] : null;
180
+ }
181
+ }
182
+
183
+ // ==================== MODEL OPERATIONS ====================
184
+
185
+ /**
186
+ * Insert or update a model
187
+ */
188
+ upsertModel(model) {
189
+ const sql = `
190
+ INSERT INTO models (id, name, family, type, description, capabilities, pulls, tags_count, namespace, url, last_updated, updated_at)
191
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
192
+ ON CONFLICT(id) DO UPDATE SET
193
+ name = excluded.name,
194
+ family = excluded.family,
195
+ type = excluded.type,
196
+ description = excluded.description,
197
+ capabilities = excluded.capabilities,
198
+ pulls = excluded.pulls,
199
+ tags_count = excluded.tags_count,
200
+ namespace = excluded.namespace,
201
+ url = excluded.url,
202
+ last_updated = excluded.last_updated,
203
+ updated_at = CURRENT_TIMESTAMP
204
+ `;
205
+
206
+ this.run(sql, [
207
+ model.id,
208
+ model.name,
209
+ model.family || this.inferFamily(model.id),
210
+ model.type || 'official',
211
+ model.description || '',
212
+ JSON.stringify(model.capabilities || []),
213
+ model.pulls || 0,
214
+ model.tags_count || 0,
215
+ model.namespace || '',
216
+ model.url || `https://ollama.com/library/${model.id}`,
217
+ model.last_updated || ''
218
+ ]);
219
+ }
220
+
221
+ /**
222
+ * Infer model family from identifier
223
+ */
224
+ inferFamily(modelId) {
225
+ const id = modelId.toLowerCase();
226
+
227
+ const families = [
228
+ { pattern: /llama3\.2/, family: 'llama3.2' },
229
+ { pattern: /llama3\.1/, family: 'llama3.1' },
230
+ { pattern: /llama3/, family: 'llama3' },
231
+ { pattern: /llama2/, family: 'llama2' },
232
+ { pattern: /qwen2\.5/, family: 'qwen2.5' },
233
+ { pattern: /qwen2/, family: 'qwen2' },
234
+ { pattern: /qwen/, family: 'qwen' },
235
+ { pattern: /mistral/, family: 'mistral' },
236
+ { pattern: /mixtral/, family: 'mixtral' },
237
+ { pattern: /gemma2/, family: 'gemma2' },
238
+ { pattern: /gemma/, family: 'gemma' },
239
+ { pattern: /phi-?3/, family: 'phi3' },
240
+ { pattern: /phi-?4/, family: 'phi4' },
241
+ { pattern: /phi/, family: 'phi' },
242
+ { pattern: /deepseek-?coder/, family: 'deepseek-coder' },
243
+ { pattern: /deepseek-?r1/, family: 'deepseek-r1' },
244
+ { pattern: /deepseek/, family: 'deepseek' },
245
+ { pattern: /codellama/, family: 'codellama' },
246
+ { pattern: /starcoder/, family: 'starcoder' },
247
+ { pattern: /tinyllama/, family: 'tinyllama' },
248
+ { pattern: /llava/, family: 'llava' },
249
+ { pattern: /dolphin/, family: 'dolphin' },
250
+ { pattern: /wizard/, family: 'wizard' },
251
+ { pattern: /neural-chat/, family: 'neural-chat' },
252
+ { pattern: /orca/, family: 'orca' },
253
+ { pattern: /vicuna/, family: 'vicuna' },
254
+ { pattern: /yi/, family: 'yi' },
255
+ { pattern: /solar/, family: 'solar' },
256
+ { pattern: /command-r/, family: 'command-r' },
257
+ { pattern: /nomic-embed/, family: 'nomic-embed' },
258
+ { pattern: /mxbai-embed/, family: 'mxbai-embed' },
259
+ { pattern: /bge/, family: 'bge' },
260
+ ];
261
+
262
+ for (const { pattern, family } of families) {
263
+ if (pattern.test(id)) {
264
+ return family;
265
+ }
266
+ }
267
+
268
+ return 'other';
269
+ }
270
+
271
+ /**
272
+ * Insert or update a variant
273
+ */
274
+ upsertVariant(variant) {
275
+ const sql = `
276
+ INSERT INTO variants (model_id, tag, params_b, quant, size_gb, context_length, input_types, is_moe, expert_count)
277
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
278
+ ON CONFLICT(model_id, tag) DO UPDATE SET
279
+ params_b = excluded.params_b,
280
+ quant = excluded.quant,
281
+ size_gb = excluded.size_gb,
282
+ context_length = excluded.context_length,
283
+ input_types = excluded.input_types,
284
+ is_moe = excluded.is_moe,
285
+ expert_count = excluded.expert_count
286
+ `;
287
+
288
+ this.run(sql, [
289
+ variant.model_id,
290
+ variant.tag,
291
+ variant.params_b || null,
292
+ variant.quant || null,
293
+ variant.size_gb || null,
294
+ variant.context_length || 4096,
295
+ JSON.stringify(variant.input_types || ['text']),
296
+ variant.is_moe ? 1 : 0,
297
+ variant.expert_count || null
298
+ ]);
299
+ }
300
+
301
+ /**
302
+ * Add benchmark result
303
+ */
304
+ addBenchmark(benchmark) {
305
+ const sql = `
306
+ INSERT INTO benchmarks (variant_id, hardware_fingerprint, tokens_per_second, time_to_first_token, memory_used_gb, backend)
307
+ VALUES (?, ?, ?, ?, ?, ?)
308
+ `;
309
+
310
+ this.run(sql, [
311
+ benchmark.variant_id,
312
+ benchmark.hardware_fingerprint,
313
+ benchmark.tokens_per_second,
314
+ benchmark.time_to_first_token,
315
+ benchmark.memory_used_gb,
316
+ benchmark.backend
317
+ ]);
318
+ }
319
+
320
+ // ==================== SEARCH OPERATIONS ====================
321
+
322
+ /**
323
+ * Search models with filters
324
+ */
325
+ searchModels(query = '', filters = {}) {
326
+ let sql = `
327
+ SELECT m.*,
328
+ COUNT(DISTINCT v.id) as variant_count,
329
+ MIN(v.size_gb) as min_size_gb,
330
+ MAX(v.size_gb) as max_size_gb,
331
+ MIN(v.params_b) as min_params_b,
332
+ MAX(v.params_b) as max_params_b
333
+ FROM models m
334
+ LEFT JOIN variants v ON m.id = v.model_id
335
+ WHERE 1=1
336
+ `;
337
+ const params = [];
338
+
339
+ // Text search
340
+ if (query) {
341
+ sql += ` AND (m.id LIKE ? OR m.name LIKE ? OR m.description LIKE ?)`;
342
+ const searchPattern = `%${query}%`;
343
+ params.push(searchPattern, searchPattern, searchPattern);
344
+ }
345
+
346
+ // Family filter
347
+ if (filters.family) {
348
+ sql += ` AND m.family = ?`;
349
+ params.push(filters.family);
350
+ }
351
+
352
+ // Type filter
353
+ if (filters.type) {
354
+ sql += ` AND m.type = ?`;
355
+ params.push(filters.type);
356
+ }
357
+
358
+ // Capability filter
359
+ if (filters.capability) {
360
+ sql += ` AND m.capabilities LIKE ?`;
361
+ params.push(`%${filters.capability}%`);
362
+ }
363
+
364
+ // Min pulls filter
365
+ if (filters.minPulls) {
366
+ sql += ` AND m.pulls >= ?`;
367
+ params.push(filters.minPulls);
368
+ }
369
+
370
+ sql += ` GROUP BY m.id`;
371
+
372
+ // Params range filter (post-group)
373
+ if (filters.minParams || filters.maxParams) {
374
+ sql += ` HAVING 1=1`;
375
+ if (filters.minParams) {
376
+ sql += ` AND max_params_b >= ?`;
377
+ params.push(filters.minParams);
378
+ }
379
+ if (filters.maxParams) {
380
+ sql += ` AND min_params_b <= ?`;
381
+ params.push(filters.maxParams);
382
+ }
383
+ }
384
+
385
+ // Size range filter (post-group)
386
+ if (filters.maxSizeGB) {
387
+ if (!sql.includes('HAVING')) sql += ` HAVING 1=1`;
388
+ sql += ` AND min_size_gb <= ?`;
389
+ params.push(filters.maxSizeGB);
390
+ }
391
+
392
+ // Order by
393
+ const orderBy = filters.orderBy || 'pulls';
394
+ const orderDir = filters.orderDir || 'DESC';
395
+ sql += ` ORDER BY m.${orderBy} ${orderDir}`;
396
+
397
+ // Limit
398
+ if (filters.limit) {
399
+ sql += ` LIMIT ?`;
400
+ params.push(filters.limit);
401
+ }
402
+
403
+ return this.all(sql, params);
404
+ }
405
+
406
+ /**
407
+ * Get variants for a model
408
+ */
409
+ getVariants(modelId, filters = {}) {
410
+ let sql = `
411
+ SELECT v.*, m.name as model_name, m.family, m.pulls
412
+ FROM variants v
413
+ JOIN models m ON v.model_id = m.id
414
+ WHERE v.model_id = ?
415
+ `;
416
+ const params = [modelId];
417
+
418
+ if (filters.quant) {
419
+ sql += ` AND v.quant = ?`;
420
+ params.push(filters.quant);
421
+ }
422
+
423
+ if (filters.maxSizeGB) {
424
+ sql += ` AND v.size_gb <= ?`;
425
+ params.push(filters.maxSizeGB);
426
+ }
427
+
428
+ if (filters.minParams) {
429
+ sql += ` AND v.params_b >= ?`;
430
+ params.push(filters.minParams);
431
+ }
432
+
433
+ if (filters.maxParams) {
434
+ sql += ` AND v.params_b <= ?`;
435
+ params.push(filters.maxParams);
436
+ }
437
+
438
+ sql += ` ORDER BY v.params_b DESC, v.size_gb DESC`;
439
+
440
+ return this.all(sql, params);
441
+ }
442
+
443
+ /**
444
+ * Get all variants matching hardware constraints
445
+ */
446
+ getVariantsForHardware(maxSizeGB, filters = {}) {
447
+ let sql = `
448
+ SELECT v.*, m.name as model_name, m.family, m.pulls, m.capabilities, m.type
449
+ FROM variants v
450
+ JOIN models m ON v.model_id = m.id
451
+ WHERE v.size_gb <= ?
452
+ `;
453
+ const params = [maxSizeGB];
454
+
455
+ if (filters.category) {
456
+ sql += ` AND m.capabilities LIKE ?`;
457
+ params.push(`%${filters.category}%`);
458
+ }
459
+
460
+ if (filters.family) {
461
+ sql += ` AND m.family = ?`;
462
+ params.push(filters.family);
463
+ }
464
+
465
+ if (filters.quant) {
466
+ sql += ` AND v.quant = ?`;
467
+ params.push(filters.quant);
468
+ }
469
+
470
+ if (filters.minContext) {
471
+ sql += ` AND v.context_length >= ?`;
472
+ params.push(filters.minContext);
473
+ }
474
+
475
+ sql += ` ORDER BY m.pulls DESC, v.params_b DESC`;
476
+
477
+ if (filters.limit) {
478
+ sql += ` LIMIT ?`;
479
+ params.push(filters.limit);
480
+ }
481
+
482
+ return this.all(sql, params);
483
+ }
484
+
485
+ /**
486
+ * Search variants by query (searches model names/descriptions)
487
+ */
488
+ searchVariants(query = '', filters = {}) {
489
+ let sql = `
490
+ SELECT v.*, m.name as model_name, m.family, m.pulls, m.capabilities, m.type, m.description
491
+ FROM variants v
492
+ JOIN models m ON v.model_id = m.id
493
+ WHERE 1=1
494
+ `;
495
+ const params = [];
496
+
497
+ // Text search on model id, name, description
498
+ if (query) {
499
+ sql += ` AND (m.id LIKE ? OR m.name LIKE ? OR m.description LIKE ?)`;
500
+ const searchPattern = `%${query}%`;
501
+ params.push(searchPattern, searchPattern, searchPattern);
502
+ }
503
+
504
+ // Size filters
505
+ if (filters.maxSize) {
506
+ sql += ` AND (v.size_gb IS NULL OR v.size_gb <= ?)`;
507
+ params.push(filters.maxSize);
508
+ }
509
+
510
+ if (filters.minSize) {
511
+ sql += ` AND (v.size_gb IS NULL OR v.size_gb >= ?)`;
512
+ params.push(filters.minSize);
513
+ }
514
+
515
+ // Quantization filter
516
+ if (filters.quant) {
517
+ sql += ` AND v.quant = ?`;
518
+ params.push(filters.quant.toUpperCase());
519
+ }
520
+
521
+ // Family filter
522
+ if (filters.family) {
523
+ sql += ` AND m.family = ?`;
524
+ params.push(filters.family.toLowerCase());
525
+ }
526
+
527
+ // Category/capability filter
528
+ if (filters.category) {
529
+ sql += ` AND m.capabilities LIKE ?`;
530
+ params.push(`%${filters.category}%`);
531
+ }
532
+
533
+ sql += ` ORDER BY m.pulls DESC, v.params_b DESC, v.size_gb DESC`;
534
+
535
+ if (filters.limit) {
536
+ sql += ` LIMIT ?`;
537
+ params.push(filters.limit);
538
+ }
539
+
540
+ return this.all(sql, params);
541
+ }
542
+
543
+ /**
544
+ * Get benchmarks for a variant on specific hardware
545
+ */
546
+ getBenchmarks(variantId, hardwareFingerprint = null) {
547
+ let sql = `SELECT * FROM benchmarks WHERE variant_id = ?`;
548
+ const params = [variantId];
549
+
550
+ if (hardwareFingerprint) {
551
+ sql += ` AND hardware_fingerprint = ?`;
552
+ params.push(hardwareFingerprint);
553
+ }
554
+
555
+ sql += ` ORDER BY created_at DESC`;
556
+
557
+ return this.all(sql, params);
558
+ }
559
+
560
+ // ==================== SYNC OPERATIONS ====================
561
+
562
+ /**
563
+ * Get last sync timestamp
564
+ */
565
+ getLastSync() {
566
+ const result = this.get(`SELECT value FROM sync_meta WHERE key = 'last_sync'`);
567
+ return result ? result.value : null;
568
+ }
569
+
570
+ /**
571
+ * Set last sync timestamp
572
+ */
573
+ setLastSync(timestamp) {
574
+ this.run(`
575
+ INSERT INTO sync_meta (key, value, updated_at)
576
+ VALUES ('last_sync', ?, CURRENT_TIMESTAMP)
577
+ ON CONFLICT(key) DO UPDATE SET
578
+ value = excluded.value,
579
+ updated_at = CURRENT_TIMESTAMP
580
+ `, [timestamp]);
581
+ }
582
+
583
+ /**
584
+ * Get total model count
585
+ */
586
+ getModelCount() {
587
+ const result = this.get(`SELECT COUNT(*) as count FROM models`);
588
+ return result ? result.count : 0;
589
+ }
590
+
591
+ /**
592
+ * Get total variant count
593
+ */
594
+ getVariantCount() {
595
+ const result = this.get(`SELECT COUNT(*) as count FROM variants`);
596
+ return result ? result.count : 0;
597
+ }
598
+
599
+ /**
600
+ * Get database stats
601
+ */
602
+ getStats() {
603
+ return {
604
+ models: this.getModelCount(),
605
+ variants: this.getVariantCount(),
606
+ lastSync: this.getLastSync(),
607
+ families: this.all(`SELECT family, COUNT(*) as count FROM models GROUP BY family ORDER BY count DESC`),
608
+ topModels: this.all(`SELECT id, name, pulls FROM models ORDER BY pulls DESC LIMIT 10`)
609
+ };
610
+ }
611
+
612
+ /**
613
+ * Clear all data
614
+ */
615
+ clear() {
616
+ this.run(`DELETE FROM benchmarks`);
617
+ this.run(`DELETE FROM variants`);
618
+ this.run(`DELETE FROM models`);
619
+ this.run(`DELETE FROM sync_meta`);
620
+ }
621
+
622
+ /**
623
+ * Close database connection
624
+ */
625
+ close() {
626
+ if (this.db) {
627
+ if (this.useBetterSqlite) {
628
+ this.db.close();
629
+ } else {
630
+ this.saveToFile();
631
+ this.db.close();
632
+ }
633
+ }
634
+ }
635
+ }
636
+
637
+ module.exports = ModelDatabase;