llm-checker 3.5.15 → 3.7.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 (39) hide show
  1. package/README.md +28 -8
  2. package/analyzer/compatibility.js +5 -0
  3. package/analyzer/performance.js +5 -4
  4. package/bin/cli.js +5 -39
  5. package/bin/enhanced_cli.js +449 -24
  6. package/bin/mcp-server.mjs +266 -101
  7. package/package.json +13 -8
  8. package/src/ai/multi-objective-selector.js +118 -11
  9. package/src/calibration/calibration-manager.js +4 -1
  10. package/src/data/model-database.js +489 -5
  11. package/src/data/registry-ingestors.js +751 -0
  12. package/src/data/registry-recommender.js +514 -0
  13. package/src/data/seed/README.md +11 -3
  14. package/src/data/seed/models.db +0 -0
  15. package/src/data/sync-manager.js +32 -18
  16. package/src/hardware/backends/apple-silicon.js +5 -1
  17. package/src/hardware/backends/cuda-detector.js +47 -19
  18. package/src/hardware/backends/intel-detector.js +6 -2
  19. package/src/hardware/backends/rocm-detector.js +6 -2
  20. package/src/hardware/detector.js +57 -30
  21. package/src/hardware/unified-detector.js +129 -25
  22. package/src/index.js +68 -4
  23. package/src/models/ai-check-selector.js +36 -5
  24. package/src/models/deterministic-selector.js +179 -18
  25. package/src/models/expanded_database.js +9 -5
  26. package/src/models/intelligent-selector.js +87 -1
  27. package/src/models/moe-assumptions.js +11 -0
  28. package/src/models/requirements.js +16 -11
  29. package/src/models/scoring-core.js +341 -0
  30. package/src/models/scoring-engine.js +9 -2
  31. package/src/ollama/capacity-planner.js +15 -2
  32. package/src/ollama/client.js +70 -30
  33. package/src/ollama/enhanced-client.js +20 -2
  34. package/src/ollama/manager.js +14 -2
  35. package/src/policy/cli-policy.js +8 -2
  36. package/src/policy/policy-engine.js +2 -1
  37. package/src/provenance/model-provenance.js +4 -1
  38. package/src/ui/cli-theme.js +47 -7
  39. package/src/ui/interactive-panel.js +162 -24
@@ -13,6 +13,12 @@ class ModelDatabase {
13
13
  this.seedDbPath = options.seedDbPath || path.join(__dirname, 'seed', 'models.db');
14
14
  this.db = null;
15
15
  this.initialized = false;
16
+ this.disableRegistrySeedImport = Boolean(options.disableRegistrySeedImport);
17
+ // Batched-write state: during a bulk sync we defer the (expensive) full
18
+ // sql.js export-and-write until the batch ends, instead of rewriting the
19
+ // whole DB file on every single row.
20
+ this._batchDepth = 0;
21
+ this._pendingSave = false;
16
22
  }
17
23
 
