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.
- package/README.md +28 -8
- package/analyzer/compatibility.js +5 -0
- package/analyzer/performance.js +5 -4
- package/bin/cli.js +5 -39
- package/bin/enhanced_cli.js +449 -24
- package/bin/mcp-server.mjs +266 -101
- package/package.json +13 -8
- package/src/ai/multi-objective-selector.js +118 -11
- package/src/calibration/calibration-manager.js +4 -1
- package/src/data/model-database.js +489 -5
- package/src/data/registry-ingestors.js +751 -0
- package/src/data/registry-recommender.js +514 -0
- package/src/data/seed/README.md +11 -3
- package/src/data/seed/models.db +0 -0
- package/src/data/sync-manager.js +32 -18
- package/src/hardware/backends/apple-silicon.js +5 -1
- package/src/hardware/backends/cuda-detector.js +47 -19
- package/src/hardware/backends/intel-detector.js +6 -2
- package/src/hardware/backends/rocm-detector.js +6 -2
- package/src/hardware/detector.js +57 -30
- package/src/hardware/unified-detector.js +129 -25
- package/src/index.js +68 -4
- package/src/models/ai-check-selector.js +36 -5
- package/src/models/deterministic-selector.js +179 -18
- package/src/models/expanded_database.js +9 -5
- package/src/models/intelligent-selector.js +87 -1
- package/src/models/moe-assumptions.js +11 -0
- package/src/models/requirements.js +16 -11
- package/src/models/scoring-core.js +341 -0
- package/src/models/scoring-engine.js +9 -2
- package/src/ollama/capacity-planner.js +15 -2
- package/src/ollama/client.js +70 -30
- package/src/ollama/enhanced-client.js +20 -2
- package/src/ollama/manager.js +14 -2
- package/src/policy/cli-policy.js +8 -2
- package/src/policy/policy-engine.js +2 -1
- package/src/provenance/model-provenance.js +4 -1
- package/src/ui/cli-theme.js +47 -7
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
411
|
-
|
|
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
|
*/
|