18
24
  /**
@@ -60,6 +66,9 @@ class ModelDatabase {
60
66
 
61
67
  this.createSchema();
62
68
  this.initialized = true;
69
+ if (!this.disableRegistrySeedImport) {
70
+ await this.seedRegistryFromPackagedSnapshotIfNeeded();
71
+ }
63
72
  }
64
73
 
65
74
  /**
@@ -114,6 +123,86 @@ class ModelDatabase {
114
123
  FOREIGN KEY (variant_id) REFERENCES variants(id) ON DELETE CASCADE
115
124
  );
116
125
 
126
+ -- Registry sources for multi-hub model discovery (Hugging Face,
127
+ -- Ollama, GPT4All, ModelScope, etc.).
128
+ CREATE TABLE IF NOT EXISTS registry_sources (
129
+ id TEXT PRIMARY KEY,
130
+ name TEXT NOT NULL,
131
+ base_url TEXT,
132
+ source_type TEXT,
133
+ last_ingested_at TEXT,
134
+ metadata TEXT DEFAULT '{}',
135
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
136
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
137
+ );
138
+
139
+ -- Source repositories or registry entries. A single repo can expose
140
+ -- many downloadable artifacts/quantizations.
141
+ CREATE TABLE IF NOT EXISTS registry_repos (
142
+ id TEXT PRIMARY KEY,
143
+ source_id TEXT NOT NULL,
144
+ repo_id TEXT NOT NULL,
145
+ namespace TEXT,
146
+ canonical_model_id TEXT NOT NULL,
147
+ display_name TEXT,
148
+ url TEXT,
149
+ license TEXT,
150
+ gated INTEGER DEFAULT 0,
151
+ requires_auth INTEGER DEFAULT 0,
152
+ downloads INTEGER DEFAULT 0,
153
+ likes INTEGER DEFAULT 0,
154
+ tags TEXT DEFAULT '[]',
155
+ tasks TEXT DEFAULT '[]',
156
+ modalities TEXT DEFAULT '["text"]',
157
+ last_modified TEXT,
158
+ sha TEXT,
159
+ metadata TEXT DEFAULT '{}',
160
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
161
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
162
+ FOREIGN KEY (source_id) REFERENCES registry_sources(id) ON DELETE CASCADE,
163
+ UNIQUE(source_id, repo_id)
164
+ );
165
+
166
+ -- Concrete downloadable/installable files or tags used by the
167
+ -- recommender. This is the table that lets llm-checker reason about
168
+ -- exact GGUF/safetensors/MLX/Ollama variants instead of only families.
169
+ CREATE TABLE IF NOT EXISTS model_artifacts (
170
+ id TEXT PRIMARY KEY,
171
+ source_id TEXT NOT NULL,
172
+ repo_key TEXT NOT NULL,
173
+ repo_id TEXT NOT NULL,
174
+ canonical_model_id TEXT NOT NULL,
175
+ artifact_name TEXT NOT NULL,
176
+ filename TEXT,
177
+ format TEXT,
178
+ quantization TEXT,
179
+ precision TEXT,
180
+ parameter_count_b REAL,
181
+ active_parameter_count_b REAL,
182
+ size_bytes INTEGER,
183
+ size_gb REAL,
184
+ context_length INTEGER,
185
+ runtime_support TEXT DEFAULT '[]',
186
+ tasks TEXT DEFAULT '[]',
187
+ modalities TEXT DEFAULT '["text"]',
188
+ download_url TEXT,
189
+ install_command TEXT,
190
+ sha256 TEXT,
191
+ etag TEXT,
192
+ license TEXT,
193
+ gated INTEGER DEFAULT 0,
194
+ requires_auth INTEGER DEFAULT 0,
195
+ downloads INTEGER DEFAULT 0,
196
+ likes INTEGER DEFAULT 0,
197
+ updated_at TEXT,
198
+ metadata TEXT DEFAULT '{}',
199
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
200
+ refreshed_at TEXT DEFAULT CURRENT_TIMESTAMP,
201
+ FOREIGN KEY (source_id) REFERENCES registry_sources(id) ON DELETE CASCADE,
202
+ FOREIGN KEY (repo_key) REFERENCES registry_repos(id) ON DELETE CASCADE,
203
+ UNIQUE(source_id, repo_key, artifact_name)
204
+ );
205
+
117
206
  -- Sync metadata table
118
207
  CREATE TABLE IF NOT EXISTS sync_meta (
119
208
  key TEXT PRIMARY KEY,
@@ -131,6 +220,16 @@ class ModelDatabase {
131
220
  CREATE INDEX IF NOT EXISTS idx_variants_model ON variants(model_id);
132
221
  CREATE INDEX IF NOT EXISTS idx_benchmarks_hardware ON benchmarks(hardware_fingerprint);
133
222
  CREATE INDEX IF NOT EXISTS idx_benchmarks_variant ON benchmarks(variant_id);
223
+ CREATE INDEX IF NOT EXISTS idx_registry_repos_source ON registry_repos(source_id);
224
+ CREATE INDEX IF NOT EXISTS idx_registry_repos_model ON registry_repos(canonical_model_id);
225
+ CREATE INDEX IF NOT EXISTS idx_registry_repos_downloads ON registry_repos(downloads DESC);
226
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_model ON model_artifacts(canonical_model_id);
227
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_source ON model_artifacts(source_id);
228
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_format ON model_artifacts(format);
229
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_quant ON model_artifacts(quantization);
230
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_runtime ON model_artifacts(runtime_support);
231
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_size ON model_artifacts(size_gb);
232
+ CREATE INDEX IF NOT EXISTS idx_model_artifacts_downloads ON model_artifacts(downloads DESC);
134
233
  `;
135
234
 
136
235
  if (this.useBetterSqlite) {
@@ -148,7 +247,29 @@ class ModelDatabase {
148
247
  if (!this.useBetterSqlite && this.db) {
149
248
  const data = this.db.export();
150
249
  const buffer = Buffer.from(data);
151
- fs.writeFileSync(this.dbPath, buffer);
250
+ // Write to a temp file then atomically rename, so a crash/SIGINT
251
+ // mid-write can't leave a truncated, unreadable models.db behind.
252
+ const tmpPath = `${this.dbPath}.tmp`;
253
+ fs.writeFileSync(tmpPath, buffer);
254
+ fs.renameSync(tmpPath, this.dbPath);
255
+ this._pendingSave = false;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Group many writes so the database file is exported/written once at the end
261
+ * instead of on every row. Nestable; the outermost endBatch() flushes.
262
+ */
263
+ beginBatch() {
264
+ this._batchDepth += 1;
265
+ }
266
+
267
+ endBatch() {
268
+ if (this._batchDepth > 0) {
269
+ this._batchDepth -= 1;
270
+ }
271
+ if (this._batchDepth === 0 && this._pendingSave) {
272
+ this.saveToFile();
152
273
  }
153
274
  }
154
275
 
@@ -160,7 +281,11 @@ class ModelDatabase {
160
281
  return this.db.prepare(sql).run(...params);
161
282
  } else {
162
283
  this.db.run(sql, params);
163
- this.saveToFile();
284
+ if (this._batchDepth > 0) {
285
+ this._pendingSave = true; // defer the full export until endBatch()
286
+ } else {
287
+ this.saveToFile();
288
+ }
164
289
  }
165
290
  }
166
291
 
@@ -334,6 +459,343 @@ class ModelDatabase {
334
459
  ]);
335
460
  }
336
461
 
462
+ // ==================== MODEL REGISTRY OPERATIONS ====================
463
+
464
+ stringifyJson(value, fallback) {
465
+ const safeValue = value === undefined ? fallback : value;
466
+ try {
467
+ return JSON.stringify(safeValue);
468
+ } catch {
469
+ return JSON.stringify(fallback);
470
+ }
471
+ }
472
+
473
+ parseJson(value, fallback) {
474
+ if (!value) return fallback;
475
+ try {
476
+ const parsed = JSON.parse(value);
477
+ return parsed;
478
+ } catch {
479
+ return fallback;
480
+ }
481
+ }
482
+
483
+ upsertRegistrySource(source) {
484
+ const sql = `
485
+ INSERT INTO registry_sources (id, name, base_url, source_type, last_ingested_at, metadata, updated_at)
486
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
487
+ ON CONFLICT(id) DO UPDATE SET
488
+ name = excluded.name,
489
+ base_url = excluded.base_url,
490
+ source_type = excluded.source_type,
491
+ last_ingested_at = excluded.last_ingested_at,
492
+ metadata = excluded.metadata,
493
+ updated_at = CURRENT_TIMESTAMP
494
+ `;
495
+
496
+ this.run(sql, [
497
+ source.id,
498
+ source.name || source.id,
499
+ source.base_url || '',
500
+ source.source_type || 'registry',
501
+ source.last_ingested_at || new Date().toISOString(),
502
+ this.stringifyJson(source.metadata || {}, {})
503
+ ]);
504
+ }
505
+
506
+ upsertRegistryRepo(repo) {
507
+ const sql = `
508
+ INSERT INTO registry_repos (
509
+ id, source_id, repo_id, namespace, canonical_model_id, display_name, url, license,
510
+ gated, requires_auth, downloads, likes, tags, tasks, modalities, last_modified,
511
+ sha, metadata, updated_at
512
+ )
513
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
514
+ ON CONFLICT(source_id, repo_id) DO UPDATE SET
515
+ namespace = excluded.namespace,
516
+ canonical_model_id = excluded.canonical_model_id,
517
+ display_name = excluded.display_name,
518
+ url = excluded.url,
519
+ license = excluded.license,
520
+ gated = excluded.gated,
521
+ requires_auth = excluded.requires_auth,
522
+ downloads = excluded.downloads,
523
+ likes = excluded.likes,
524
+ tags = excluded.tags,
525
+ tasks = excluded.tasks,
526
+ modalities = excluded.modalities,
527
+ last_modified = excluded.last_modified,
528
+ sha = excluded.sha,
529
+ metadata = excluded.metadata,
530
+ updated_at = CURRENT_TIMESTAMP
531
+ `;
532
+
533
+ const repoId = repo.repo_id || repo.id;
534
+ this.run(sql, [
535
+ repo.id,
536
+ repo.source_id,
537
+ repoId,
538
+ repo.namespace || '',
539
+ repo.canonical_model_id || repoId,
540
+ repo.display_name || repo.name || repoId,
541
+ repo.url || '',
542
+ repo.license || 'unknown',
543
+ repo.gated ? 1 : 0,
544
+ repo.requires_auth ? 1 : 0,
545
+ repo.downloads || 0,
546
+ repo.likes || 0,
547
+ this.stringifyJson(repo.tags || [], []),
548
+ this.stringifyJson(repo.tasks || [], []),
549
+ this.stringifyJson(repo.modalities || ['text'], ['text']),
550
+ repo.last_modified || '',
551
+ repo.sha || '',
552
+ this.stringifyJson(repo.metadata || {}, {})
553
+ ]);
554
+ }
555
+
556
+ upsertModelArtifact(artifact) {
557
+ const sql = `
558
+ INSERT INTO model_artifacts (
559
+ id, source_id, repo_key, repo_id, canonical_model_id, artifact_name, filename,
560
+ format, quantization, precision, parameter_count_b, active_parameter_count_b,
561
+ size_bytes, size_gb, context_length, runtime_support, tasks, modalities,
562
+ download_url, install_command, sha256, etag, license, gated, requires_auth,
563
+ downloads, likes, updated_at, metadata, refreshed_at
564
+ )
565
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
566
+ ON CONFLICT(source_id, repo_key, artifact_name) DO UPDATE SET
567
+ filename = excluded.filename,
568
+ format = excluded.format,
569
+ quantization = excluded.quantization,
570
+ precision = excluded.precision,
571
+ parameter_count_b = excluded.parameter_count_b,
572
+ active_parameter_count_b = excluded.active_parameter_count_b,
573
+ size_bytes = excluded.size_bytes,
574
+ size_gb = excluded.size_gb,
575
+ context_length = excluded.context_length,
576
+ runtime_support = excluded.runtime_support,
577
+ tasks = excluded.tasks,
578
+ modalities = excluded.modalities,
579
+ download_url = excluded.download_url,
580
+ install_command = excluded.install_command,
581
+ sha256 = excluded.sha256,
582
+ etag = excluded.etag,
583
+ license = excluded.license,
584
+ gated = excluded.gated,
585
+ requires_auth = excluded.requires_auth,
586
+ downloads = excluded.downloads,
587
+ likes = excluded.likes,
588
+ updated_at = excluded.updated_at,
589
+ metadata = excluded.metadata,
590
+ refreshed_at = CURRENT_TIMESTAMP
591
+ `;
592
+
593
+ const sizeBytes = Number(artifact.size_bytes);
594
+ const sizeGB = Number(artifact.size_gb);
595
+ const repoId = artifact.repo_id || artifact.repo_key;
596
+
597
+ this.run(sql, [
598
+ artifact.id,
599
+ artifact.source_id,
600
+ artifact.repo_key,
601
+ repoId,
602
+ artifact.canonical_model_id || repoId,
603
+ artifact.artifact_name || artifact.filename || repoId,
604
+ artifact.filename || '',
605
+ artifact.format || 'unknown',
606
+ artifact.quantization || '',
607
+ artifact.precision || '',
608
+ artifact.parameter_count_b || null,
609
+ artifact.active_parameter_count_b || null,
610
+ Number.isFinite(sizeBytes) && sizeBytes > 0 ? sizeBytes : null,
611
+ Number.isFinite(sizeGB) && sizeGB > 0 ? sizeGB : null,
612
+ artifact.context_length || null,
613
+ this.stringifyJson(artifact.runtime_support || [], []),
614
+ this.stringifyJson(artifact.tasks || [], []),
615
+ this.stringifyJson(artifact.modalities || ['text'], ['text']),
616
+ artifact.download_url || '',
617
+ artifact.install_command || '',
618
+ artifact.sha256 || '',
619
+ artifact.etag || '',
620
+ artifact.license || 'unknown',
621
+ artifact.gated ? 1 : 0,
622
+ artifact.requires_auth ? 1 : 0,
623
+ artifact.downloads || 0,
624
+ artifact.likes || 0,
625
+ artifact.updated_at || '',
626
+ this.stringifyJson(artifact.metadata || {}, {})
627
+ ]);
628
+ }
629
+
630
+ searchModelArtifacts(query = '', filters = {}) {
631
+ let sql = `
632
+ SELECT
633
+ a.*,
634
+ s.name as source_name,
635
+ s.base_url as source_base_url,
636
+ r.display_name as repo_display_name,
637
+ r.url as repo_url
638
+ FROM model_artifacts a
639
+ JOIN registry_sources s ON s.id = a.source_id
640
+ JOIN registry_repos r ON r.id = a.repo_key
641
+ WHERE 1=1
642
+ `;
643
+ const params = [];
644
+
645
+ if (query) {
646
+ sql += ` AND (
647
+ a.canonical_model_id LIKE ? OR
648
+ a.artifact_name LIKE ? OR
649
+ a.filename LIKE ? OR
650
+ a.repo_id LIKE ? OR
651
+ r.display_name LIKE ?
652
+ )`;
653
+ const pattern = `%${query}%`;
654
+ params.push(pattern, pattern, pattern, pattern, pattern);
655
+ }
656
+
657
+ if (filters.source) {
658
+ sql += ` AND a.source_id = ?`;
659
+ params.push(filters.source);
660
+ }
661
+
662
+ if (filters.format) {
663
+ sql += ` AND a.format = ?`;
664
+ params.push(String(filters.format).toLowerCase());
665
+ }
666
+
667
+ if (filters.quantization) {
668
+ sql += ` AND UPPER(a.quantization) = ?`;
669
+ params.push(String(filters.quantization).toUpperCase());
670
+ }
671
+
672
+ if (filters.runtime && !['auto', 'all', '*'].includes(String(filters.runtime).toLowerCase())) {
673
+ // Escape LIKE wildcards so a runtime value (e.g. "lla_a") can't act as
674
+ // a pattern and over-match (it would otherwise match "llama").
675
+ const runtimeNeedle = String(filters.runtime).replace(/[\\%_]/g, '\\$&');
676
+ sql += ` AND a.runtime_support LIKE ? ESCAPE '\\'`;
677
+ params.push(`%"${runtimeNeedle}"%`);
678
+ }
679
+
680
+ if (filters.maxSizeGB) {
681
+ sql += ` AND (a.size_gb IS NULL OR a.size_gb <= ?)`;
682
+ params.push(filters.maxSizeGB);
683
+ }
684
+
685
+ if (filters.minParamsB) {
686
+ sql += ` AND (a.parameter_count_b IS NULL OR a.parameter_count_b >= ?)`;
687
+ params.push(filters.minParamsB);
688
+ }
689
+
690
+ if (filters.maxParamsB) {
691
+ sql += ` AND (a.parameter_count_b IS NULL OR a.parameter_count_b <= ?)`;
692
+ params.push(filters.maxParamsB);
693
+ }
694
+
695
+ if (filters.localOnly) {
696
+ sql += ` AND a.requires_auth = 0 AND a.gated = 0`;
697
+ }
698
+
699
+ sql += ` ORDER BY a.downloads DESC, a.likes DESC, a.size_gb ASC`;
700
+
701
+ if (filters.limit) {
702
+ sql += ` LIMIT ?`;
703
+ params.push(filters.limit);
704
+ }
705
+
706
+ return this.all(sql, params).map((row) => ({
707
+ ...row,
708
+ runtime_support: this.parseJson(row.runtime_support, []),
709
+ tasks: this.parseJson(row.tasks, []),
710
+ modalities: this.parseJson(row.modalities, ['text']),
711
+ metadata: this.parseJson(row.metadata, {})
712
+ }));
713
+ }
714
+
715
+ getRegistryStats() {
716
+ return {
717
+ sources: this.get(`SELECT COUNT(*) as count FROM registry_sources`)?.count || 0,
718
+ repos: this.get(`SELECT COUNT(*) as count FROM registry_repos`)?.count || 0,
719
+ artifacts: this.get(`SELECT COUNT(*) as count FROM model_artifacts`)?.count || 0,
720
+ bySource: this.all(`
721
+ SELECT source_id, COUNT(*) as artifact_count
722
+ FROM model_artifacts
723
+ GROUP BY source_id
724
+ ORDER BY artifact_count DESC
725
+ `),
726
+ byFormat: this.all(`
727
+ SELECT format, COUNT(*) as artifact_count
728
+ FROM model_artifacts
729
+ GROUP BY format
730
+ ORDER BY artifact_count DESC
731
+ `)
732
+ };
733
+ }
734
+
735
+ async seedRegistryFromPackagedSnapshotIfNeeded() {
736
+ if (!this.seedDbPath || !fs.existsSync(this.seedDbPath)) return false;
737
+ if (path.resolve(this.dbPath) === path.resolve(this.seedDbPath)) return false;
738
+
739
+ const currentArtifacts = this.get(`SELECT COUNT(*) as count FROM model_artifacts`)?.count || 0;
740
+ if (currentArtifacts > 0) return false;
741
+
742
+ const seed = new ModelDatabase({
743
+ dbPath: this.seedDbPath,
744
+ seedDbPath: this.seedDbPath,
745
+ disableRegistrySeedImport: true
746
+ });
747
+
748
+ await seed.initialize();
749
+ try {
750
+ const seedArtifacts = seed.get(`SELECT COUNT(*) as count FROM model_artifacts`)?.count || 0;
751
+ if (seedArtifacts === 0) return false;
752
+
753
+ const sources = seed.all(`SELECT * FROM registry_sources`);
754
+ const repos = seed.all(`SELECT * FROM registry_repos`);
755
+ const artifacts = seed.all(`SELECT * FROM model_artifacts`);
756
+
757
+ this.beginBatch();
758
+ try {
759
+ for (const source of sources) {
760
+ this.upsertRegistrySource({
761
+ ...source,
762
+ metadata: this.parseJson(source.metadata, {})
763
+ });
764
+ }
765
+
766
+ for (const repo of repos) {
767
+ this.upsertRegistryRepo({
768
+ ...repo,
769
+ gated: Boolean(repo.gated),
770
+ requires_auth: Boolean(repo.requires_auth),
771
+ tags: this.parseJson(repo.tags, []),
772
+ tasks: this.parseJson(repo.tasks, []),
773
+ modalities: this.parseJson(repo.modalities, ['text']),
774
+ metadata: this.parseJson(repo.metadata, {})
775
+ });
776
+ }
777
+
778
+ for (const artifact of artifacts) {
779
+ this.upsertModelArtifact({
780
+ ...artifact,
781
+ gated: Boolean(artifact.gated),
782
+ requires_auth: Boolean(artifact.requires_auth),
783
+ runtime_support: this.parseJson(artifact.runtime_support, []),
784
+ tasks: this.parseJson(artifact.tasks, []),
785
+ modalities: this.parseJson(artifact.modalities, ['text']),
786
+ metadata: this.parseJson(artifact.metadata, {})
787
+ });
788
+ }
789
+ } finally {
790
+ this.endBatch();
791
+ }
792
+
793
+ return true;
794
+ } finally {
795
+ seed.close();
796
+ }
797
+ }
798
+
337
799
  // ==================== SEARCH OPERATIONS ====================
338
800
 
339
801
  /**
@@ -406,9 +868,12 @@ class ModelDatabase {
406
868
  params.push(filters.maxSizeGB);
407
869
  }
408
870
 
409
- // Order by
410
- const orderBy = filters.orderBy || 'pulls';
411
- const orderDir = filters.orderDir || 'DESC';
871
+ // Order by — column names and direction can't be parameterized, so whitelist
872
+ // them. A future caller forwarding a user-supplied sort field would otherwise
873
+ // be a SQL-injection / crash vector on this public filters API.
874
+ const ORDERABLE_COLUMNS = new Set(['pulls', 'name', 'tags_count', 'updated_at', 'created_at']);
875
+ const orderBy = ORDERABLE_COLUMNS.has(filters.orderBy) ? filters.orderBy : 'pulls';
876
+ const orderDir = String(filters.orderDir).toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
412
877
  sql += ` ORDER BY m.${orderBy} ${orderDir}`;
413
878
 
414
879
  // Limit
@@ -704,12 +1169,31 @@ class ModelDatabase {
704
1169
  * Clear all data
705
1170
  */
706
1171
  clear() {
1172
+ // The registry's Ollama source is derived from the local Ollama catalog.
1173
+ // Clear only that source so a classic Ollama sync does not erase HF/GPT4All data.
1174
+ this.clearRegistrySource('ollama');
707
1175
  this.run(`DELETE FROM benchmarks`);
708
1176
  this.run(`DELETE FROM variants`);
709
1177
  this.run(`DELETE FROM models`);
710
1178
  this.run(`DELETE FROM sync_meta`);
711
1179
  }
712
1180
 
1181
+ /**
1182
+ * Clear registry data. When sourceId is provided, only that source is removed.
1183
+ */
1184
+ clearRegistrySource(sourceId = null) {
1185
+ if (sourceId) {
1186
+ this.run(`DELETE FROM model_artifacts WHERE source_id = ?`, [sourceId]);
1187
+ this.run(`DELETE FROM registry_repos WHERE source_id = ?`, [sourceId]);
1188
+ this.run(`DELETE FROM registry_sources WHERE id = ?`, [sourceId]);
1189
+ return;
1190
+ }
1191
+
1192
+ this.run(`DELETE FROM model_artifacts`);
1193
+ this.run(`DELETE FROM registry_repos`);
1194
+ this.run(`DELETE FROM registry_sources`);
1195
+ }
1196
+
713
1197
  /**
714
1198
  * Close database connection
715
1199
  */