fix: enforce artifact and checkpoint contracts in model rankings

This commit is contained in:
signerless 2026-09-06 15:28:57 +02:00
parent 9d4d31366b
commit d0c5a414d0
42 changed files with 7343 additions and 233 deletions

3
.gitignore vendored
View file

@ -81,3 +81,6 @@ debug.log
# Transient feature-probe artifacts (never commit)
.tmp-feature-check/
.tmp-*/
!desktop/test/
!desktop/test/**/*.test.js

56
desktop/README.md Normal file
View file

@ -0,0 +1,56 @@
# LLM Checker desktop
Electron interface for the model ranking core in this repository. Keep the core
and desktop on the same checkout; the desktop requires ranking contract version 1.
From the repository root:
```sh
npm ci
npm ci --prefix desktop
npm start --prefix desktop
npm test
npm run pack --prefix desktop
```
The main process runs hardware analysis and runtime probes. The renderer receives
structured results through an isolated preload bridge. It can copy installation
commands, inspect local runtimes, refresh the catalog and update benchmarks.
## Ranking and compatibility
Cards retain the exact artifact, precision and runtime selected by the core.
Memory includes weights, estimated KV cache at the effective context, and runtime
overhead. The budget comes from the core, including shared/unified memory and
CPU-only mode. Missing memory data is marked unknown. A preferred context that
exceeds the known window is displayed as limited; an explicitly required context
is enforced by the selector.
Quality chips distinguish checkpoint measurements from parameter-based estimates.
Benchmark matching preserves base/Instruct, revision, context-extension and size
variants. Quantized copies need a verified quantization alias to borrow an
upstream checkpoint's measurements. Family coverage is reported separately from
exact checkpoint coverage.
The benchmark action uses all sources registered in `src/data/quality-evals.js`:
HF Open LLM, LMArena, BigCodeBench, EvalPlus, LiveBench and MMMU. Individual source
failures are shown and leave cached rows intact. Updates and scans are serialized;
the ranking is recalculated after updates.
Speed and memory values are estimates unless explicitly backed by a measurement.
Runtime installation and hardware compatibility do not establish measured speed.
## Packaging
`electron-builder.yml` includes this checkout's core source, CLI entry points and
dependencies under `resources/core`. The desktop interface lives in `app.asar`.
SQLite seed data remains outside the archive. The packaged CLI runs Electron in
Node mode for catalog refreshes.
Local development and packaged builds use the same ranking contract. Linux,
macOS and Windows packaging targets are configured; validation performed for a
change is recorded in its PR. Tests for the bridge and runtime registry run from
the root suite without installing Electron.
The bundled Inter font uses the SIL Open Font License; see
`src/renderer/assets/Inter-LICENSE.txt`.

View file

@ -0,0 +1,85 @@
appId: com.llmchecker.desktop
productName: LLM Checker
copyright: Copyright © Pavelevich
directories:
output: dist
buildResources: build
# Only the Electron layer lives in the asar. The analysis core is shipped as an
# unpacked resource because it carries src/data/seed/models.db (42 MB) — sql.js
# and node:sqlite both want a real file on disk, and an asar-packed DB would
# have to be copied out at first run.
files:
- src/**/*
- package.json
- "!**/*.map"
- "!test/**"
extraResources:
- from: ../bin
to: core/bin
- from: ../analyzer
to: core/analyzer
- from: ../LICENSE
to: core/LICENSE
- from: ../THIRD_PARTY_NOTICES
to: core/THIRD_PARTY_NOTICES
- from: ../src
to: core/src
filter:
- "**/*"
- "!**/*.md"
- from: ../node_modules
to: core/node_modules
filter:
- "**/*"
- "!**/test/**"
- "!**/tests/**"
- "!**/*.md"
- "!**/*.map"
- "!**/.bin/**"
- from: ../package.json
to: core/package.json
asar: true
# Nothing in the tree is a native module (systeminformation is pure JS, sql.js
# is WASM), so there is no electron-rebuild step and no per-platform native ABI
# to match. That is the one genuinely easy part of packaging this app.
mac:
category: public.app-category.developer-tools
target:
- target: dmg
arch: [arm64, x64]
- target: zip
arch: [arm64, x64]
hardenedRuntime: true
gatekeeperAssess: false
win:
target:
- target: nsis
arch: [x64, arm64]
artifactName: ${productName}-${version}-${arch}.${ext}
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
linux:
category: Development
target:
- target: AppImage
arch: [x64, arm64]
- target: deb
arch: [x64]
maintainer: Pavelevich
desktop:
entry:
Name: LLM Checker
Comment: Find which local models your machine can actually run
Categories: Development;Utility;
publish: null

3601
desktop/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
desktop/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "llm-checker-desktop",
"version": "3.8.1",
"private": true,
"description": "Desktop app for LLM Checker \u2014 hardware-aware local model selection across Ollama, llama.cpp, LM Studio, vLLM, MLX and Transformers",
"main": "src/main/main.js",
"author": "Pavelevich (https://github.com/Pavelevich)",
"license": "SEE LICENSE IN ../LICENSE",
"scripts": {
"start": "electron .",
"dev": "electron . --enable-logging",
"test:runtimes": "node test/runtimes.test.js",
"pack": "electron-builder --dir",
"dist": "electron-builder",
"dist:linux": "electron-builder --linux",
"dist:mac": "electron-builder --mac",
"dist:win": "electron-builder --win",
"test": "node test/runtimes.test.js && node ../tests/desktop-core.test.js"
},
"devDependencies": {
"electron": "^44.0.0",
"electron-builder": "^26.15.3"
},
"engines": {
"node": ">=20"
}
}

517
desktop/src/main/core.js Normal file
View file

@ -0,0 +1,517 @@
'use strict';
/** Bridge between the Electron main process and this checkout's ranking core. */
const path = require('path');
const { EventEmitter } = require('events');
const runtimes = require('./runtimes');
/**
* In development the core sits one level up in the repo. In a packaged build
* electron-builder copies it to resources/core (see extraResources), because
* src/data/seed/models.db is 42 MB and needs to stay a real file on disk.
*/
function resolveCoreRoot() {
const devRoot = path.resolve(__dirname, '..', '..', '..');
if (require('fs').existsSync(path.join(devRoot, 'src', 'index.js'))) return devRoot;
return path.join(process.resourcesPath ?? devRoot, 'core');
}
const REPO_ROOT = resolveCoreRoot();
/** Load the CLI core lazily so a require error surfaces as app state, not a crash. */
function loadChecker() {
// eslint-disable-next-line global-require, import/no-dynamic-require
const LLMChecker = require(path.join(REPO_ROOT, 'src', 'index.js'));
return new LLMChecker();
}
const { RANKING_CONTRACT_VERSION, classifyFit, memoryBudgetGB } = require(path.join(REPO_ROOT, 'src/models/ranking-contract'));
const { getRuntimeCommandSet } = require(path.join(REPO_ROOT, 'src/runtime/runtime-support'));
const { SOURCES } = require(path.join(REPO_ROOT, 'src/data/quality-evals'));
if (RANKING_CONTRACT_VERSION !== 1) throw new Error('Desktop requires ranking contract version 1. Update the core and desktop together.');
const FIT = { GOOD: 'fits', TIGHT: 'tight', OVER: 'over', UNKNOWN: 'unknown' };
function num(...vals) {
for (const v of vals) {
const n = Number(v);
if (Number.isFinite(n) && n > 0) return n;
}
return null;
}
/**
* The seven use cases the core ranks for, in the order they belong on a board.
* `talking` and `reading` are the core's own keys — renamed here for people.
*/
const CATEGORIES = [
{ key: 'general', label: 'General', emoji: '⚡', colour: 'green' },
{ key: 'coding', label: 'Coding', emoji: '💻', colour: 'blue' },
{ key: 'reasoning', label: 'Reasoning', emoji: '🧠', colour: 'purple' },
{ key: 'multimodal', label: 'Vision', emoji: '👁', colour: 'pink' },
{ key: 'creative', label: 'Creative', emoji: '✨', colour: 'orange' },
{ key: 'talking', label: 'Chat', emoji: '💬', colour: 'mint' },
{ key: 'reading', label: 'Long context', emoji: '📖', colour: 'yellow' },
];
/** Turn "fits in 6.108751999999999/12GB, Q6_K, 7B is sweet spot" into prose. */
function tidyReason(text) {
if (!text) return null;
return String(text)
.replace(/(\d+\.\d{2,})/g, (m) => Number(m).toFixed(1))
.replace(/\s+/g, ' ')
.trim();
}
/**
* Normalise one entry from `recommendations.recommendations[cat].bestModels`.
*
* The field names are not what they look like: `size` is a PARAMETER COUNT in
* billions (7, 14, 70), not bytes, and the memory a model actually needs is
* `estimatedRAM` in GB. Reading `size` as gigabytes puts every model in the
* wrong column — which is exactly what the first cut of this did.
*/
function toCard(raw = {}, budgetGB, detected, hardware) {
const name = raw.model_name ?? raw.name ?? raw.model ?? raw.modelName ?? 'unknown';
const req = raw.requirements ?? {};
const sizeGB = num(
raw.memory?.requiredGB, raw.estimatedRAM, raw.sizeGB, raw.memoryGB, raw.estimatedMemoryGB,
req.vram, req.recommended_vram, req.ram
);
const paramsB = num(raw.size, raw.parameters, raw.params);
const format = String(raw.artifactFormat ?? raw.format ?? '').toLowerCase() || null;
const chosen = raw.runtime ? { id: raw.runtime } : runtimes.chooseFor({ format }, detected, hardware);
// model_identifier is the exact pull ref ("qwen2.5-coder:7b-base-q6_K").
const ref =
raw.model_identifier ??
raw.installCommand?.replace(/^\S+\s+(pull|run)\s+/i, '') ??
raw.tag ?? raw.ref ?? name;
return {
name,
ref,
params: paramsB ? `${paramsB}B` : (raw.parameterSize ?? null),
purpose: tidyReason(raw.reasoning ?? raw.reason ?? raw.description ?? null),
family: raw.family ?? null,
sizeGB,
quant: raw.quantization ?? raw.quant ?? null,
tokensPerSec: num(raw.speed?.estimatedTPS, raw.speedAssumptions?.tokensPerSecond, raw.estimatedSpeed, raw.tokensPerSecond),
pulls: num(raw.pulls, raw.downloads),
score: num(raw.categoryScore, raw.score, raw.compatibilityScore),
hardwareScore: num(raw.hardwareScore),
category: raw.category ?? null,
license: raw.license ?? null,
format,
// Whether the quality half of this score was measured or guessed.
// Only 23% of the catalog has a real benchmark, so the UI must show
// which kind of number it is rather than letting them look alike.
quality: raw.qualitySource
? {
measured: raw.qualitySource.kind === 'measured',
metric: raw.qualitySource.metric ?? null,
rawScore: raw.qualitySource.rawScore ?? null,
source: raw.qualitySource.source ?? null,
independent: raw.qualitySource.independent !== false,
sizeUnknown: Boolean(raw.qualitySource.sizeUnknown),
basis: raw.qualitySource.basis ?? null,
}
: { measured: false, basis: 'parameter count' },
budgetGB: num(raw.memory?.budgetGB, budgetGB),
context: raw.context || null,
fit: classifyFit(sizeGB, num(raw.memory?.budgetGB, budgetGB)),
runtime: chosen ? chosen.id : null,
commands: chosen ? (chosen.id === 'lmstudio' ? runtimes.commandsFor(chosen.id, ref) :
getRuntimeCommandSet({ ...raw, model_identifier: ref }, chosen.id)) : null,
};
}
/**
* The ranked models live at `analysis.recommendations.recommendations[cat]`,
* two levels deep, each category holding a `bestModels` array plus the counts
* behind it. Returns one group per category in board order.
*/
function harvestCategories(analysis = {}) {
const root =
analysis.recommendations?.recommendations ??
analysis.intelligentRecommendations?.recommendations ??
null;
if (!root || typeof root !== 'object') return [];
return CATEGORIES.map((spec) => {
const node = root[spec.key];
const best = Array.isArray(node?.bestModels) ? node.bestModels : [];
return {
...spec,
models: best,
evaluated: num(node?.totalCandidates, node?.totalEvaluated) ?? null,
};
}).filter((g) => g.models.length);
}
/**
* Flat, de-duplicated list across every bucket the core exposes. Used for the
* fit tally in the sidebar; `incompatible` is included on purpose so the count
* does not claim everything runs on this machine.
*/
function harvest(analysis = {}) {
const buckets = [
analysis.recommended, analysis.compatible,
analysis.models, analysis.results, analysis.marginal, analysis.incompatible,
];
const flat = [];
for (const b of buckets) {
if (Array.isArray(b)) flat.push(...b);
else if (b && typeof b === 'object') flat.push(...Object.values(b).flat().filter(Boolean));
}
const seen = new Set();
return flat.filter((m) => {
const key = String(m?.model_name ?? m?.name ?? m?.model ?? '').toLowerCase();
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
}
class Core extends EventEmitter {
constructor(options = {}) {
super();
this.checker = null;
this.options = options;
this.queue = Promise.resolve();
this.checkerFactory = options.checkerFactory || loadChecker;
this.detectRuntimes = options.detectRuntimes || runtimes.detectAll;
this.state = {
phase: 'idle', // idle | hardware | runtimes | models | ready | error
hardware: null,
runtimes: [],
groups: [], // one per use case — the board columns
models: [], // flat list, drives the sidebar tallies
tally: {}, // { fits, tight, over }
budgetGB: null,
error: null,
timings: {},
};
}
snapshot() {
return JSON.parse(JSON.stringify(this.state));
}
#publish(patch) {
Object.assign(this.state, patch);
this.emit('state', this.snapshot());
}
/**
* Full scan, published in phases. Safe to call again — a refresh reuses the
* same long-lived LLMChecker instance and only re-runs detection.
*/
#enqueue(work) {
const result = this.queue.then(work, work);
this.queue = result.catch(() => {});
return result;
}
scan(options = {}) { return this.#enqueue(() => this.#scan(options)); }
async #scan({ useCase = 'general' } = {}) {
const t0 = Date.now();
this.#publish({ phase: 'hardware', error: null });
try {
if (!this.checker) this.checker = this.checkerFactory();
} catch (err) {
this.#publish({ phase: 'error', error: `Could not load the analysis core: ${err.message}` });
return this.snapshot();
}
let hardware = null;
try {
// analyze() detects hardware and scores in one pass; we surface the
// hardware half as soon as it is available.
const analysisPromise = this.checker.analyze({ useCase });
// Runtime detection is independent of the model ranking, so it runs
// alongside it instead of after — this is the parallelism the CLI
// gives up by using spawnSync.
//
// Hardware is not known yet at this point, so probe permissively
// (a placeholder accelerator keeps vLLM in the sweep) and apply the
// real eligibility filter once the analysis lands. Gating first
// would silently drop runtimes the machine can actually use.
const runtimePromise = this.detectRuntimes({
gpu: { vramGB: 1 },
summary: { backend: 'unknown' },
});
const [analysis, probed] = await Promise.all([analysisPromise, runtimePromise]);
hardware = this.#shapeHardware(analysis);
const eligibleIds = new Set(
runtimes.eligible(analysis.hardware ?? {}).map((r) => r.id)
);
const detected = probed.filter((r) => eligibleIds.has(r.id));
const tHw = Date.now() - t0;
this.#publish({ phase: 'runtimes', hardware, runtimes: detected, timings: { hardware: tHw } });
const budgetGB = memoryBudgetGB(analysis.hardware ?? {});
const hw = analysis.hardware ?? {};
// Board columns: one per use case, ranked within.
const groups = harvestCategories(analysis).map((g) => ({
key: g.key,
label: g.label,
emoji: g.emoji,
colour: g.colour,
evaluated: g.evaluated,
models: g.models
.map((m) => toCard(m, budgetGB, detected, hw))
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)),
}));
// Flat list drives the counts in the sidebar.
const models = harvest(analysis).map((m) => toCard(m, budgetGB, detected, hw));
const tally = models.reduce((acc, m) => {
acc[m.fit] = (acc[m.fit] ?? 0) + 1;
return acc;
}, {});
this.#publish({
phase: 'ready',
groups,
models,
tally,
budgetGB,
timings: { hardware: tHw, runtimes: tHw, models: Date.now() - t0, total: Date.now() - t0 },
});
} catch (err) {
this.#publish({ phase: 'error', error: err?.message ?? String(err) });
}
return this.snapshot();
}
#shapeHardware(analysis = {}) {
const hw = analysis.hardware ?? analysis.system ?? {};
const gpu = hw.gpu ?? {};
const cpu = hw.cpu ?? {};
return {
gpuModel: gpu.model ?? gpu.name ?? hw.summary?.gpu ?? null,
vramGB: num(gpu.vramGB, gpu.vram, hw.summary?.totalVRAM),
backend: hw.summary?.bestBackend ?? hw.summary?.backend ?? gpu.backend ?? null,
cudaVersion: hw.cuda?.version ?? null,
driver: hw.cuda?.driver ?? null,
cpuModel: cpu.brand ?? cpu.model ?? null,
cores: num(cpu.cores, cpu.threads),
physicalCores: num(cpu.physicalCores),
simd: cpu.simd ?? (cpu.avx512 ? 'AVX-512' : cpu.avx2 ? 'AVX2' : null),
ramGB: num(hw.memory?.totalGB, hw.memory?.total, hw.summary?.systemRAM),
tier: hw.summary?.tier ?? analysis.tier ?? null,
maxModelGB: num(hw.summary?.maxModelSizeGB, analysis.maxModelSize),
fingerprint: hw.fingerprint ?? hw.summary?.fingerprint ?? null,
platform: process.platform,
arch: process.arch,
};
}
/** Commands for a card under a runtime the user picked explicitly. */
commandsFor(runtimeId, ref) {
return runtimes.commandsFor(runtimeId, ref);
}
/**
* The CLI capabilities, exposed as in-app actions.
*
* These used to be rendered as a list of commands with a Copy button,
* which defeats the point of shipping a desktop app. Each entry below runs
* the real code in this process and returns structured output the UI
* renders directly.
*
* `destructive` entries are the ones that write to the catalog; the UI
* confirms those before running.
*/
listActions() {
return [
{
id: 'sync-catalog', label: 'Refresh model catalog',
blurb: 'Re-scrape the Ollama library for new models',
destructive: true, estimate: '~30 s',
},
{
id: 'refresh-benchmarks', label: 'Update benchmark scores',
blurb: `Refresh all ${Object.keys(SOURCES).length} public benchmark sources`,
destructive: true, estimate: '~10 s',
},
{
id: 'toolcheck', label: 'Check local AI tooling',
blurb: 'Probe every runtime and report what is usable',
destructive: false, estimate: 'instant',
},
{
id: 'gpu-plan', label: 'Plan GPU offload',
blurb: 'How many layers of a model fit in VRAM',
destructive: false, estimate: 'instant',
},
{
id: 'coverage', label: 'Benchmark coverage',
blurb: 'Catalog families represented in public benchmarks',
destructive: false, estimate: 'instant',
},
];
}
runAction(id, args = {}) { return this.#enqueue(() => this.#runAction(id, args)); }
async #runAction(id, args = {}) {
switch (id) {
case 'toolcheck': {
const hw = this.state.hardware ?? {};
const found = await runtimes.detectAll({
gpu: { vramGB: hw.vramGB }, summary: { backend: hw.backend },
});
this.#publish({ runtimes: found });
return {
kind: 'table',
columns: ['Runtime', 'State', 'Version', 'Models'],
rows: found.map((r) => [
r.name,
r.serving ? 'Running' : r.installed ? 'Installed' : 'Not installed',
r.version ?? '—',
String(r.models?.length ?? 0),
]),
};
}
case 'coverage': {
const { db, evals } = await this.#openEvals();
if (!evals) return { kind: 'text', text: 'No benchmark database found.' };
let cov, stats;
try {
cov = evals.coverage(db.prepare('SELECT name FROM models').all());
stats = evals.stats();
} finally { db.close(); }
return {
kind: 'table',
columns: ['Metric', 'Value'],
rows: [
['Catalog models', String(cov.total)],
['Families with benchmark data', `${cov.measured} (${Math.round(cov.measured / cov.total * 100)}%)`],
['Families without benchmark data', String(cov.total - cov.measured)],
['Total eval rows', String(stats.evals)],
...stats.byCategory.map((c) => [` ${c.category}`, String(c.c)]),
...stats.sources.map((s) => [` ${s.display_name}`, `${s.row_count} rows`]),
],
};
}
case 'gpu-plan': {
const hw = this.state.hardware ?? {};
const budget = this.state.budgetGB;
const rows = (this.state.models ?? [])
.filter((m) => Number.isFinite(m.sizeGB))
.sort((a, b) => a.sizeGB - b.sizeGB)
.slice(0, 25)
.map((m) => [
m.name,
`${m.sizeGB.toFixed(1)} GB`,
budget ? `${Math.round(m.sizeGB / budget * 100)}%` : '—',
m.fit === 'unknown' ? 'Unknown' : m.fit === 'over' ? 'Exceeds budget' : m.fit === 'tight' ? 'Limited headroom' : 'Within memory budget',
]);
return {
kind: 'table',
columns: ['Model', 'Estimated memory', `of ${budget ?? 'unknown'} GB`, 'Fit'],
rows,
};
}
case 'refresh-benchmarks': {
const { db, evals } = await this.#openEvals({ write: true });
if (!evals) return { kind: 'text', text: 'No benchmark database found.' };
const out = [];
try {
for (const src of Object.keys(SOURCES)) {
try {
const r = await evals.ingest(src, this.options.fetchImpl ? { fetchImpl: this.options.fetchImpl } : {});
out.push([src, `${r.rows} rows`, 'ok']);
} catch (err) {
out.push([src, '—', err.message.slice(0, 60)]);
}
}
const catalog = db.prepare('SELECT name FROM models').all();
evals.refreshCatalogCohort(catalog);
} finally { db.close(); }
this.#invalidateQuality();
await this.#scan();
return { kind: 'table', columns: ['Source', 'Rows', 'Result'], rows: out };
}
case 'sync-catalog': {
const before = await this.#catalogCount();
await this.#runCli(['sync', '--force', '--quiet']);
const after = await this.#catalogCount();
// The cohort is derived from the catalog, so it must follow it.
const { db, evals } = await this.#openEvals({ write: true });
if (evals) {
try { evals.refreshCatalogCohort(db.prepare('SELECT name FROM models').all()); }
finally { db.close(); }
}
this.#invalidateQuality();
await this.#scan();
return {
kind: 'table',
columns: ['Metric', 'Value'],
rows: [
['Models before', String(before)],
['Models after', String(after)],
['New', String(Math.max(0, after - before))],
],
};
}
default:
throw new Error(`Unknown action: ${id}`);
}
}
#invalidateQuality() {
this.checker?.intelligentRecommender?.invalidateQualityEvals();
}
async #openEvals({ write = false } = {}) {
const ModelDatabase = require(path.join(REPO_ROOT, 'src/data/model-database'));
const { QualityEvals } = require(path.join(REPO_ROOT, 'src/data/quality-evals'));
const dbPath = this.options.dbPath || path.join(require('os').homedir(), '.llm-checker', 'models.db');
if (!write && !require('fs').existsSync(dbPath)) return { db: null, evals: null };
const database = new ModelDatabase({ dbPath, readOnly: !write });
try {
await database.initialize();
const evals = new QualityEvals(database, { readOnly: !write });
// Keep the small query facade shared across native SQLite and WASM.
return { db: { prepare: sql => ({ all: () => database.all(sql), get: () => database.get(sql) }),
close: () => database.close() }, evals };
} catch (error) {
database.close();
throw error;
}
}
async #catalogCount() {
const { db } = await this.#openEvals();
if (!db) return 0;
try { return db.prepare('SELECT COUNT(*) c FROM models').get().c; }
finally { db.close(); }
}
#runCli(args) {
return new Promise((resolve, reject) => {
const { execFile } = require('child_process');
execFile(process.execPath, [path.join(REPO_ROOT, 'bin', 'cli.js'), ...args],
{ env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
(err, stdout, stderr) => (err ? reject(err) : resolve(`${stdout}${stderr}`)));
});
}
}
module.exports = { Core, classifyFit, FIT, toCard, harvest, harvestCategories, CATEGORIES, tidyReason };

171
desktop/src/main/main.js Normal file
View file

@ -0,0 +1,171 @@
'use strict';
const path = require('path');
const { app, BrowserWindow, Menu, ipcMain, shell, clipboard, nativeTheme } = require('electron');
const { Core } = require('./core');
const runtimes = require('./runtimes');
const IS_MAC = process.platform === 'darwin';
// One Core for the life of the app. This is the whole point of the desktop
// build: hardware detection and the 42 MB catalog load happen once, not on
// every invocation the way they do in the CLI.
const core = new Core();
/** @type {BrowserWindow | null} */
let win = null;
function createWindow() {
// No menu bar anywhere but macOS (whose edit shortcuts need it); the few
// useful entries live in the app's own Settings view instead.
if (!IS_MAC) Menu.setApplicationMenu(null);
win = new BrowserWindow({
width: 1280,
height: 860,
minWidth: 860,
minHeight: 560,
show: false, // avoid a white flash; show on ready-to-show
// macOS keeps its native chrome with real traffic lights; everywhere
// else the window is frameless + transparent so the renderer can draw
// the design's 10px-radius window on all four corners itself.
...(IS_MAC
? {
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1c1c1e' : '#ffffff',
titleBarStyle: 'hiddenInset',
trafficLightPosition: { x: 20, y: 20 },
}
: {
frame: false,
transparent: true,
backgroundColor: '#00000000',
}),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false, // preload needs require() for the bridge
spellcheck: false,
},
});
win.loadFile(path.join(__dirname, '..', 'renderer', 'index.html'));
win.once('ready-to-show', () => {
win.show();
// Kick the scan only after the window is up, so the first paint is not
// blocked behind hardware probes.
core.scan().catch((err) => console.error('[scan]', err));
});
// Push every phase transition to the renderer as it happens.
core.on('state', (state) => {
if (win && !win.isDestroyed()) win.webContents.send('core:state', state);
});
// External links open in the real browser, never in the app window.
win.webContents.setWindowOpenHandler(({ url }) => {
if (/^https?:\/\//i.test(url)) shell.openExternal(url);
return { action: 'deny' };
});
win.on('closed', () => { win = null; });
}
// Single instance: a second launch focuses the existing window.
if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
app.on('second-instance', () => {
if (!win) return;
if (win.isMinimized()) win.restore();
win.focus();
});
app.whenReady().then(() => {
registerIpc();
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
}
function registerIpc() {
// Renderer asks for the current snapshot on boot (it may attach after the
// first phases have already fired).
ipcMain.handle('core:snapshot', () => core.snapshot());
ipcMain.handle('core:scan', async (_e, opts) => core.scan(opts ?? {}));
ipcMain.handle('runtimes:detect', async () => {
const hw = core.state.hardware ?? {};
return runtimes.detectAll({ gpu: { vramGB: hw.vramGB }, summary: { backend: hw.backend } });
});
ipcMain.handle('runtimes:commands', (_e, { runtime, ref }) =>
runtimes.commandsFor(runtime, ref)
);
// Deep probe for one runtime's detail view. Lazy on purpose — these cost
// more than the per-scan detect(), so they only run when the user opens
// that page.
ipcMain.handle('runtimes:details', async (_e, runtime) => {
const hw = core.state.hardware ?? {};
return runtimes.details(runtime, {
gpu: { vramGB: hw.vramGB },
summary: { backend: hw.backend },
});
});
ipcMain.handle('clipboard:write', (_e, text) => {
clipboard.writeText(String(text ?? ''));
return true;
});
// Only http(s) ever reaches the shell — never a file: or custom scheme.
ipcMain.handle('shell:open', (_e, url) => {
const u = String(url ?? '');
if (!/^https?:\/\//i.test(u)) return false;
shell.openExternal(u);
return true;
});
// Run one of the CLI's capabilities in-process and hand back structured
// output. The point of the desktop app is that none of this needs a
// terminal, so these are real actions rather than commands to copy.
ipcMain.handle('action:run', async (_e, { id, args } = {}) => {
const started = Date.now();
try {
const result = await core.runAction(id, args ?? {});
return { ok: true, id, ms: Date.now() - started, ...result };
} catch (err) {
return { ok: false, id, ms: Date.now() - started, error: err?.message ?? String(err) };
}
});
ipcMain.handle('action:list', () => core.listActions());
ipcMain.handle('app:info', () => ({
version: app.getVersion(),
electron: process.versions.electron,
node: process.versions.node,
platform: process.platform,
arch: process.arch,
}));
// Frameless window controls (Linux/Windows — macOS has the real ones).
ipcMain.handle('win:minimize', () => { win?.minimize(); return true; });
ipcMain.handle('win:maximize', () => {
if (!win) return false;
if (win.isMaximized()) win.unmaximize();
else win.maximize();
return win.isMaximized();
});
ipcMain.handle('win:close', () => { win?.close(); return true; });
}

View file

@ -0,0 +1,53 @@
'use strict';
const { contextBridge, ipcRenderer } = require('electron');
/**
* The only surface the renderer gets. No node, no ipcRenderer, no filesystem —
* just these calls. Every argument crosses as structured-clonable data.
*/
contextBridge.exposeInMainWorld('llm', {
/** Current state without waiting for the next phase transition. */
snapshot: () => ipcRenderer.invoke('core:snapshot'),
/** Re-run detection and ranking. Resolves with the final state. */
scan: (opts) => ipcRenderer.invoke('core:scan', opts),
/** Re-probe the local runtimes only. */
detectRuntimes: () => ipcRenderer.invoke('runtimes:detect'),
/** Deep data for one runtime's own view. Lazy — call on navigation. */
runtimeDetails: (id) => ipcRenderer.invoke('runtimes:details', id),
/** The CLI's capabilities as in-app actions, so no terminal is needed. */
listActions: () => ipcRenderer.invoke('action:list'),
runAction: (id, args) => ipcRenderer.invoke('action:run', { id, args }),
/** install/pull/run/serve command set for a (runtime, modelRef) pair. */
commandsFor: (runtime, ref) => ipcRenderer.invoke('runtimes:commands', { runtime, ref }),
copy: (text) => ipcRenderer.invoke('clipboard:write', text),
/** http(s) only; anything else is refused in the main process. */
openExternal: (url) => ipcRenderer.invoke('shell:open', url),
appInfo: () => ipcRenderer.invoke('app:info'),
/** Synchronous platform id so the renderer can pick its chrome at once. */
platform: process.platform,
/** Frameless window controls — no-ops on macOS, which has real chrome. */
winMinimize: () => ipcRenderer.invoke('win:minimize'),
winMaximize: () => ipcRenderer.invoke('win:maximize'),
winClose: () => ipcRenderer.invoke('win:close'),
/**
* Subscribe to phase transitions. Returns an unsubscribe function so the
* renderer never leaks listeners across reloads.
*/
onState: (handler) => {
const wrapped = (_event, state) => handler(state);
ipcRenderer.on('core:state', wrapped);
return () => ipcRenderer.removeListener('core:state', wrapped);
},
});

View file

@ -0,0 +1,966 @@
'use strict';
/**
* Unified runtime registry for the desktop app.
*
* The CLI advertises `auto|ollama|vllm|mlx|llama.cpp|transformers` on several
* commands, but src/runtime/runtime-support.js only knows about three of them
* (`ollama`, `vllm`, `mlx`) and silently normalises everything else to
* `ollama`. That is fine for the registry/recommender path — which does model
* llama.cpp and transformers — but it means install/pull/run commands come out
* wrong for GGUF and safetensors workflows.
*
* This module is the single source of truth for the desktop app: every runtime
* the catalog can target, how to detect it, and how to install/pull/run with
* it. Detection is async and runs in parallel; nothing here blocks the event
* loop the way the CLI's 40 spawnSync call sites do.
*/
const { execFile } = require('child_process');
const fs = require('fs');
const fsp = require('fs/promises');
const os = require('os');
const path = require('path');
const PROBE_TIMEOUT_MS = 2500;
const HTTP_TIMEOUT_MS = 1200;
/**
* Run a command without blocking; resolve to null instead of throwing.
*
* Two rules learned the hard way from probing these runtimes for real:
*
* - Exit code is NOT the success signal. `llama-quantize --help` exits 1 with
* perfectly good output; `ollama --version` exits 0 while printing warnings
* that mean the server is down. Callers pass `expectNonZero` and always get
* whatever was written.
* - Some tools write everything to stderr (`llama-cli --version` emits zero
* bytes on stdout), so both streams are merged.
*/
function probe(cmd, args, { timeout = PROBE_TIMEOUT_MS, expectNonZero = false } = {}) {
return new Promise((resolve) => {
let done = false;
const finish = (v) => { if (!done) { done = true; resolve(v); } };
const child = execFile(cmd, args, { timeout, windowsHide: true }, (err, stdout, stderr) => {
const out = `${stdout ?? ''}${stderr ?? ''}`.trim();
// ENOENT / timeout / signal → genuinely nothing to read.
if (err && !expectNonZero && !out) return finish(null);
if (err && err.code === 'ENOENT') return finish(null);
finish(out || null);
});
child.on('error', () => finish(null));
});
}
/** Absolute path under the user's home. Never build these with a literal '~':
* execFile does not run a shell, so the tilde stays literal and every probe
* using it silently fails. */
const home = (...parts) => path.join(os.homedir(), ...parts);
/**
* Find a binary without trusting PATH.
*
* An Electron app launched from a .desktop entry does not source ~/.profile,
* so ~/.local/bin — where ollama, lms, llama-cli and the python shim all live
* on this machine — is simply absent. Verified: with a login-shell PATH,
* `lms` resolves; with a desktop-launcher PATH it is "not found". So every CLI
* probe resolves an absolute path first and spawns that.
*/
const BIN_DIRS = [
home('.local', 'bin'),
'/usr/local/bin',
'/usr/bin',
'/bin',
'/opt/homebrew/bin',
'/usr/local/sbin',
...String(process.env.PATH ?? '').split(path.delimiter).filter(Boolean),
];
const binCache = new Map();
function resolveBin(name) {
if (binCache.has(name)) return binCache.get(name);
let found = null;
for (const dir of BIN_DIRS) {
const p = path.join(dir, name);
try {
fs.accessSync(p, fs.constants.X_OK);
found = p;
break;
} catch { /* keep looking */ }
}
binCache.set(name, found);
return found;
}
/** probe() against a resolved absolute path; null when the binary is absent. */
async function probeBin(name, args, opts) {
const bin = resolveBin(name);
if (!bin) return null;
return probe(bin, args, opts);
}
/** Recursive directory size in bytes, with a work cap so a huge tree cannot
* stall the probe. Returns { bytes, capped }. */
async function dirSize(dir, { maxEntries = 20000 } = {}) {
let bytes = 0;
let seen = 0;
let capped = false;
const stack = [dir];
while (stack.length) {
const cur = stack.pop();
let entries;
try {
entries = await fsp.readdir(cur, { withFileTypes: true });
} catch { continue; }
for (const e of entries) {
if (++seen > maxEntries) { capped = true; stack.length = 0; break; }
const full = path.join(cur, e.name);
if (e.isDirectory()) stack.push(full);
else if (e.isFile()) {
try { bytes += (await fsp.stat(full)).size; } catch { /* vanished */ }
}
}
}
return { bytes, capped };
}
/** Whitelisted env reporting. Tokens are reported as a boolean and never by
* value — one probe draft would have printed HF_TOKEN verbatim into JSON the
* app parses and logs. */
const ENV_SAFE = [
'OLLAMA_HOST', 'OLLAMA_MODELS', 'OLLAMA_KEEP_ALIVE', 'OLLAMA_CONTEXT_LENGTH',
'OLLAMA_NUM_PARALLEL', 'OLLAMA_MAX_LOADED_MODELS', 'OLLAMA_KV_CACHE_TYPE',
'CUDA_VISIBLE_DEVICES', 'HF_HOME', 'HF_HUB_CACHE', 'HF_HUB_OFFLINE',
];
const ENV_SECRET = ['HF_TOKEN', 'HUGGING_FACE_HUB_TOKEN', 'OPENAI_API_KEY'];
function safeEnvRows() {
const rows = [];
for (const k of ENV_SAFE) {
if (process.env[k]) rows.push({ label: k, value: process.env[k] });
}
for (const k of ENV_SECRET) {
if (process.env[k]) rows.push({ label: k, value: 'set (value hidden)' });
}
return rows;
}
/** LM Studio reports vision capability differently in its two APIs; check both
* shapes rather than assuming either. */
const extraHasVision = (entry) =>
Boolean(entry?.vision ?? entry?.visionCapable ?? entry?.capabilities?.includes?.('vision'));
const fmtBytes = (n) => {
if (!Number.isFinite(n) || n <= 0) return null;
if (n >= 1e9) return `${(n / 1e9).toFixed(1).replace(/\.0$/, '')} GB`;
if (n >= 1e6) return `${Math.round(n / 1e6)} MB`;
return `${Math.round(n / 1e3)} KB`;
};
/** GET a local endpoint with a short deadline. Resolves to parsed JSON or null. */
async function probeHttp(url, { timeout = HTTP_TIMEOUT_MS } = {}) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), timeout);
try {
const res = await fetch(url, { signal: ac.signal });
if (!res.ok) return null;
return await res.json();
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
function pythonCandidates() {
return process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
}
/** Try `import <mod>` across the available python binaries.
* Cold imports of heavy libs (transformers ~3-5 s) far exceed the default
* probe timeout, so these get their own budget. */
async function probePythonModule(mod, versionExpr = `${mod}.__version__`) {
for (const py of pythonCandidates()) {
const bin = resolveBin(py) ?? py;
const out = await probe(bin, ['-c', `import ${mod}; print(${versionExpr})`], { timeout: 10000 });
if (out) return { version: out.split('\n').pop().trim(), python: bin };
}
return null;
}
/**
* Package version WITHOUT importing it.
*
* `import vllm` costs ~2.7 s and `import transformers` ~4 s, which is far too
* much for a probe that runs on every scan. Reading the installed distribution
* metadata answers the same question in a fraction of that, and it exits
* non-zero when the package is absent — which is a finding, not an error.
*/
async function probePythonMeta(dist) {
for (const py of pythonCandidates()) {
const bin = resolveBin(py) ?? py;
const out = await probe(
bin,
['-c', `import importlib.metadata as m;print(m.version(${JSON.stringify(dist)}))`],
{ timeout: 6000, expectNonZero: true }
);
const ver = out && out.match(/^\s*(\d[\w.+-]*)\s*$/m);
if (ver) return { version: ver[1], python: bin };
}
return null;
}
/** One cheap torch query for the GPU facts both vLLM and Transformers need. */
async function probeTorchCuda() {
const code = [
'import json',
'try:',
' import torch',
' a = torch.cuda.is_available()',
' d = {"torch": torch.__version__, "cudaAvailable": a}',
' if a:',
' d["deviceName"] = torch.cuda.get_device_name(0)',
' cc = torch.cuda.get_device_capability(0)',
' d["capability"] = f"sm_{cc[0]}{cc[1]}"',
' free, total = torch.cuda.mem_get_info(0)',
' d["freeMiB"] = free // (1024*1024)',
' d["totalMiB"] = total // (1024*1024)',
' d["bf16"] = torch.cuda.is_bf16_supported()',
' print(json.dumps(d))',
'except Exception as e:',
' print(json.dumps({"error": type(e).__name__}))',
].join('\n');
for (const py of pythonCandidates()) {
const bin = resolveBin(py) ?? py;
const out = await probe(bin, ['-c', code], { timeout: 12000, expectNonZero: true });
if (!out) continue;
try {
const parsed = JSON.parse(out.split('\n').filter(Boolean).pop());
if (!parsed.error) return parsed;
} catch { /* try the next interpreter */ }
}
return null;
}
const isAppleSilicon = () => process.platform === 'darwin' && process.arch === 'arm64';
const isWin = () => process.platform === 'win32';
/**
* Every runtime is declared once. `formats` drives which catalog artifacts can
* actually run under it; `supported(hw)` gates it against this machine.
*/
const RUNTIMES = [
{
id: 'ollama',
name: 'Ollama',
blurb: 'Easiest local runner. One command per model.',
formats: ['ollama', 'gguf'],
endpoint: 'http://127.0.0.1:11434',
supported: () => true,
install: () =>
process.platform === 'darwin' ? 'brew install ollama'
: isWin() ? 'winget install Ollama.Ollama'
: 'curl -fsSL https://ollama.com/install.sh | sh',
pull: (ref) => `ollama pull ${ref}`,
run: (ref) => `ollama run ${ref}`,
async detect() {
const [version, tags] = await Promise.all([
probeBin('ollama', ['--version']),
probeHttp('http://127.0.0.1:11434/api/tags'),
]);
if (!version && !tags) return { installed: false, serving: false };
// `ollama --version` exits 0 even with the server down, printing
// two Warning: lines — so the semver, not the exit code, is the tell.
const sem = version && version.match(/(\d+\.\d+\.\d+)/);
return {
installed: Boolean(version) || Boolean(tags),
serving: Boolean(tags),
version: sem ? sem[1] : null,
models: Array.isArray(tags?.models)
? tags.models.map((m) => ({
ref: m.name,
sizeBytes: m.size ?? null,
quant: m.details?.quantization_level ?? null,
params: m.details?.parameter_size ?? null,
family: m.details?.family ?? null,
contextLength: m.details?.context_length ?? null,
capabilities: m.capabilities ?? [],
}))
: [],
};
},
async probeDetails({ state }) {
const modelsDir = process.env.OLLAMA_MODELS || home('.ollama', 'models');
const [ps, disk, backends] = await Promise.all([
// What is resident in VRAM right now, as opposed to merely downloaded.
probeHttp('http://127.0.0.1:11434/api/ps'),
dirSize(modelsDir),
// Which accelerators this build actually shipped with.
(async () => {
const roots = [
home('.local', 'ollama-dist', 'lib', 'ollama'),
'/usr/local/lib/ollama', '/usr/lib/ollama',
home('.local', 'lib', 'ollama'),
];
for (const r of roots) {
try {
const names = await fsp.readdir(r);
const gpu = names.filter((n) => /cuda|rocm|hip|vulkan|metal/i.test(n));
const cpu = names.filter((n) => /^libggml-cpu-/.test(n)).length;
return { root: r, gpu, cpu };
} catch { /* next candidate */ }
}
return null;
})(),
]);
const loaded = Array.isArray(ps?.models) ? ps.models : [];
const loadedNames = new Set(loaded.map((m) => m.name));
const models = (state.models ?? []).map((m) => ({ ...m, loaded: loadedNames.has(m.ref) }));
const sections = [];
sections.push({
id: 'capability', title: 'What it can do',
emptyState: 'Start the server to read its capabilities.',
rows: [
backends?.gpu?.length
? { label: 'GPU backends in this build', value: backends.gpu.join(', ') }
: { label: 'GPU backends in this build', value: 'none found' },
backends?.cpu
? { label: 'CPU kernels shipped', value: `${backends.cpu} microarchitectures` }
: null,
{ label: 'OpenAI-compatible API', value: `${'http://127.0.0.1:11434'}/v1` },
].filter(Boolean),
});
sections.push({
id: 'storage', title: 'Storage',
emptyState: 'No model directory found.',
rows: [
{ label: 'Models directory', value: modelsDir },
disk.bytes
? { label: 'Disk used by models', value: fmtBytes(disk.bytes) + (disk.capped ? ' (partial)' : '') }
: null,
].filter(Boolean),
});
const envRows = safeEnvRows().filter((r) => r.label.startsWith('OLLAMA_'));
sections.push({
id: 'config', title: 'Configuration',
emptyState: 'No OLLAMA_* overrides set — all defaults.',
rows: envRows,
});
return {
sections,
models,
actions: [
{ label: 'List downloaded models', command: 'ollama list' },
{ label: 'Show what is loaded now', command: 'ollama ps' },
],
counts: { downloaded: models.length, loaded: loaded.length },
};
},
},
{
id: 'llama.cpp',
name: 'llama.cpp',
blurb: 'Runs any GGUF directly. Most control over offload and context.',
formats: ['gguf'],
endpoint: 'http://127.0.0.1:8080',
supported: () => true,
install: () =>
process.platform === 'darwin' ? 'brew install llama.cpp'
: isWin() ? 'winget install ggml.llamacpp'
: 'brew install llama.cpp # or build: cmake -B build && cmake --build build',
// llama.cpp has no registry of its own — GGUF comes from Hugging Face.
pull: (ref) => `huggingface-cli download ${ref} --local-dir ./models/${String(ref).split('/').pop()}`,
run: (ref) => `llama-cli -hf ${ref} -c 4096`,
serve: (ref) => `llama-server -hf ${ref} -c 4096 --port 8080`,
async detect() {
const [cli, server, health] = await Promise.all([
// Writes its banner to stderr and nothing to stdout.
probeBin('llama-cli', ['--version'], { expectNonZero: true }),
probeBin('llama-server', ['--version'], { expectNonZero: true }),
probeHttp('http://127.0.0.1:8080/health'),
]);
const raw = cli || server;
if (!raw && !health) return { installed: false, serving: false };
const m = raw && raw.match(/version:\s*(\S+)/i);
const build = raw && raw.match(/build\s+(\d+)/i);
return {
installed: Boolean(raw),
serving: Boolean(health),
version: m ? m[1] : (raw ? raw.split('\n')[0].slice(0, 40) : null),
build: build ? build[1] : null,
models: [],
};
},
async probeDetails() {
// llama.cpp has no model registry, so its page is about CAPABILITY,
// not inventory: what this particular build can actually do.
const [devices, props, served, banner] = await Promise.all([
probeBin('llama-cli', ['--list-devices'], { expectNonZero: true, timeout: 4000 }),
probeHttp('http://127.0.0.1:8080/props'),
probeHttp('http://127.0.0.1:8080/v1/models'),
probeBin('llama-cli', ['--version'], { expectNonZero: true }),
]);
// "(none)" is a real and important answer: a CPU-only build.
const devLines = (devices ?? '')
.split('\n')
.filter((l) => /^\s{2,}\S/.test(l) && !/available devices/i.test(l))
.map((l) => l.trim())
.filter(Boolean);
const hasGpu = devLines.length > 0 && !/^\(none\)$/i.test(devLines[0] ?? '');
const libRoot = (() => {
const bin = resolveBin('llama-cli');
if (!bin) return null;
try {
// The shim points at the real build directory.
const txt = fs.readFileSync(bin, 'utf8');
const m = txt.match(/exec\s+"([^"]+)\/llama-cli"/);
return m ? m[1] : path.dirname(fs.realpathSync(bin));
} catch { return null; }
})();
const backendLibs = (() => {
if (!libRoot) return [];
try {
return fs.readdirSync(libRoot)
.filter((n) => /^libggml-(cuda|vulkan|hip|rocm|metal|blas|sycl)/i.test(n));
} catch { return []; }
})();
const sections = [];
sections.push({
id: 'capability', title: 'What it can do',
emptyState: 'Could not read this build.',
rows: [
{
label: 'Accelerator backends',
value: hasGpu ? devLines.join(', ')
: 'None — this is a CPU-only build',
hint: hasGpu ? null
: 'Models will run on CPU regardless of the GPU in this machine. Install a CUDA or Vulkan build to use it.',
},
backendLibs.length
? { label: 'Backend libraries', value: backendLibs.join(', ') }
: null,
banner?.match(/built with (.+?) for (\S+)/i)
? { label: 'Built with', value: banner.match(/built with (.+?) for (\S+)/i).slice(1, 3).join(' — ') }
: null,
{ label: 'Model format', value: 'GGUF only (pass one with -m)' },
].filter(Boolean),
});
sections.push({
id: 'config', title: 'Configuration',
emptyState: 'Server not running.',
rows: [
libRoot ? { label: 'Build directory', value: libRoot } : null,
{ label: 'Server endpoint', value: 'http://127.0.0.1:8080 (when started)' },
props?.model_path
? { label: 'Loaded model', value: String(props.model_path).split('/').pop() }
: null,
props?.n_ctx ? { label: 'Active context', value: `${props.n_ctx} tokens` } : null,
].filter(Boolean),
});
const models = Array.isArray(served?.data)
? served.data.map((m) => ({ ref: m.id, loaded: true }))
: [];
return {
sections,
models,
actions: [
{ label: 'Start a server on :8080', command: 'llama-server -m model.gguf -c 4096 --port 8080' },
{ label: 'Run a model once', command: 'llama-cli -m model.gguf -p "hello"' },
],
gaps: models.length ? [] : [
'llama.cpp keeps no model registry, so there is nothing to list until a server is running with -m.',
],
};
},
},
{
id: 'lmstudio',
name: 'LM Studio',
blurb: 'GUI plus an OpenAI-compatible server. GGUF and MLX.',
formats: ['gguf', 'mlx'],
endpoint: 'http://127.0.0.1:1234',
supported: () => true,
install: () =>
process.platform === 'darwin' ? 'brew install --cask lm-studio'
: 'Download from https://lmstudio.ai',
pull: (ref) => `lms get ${ref}`,
run: (ref) => `lms load ${ref}`,
async detect() {
const [version, models] = await Promise.all([
probeBin('lms', ['version']),
// /api/v0 is LM Studio's own API and carries far more than the
// OpenAI-compatible /v1: quantisation, context, load state.
probeHttp('http://127.0.0.1:1234/api/v0/models'),
]);
if (!version && !models) return { installed: false, serving: false };
// `lms version` prints an ANSI banner; the semver is inside it.
const sem = version && version.match(/(\d+\.\d+\.\d+)/);
const list = Array.isArray(models?.data) ? models.data : [];
return {
installed: Boolean(version) || Boolean(models),
serving: Boolean(models),
version: sem ? sem[1] : null,
models: list.map((m) => ({
ref: m.id,
quant: m.quantization ?? null,
family: m.arch ?? null,
contextLength: m.max_context_length ?? null,
loaded: m.state === 'loaded',
kind: m.type ?? null,
})),
};
},
async probeDetails({ state }) {
// `lms ls --json` adds on-disk size and parameter strings that the
// HTTP API omits. It is optional: if the CLI is not resolvable the
// page still renders from the HTTP data.
const [lsRaw, api] = await Promise.all([
probeBin('lms', ['ls', '--json'], { timeout: 6000 }),
probeHttp('http://127.0.0.1:1234/api/v0/models'),
]);
let byKey = new Map();
try {
const arr = JSON.parse(lsRaw ?? '[]');
byKey = new Map(arr.map((m) => [m.modelKey, m]));
} catch { /* CLI absent or shape changed — HTTP data still stands */ }
const list = Array.isArray(api?.data) ? api.data : [];
const models = list.map((m) => {
const extra = byKey.get(m.id) ?? {};
return {
ref: m.id,
quant: m.quantization ?? extra.quantization?.name ?? null,
params: extra.paramsString ?? null,
family: m.arch ?? extra.architecture ?? null,
contextLength: m.max_context_length ?? null,
sizeBytes: extra.sizeBytes ?? null,
loaded: m.state === 'loaded',
kind: m.type ?? null,
capabilities: m.capabilities ?? [],
};
});
const totalBytes = models.reduce((a, m) => a + (m.sizeBytes ?? 0), 0);
const loaded = models.filter((m) => m.loaded).length;
const vision = models.filter((m) => extraHasVision(byKey.get(m.ref))).length;
const sections = [];
sections.push({
id: 'capability', title: 'What it can do',
emptyState: 'Start the server to read capabilities.',
rows: [
{ label: 'Loaded vs downloaded', value: `${loaded} loaded of ${models.length} downloaded` },
models.length
? { label: 'Largest context available', value: `${Math.max(...models.map((m) => m.contextLength ?? 0)).toLocaleString()} tokens` }
: null,
vision ? { label: 'Vision-capable models', value: String(vision) } : null,
{ label: 'OpenAI-compatible API', value: 'http://127.0.0.1:1234/v1' },
].filter(Boolean),
});
sections.push({
id: 'storage', title: 'Storage',
emptyState: 'No models downloaded.',
rows: [
totalBytes ? { label: 'Disk used by models', value: fmtBytes(totalBytes) } : null,
{ label: 'Models directory', value: home('.lmstudio', 'models') },
].filter(Boolean),
});
return {
sections,
models,
actions: [
{ label: 'List downloaded models', command: 'lms ls' },
{ label: 'Show what is loaded', command: 'lms ps' },
],
};
},
},
{
id: 'vllm',
name: 'vLLM',
blurb: 'Throughput-oriented server for safetensors on CUDA or ROCm.',
formats: ['safetensors'],
endpoint: 'http://127.0.0.1:8000',
// vLLM has no usable macOS path; it wants a discrete accelerator.
supported: (hw) => process.platform !== 'darwin' && hasAccelerator(hw),
install: () => 'pip install vllm',
pull: (ref) => `huggingface-cli download ${ref}`,
run: (ref) => `vllm serve ${ref}`,
async detect() {
const [mod, served] = await Promise.all([
// Reading package metadata is ~10x cheaper than importing vllm
// (measured: 2688 ms for a real import), and this runs on every scan.
probePythonMeta('vllm'),
probeHttp('http://127.0.0.1:8000/v1/models'),
]);
if (!mod && !served) return { installed: false, serving: false };
return {
installed: Boolean(mod),
serving: Boolean(served),
version: mod?.version ?? null,
python: mod?.python ?? null,
models: Array.isArray(served?.data) ? served.data.map((m) => ({ ref: m.id, loaded: true })) : [],
};
},
async probeDetails({ state }) {
// vLLM only matters if this machine can actually run it, so the
// page leads with the GPU verdict rather than the version.
const gpu = await probeTorchCuda();
const sections = [];
sections.push({
id: 'capability', title: 'Can this machine run it',
emptyState: 'Could not query torch.',
rows: gpu ? [
{ label: 'CUDA available', value: gpu.cudaAvailable ? 'Yes' : 'No' },
gpu.deviceName ? { label: 'GPU', value: gpu.deviceName } : null,
gpu.capability ? { label: 'Compute capability', value: gpu.capability } : null,
gpu.totalMiB ? { label: 'VRAM', value: `${gpu.freeMiB ?? '?'} MiB free of ${gpu.totalMiB} MiB` } : null,
gpu.torch ? { label: 'torch', value: gpu.torch } : null,
gpu.bf16 != null ? { label: 'bfloat16', value: gpu.bf16 ? 'Supported' : 'Not supported' } : null,
].filter(Boolean) : [],
});
sections.push({
id: 'config', title: 'Configuration',
emptyState: 'Not installed.',
rows: [
state.python ? { label: 'Python interpreter', value: state.python } : null,
{ label: 'Server endpoint', value: 'http://127.0.0.1:8000/v1 (when started)' },
...safeEnvRows().filter((r) => r.label === 'CUDA_VISIBLE_DEVICES'),
].filter(Boolean),
});
return {
sections,
models: state.models ?? [],
actions: [{ label: 'Serve a model', command: 'vllm serve <model> --port 8000' }],
gaps: (state.models ?? []).length ? [] : [
'vLLM has no local model registry. Nothing is listed until a server is running, and it downloads weights from Hugging Face on first serve.',
],
};
},
},
{
id: 'mlx',
name: 'MLX-LM',
blurb: 'Apple Silicon native. Uses unified memory, not VRAM.',
formats: ['mlx', 'safetensors'],
endpoint: null,
supported: () => isAppleSilicon(),
install: () => 'pip install mlx-lm',
pull: (ref) => `huggingface-cli download ${ref}`,
run: (ref) => `mlx_lm.generate --model ${ref} --prompt "hello"`,
async detect() {
if (!isAppleSilicon()) return { installed: false, serving: false, reason: 'apple-silicon-only' };
const mod = await probePythonModule('mlx_lm', 'mlx_lm.version.__version__');
if (!mod) return { installed: false, serving: false };
return { installed: true, serving: false, version: mod.version, models: [] };
},
},
{
id: 'transformers',
name: 'Transformers',
blurb: 'Reference PyTorch path. Widest model coverage, slowest to start.',
formats: ['safetensors'],
endpoint: null,
supported: () => true,
install: () => 'pip install transformers torch',
pull: (ref) => `huggingface-cli download ${ref}`,
run: (ref) => `python -c "from transformers import pipeline; print(pipeline('text-generation', '${ref}')('hello'))"`,
async detect() {
const mod = await probePythonMeta('transformers');
if (!mod) return { installed: false, serving: false };
return {
installed: true, serving: false,
version: mod.version, python: mod.python, models: [],
};
},
async probeDetails({ state }) {
const [gpu, accelerate, cache] = await Promise.all([
probeTorchCuda(),
probePythonMeta('accelerate'),
// The HF cache is the closest thing this library has to an
// inventory. Scanning it is the expensive part of this page.
(async () => {
const root = process.env.HF_HUB_CACHE
|| (process.env.HF_HOME ? path.join(process.env.HF_HOME, 'hub') : null)
|| home('.cache', 'huggingface', 'hub');
let entries;
try { entries = await fsp.readdir(root, { withFileTypes: true }); }
catch { return { root, repos: [], missing: true }; }
const repos = [];
for (const e of entries) {
if (!e.isDirectory() || !e.name.startsWith('models--')) continue;
const full = path.join(root, e.name);
const { bytes } = await dirSize(full, { maxEntries: 4000 });
repos.push({
ref: e.name.replace(/^models--/, '').replace(/--/g, '/'),
sizeBytes: bytes,
});
}
return { root, repos: repos.sort((a, b) => b.sizeBytes - a.sizeBytes) };
})(),
]);
const sections = [];
// Without torch this is a tokenizer library, which is the single
// most useful thing to tell someone who expects to run a model.
sections.push({
id: 'capability', title: 'What it can do',
emptyState: 'Could not query the environment.',
rows: [
gpu?.torch
? { label: 'torch', value: gpu.torch }
: {
label: 'torch', value: 'Not installed',
hint: 'Without torch, transformers can only do tokenizers and config — it cannot run a model.',
},
gpu ? { label: 'CUDA available', value: gpu.cudaAvailable ? 'Yes' : 'No' } : null,
gpu?.deviceName ? { label: 'GPU', value: gpu.deviceName } : null,
gpu?.totalMiB ? { label: 'VRAM', value: `${gpu.freeMiB ?? '?'} MiB free of ${gpu.totalMiB} MiB` } : null,
{
label: 'accelerate',
value: accelerate ? accelerate.version : 'Not installed',
hint: accelerate ? null : 'device_map="auto" and multi-GPU sharding need accelerate.',
},
].filter(Boolean),
});
sections.push({
id: 'storage', title: 'Storage',
emptyState: 'No Hugging Face cache found.',
rows: cache.missing ? [] : [
{ label: 'Cache directory', value: cache.root },
{ label: 'Cached repositories', value: String(cache.repos.length) },
{
label: 'Cache size',
value: fmtBytes(cache.repos.reduce((a, r) => a + r.sizeBytes, 0)) ?? '0',
},
],
});
sections.push({
id: 'config', title: 'Configuration',
emptyState: 'No overrides set.',
rows: [
state.python ? { label: 'Python interpreter', value: state.python } : null,
...safeEnvRows().filter((r) => r.label.startsWith('HF_')),
].filter(Boolean),
});
return {
sections,
models: cache.repos.map((r) => ({ ref: r.ref, sizeBytes: r.sizeBytes })),
actions: [{ label: 'Install torch for GPU inference', command: 'pip install torch' }],
gaps: [
'Cached repositories are not necessarily runnable — the cache also holds tokenizers, datasets and non-transformers formats.',
],
};
},
},
];
function hasAccelerator(hw = {}) {
const vram = Number(hw?.gpu?.vramGB ?? hw?.summary?.vramGB ?? 0);
const backend = String(hw?.summary?.backend ?? hw?.gpu?.backend ?? '').toLowerCase();
return vram > 0 || backend.includes('cuda') || backend.includes('rocm');
}
const BY_ID = new Map(RUNTIMES.map((r) => [r.id, r]));
/** Aliases the CLI and the registry use for the same runtime. */
const ALIASES = new Map([
['llamacpp', 'llama.cpp'],
['llama-cpp', 'llama.cpp'],
['llama_cpp', 'llama.cpp'],
['lm-studio', 'lmstudio'],
['lmstudio.ai', 'lmstudio'],
['mlx-lm', 'mlx'],
['mlx_lm', 'mlx'],
['hf', 'transformers'],
['huggingface', 'transformers'],
['pytorch', 'transformers'],
]);
/**
* Unlike the CLI's normalizeRuntime, this never silently rewrites an unknown
* runtime to 'ollama' — it returns null so the caller can decide.
*/
function normalize(runtime) {
if (!runtime) return null;
const key = String(runtime).trim().toLowerCase();
if (key === 'auto' || key === 'all' || key === '*') return 'auto';
if (BY_ID.has(key)) return key;
return ALIASES.get(key) ?? null;
}
function get(runtime) {
const id = normalize(runtime);
return id && id !== 'auto' ? BY_ID.get(id) : null;
}
/** Runtimes that could run on this hardware at all, installed or not. */
function eligible(hardware = {}) {
return RUNTIMES.filter((r) => r.supported(hardware));
}
/**
* Detect every eligible runtime concurrently. The whole sweep costs about as
* much as the slowest single probe (~2.5 s worst case), not the sum.
*/
async function detectAll(hardware = {}) {
const list = eligible(hardware);
const results = await Promise.all(
list.map(async (r) => {
let state;
try {
state = await r.detect();
} catch (err) {
state = { installed: false, serving: false, error: err?.message ?? String(err) };
}
return {
id: r.id,
name: r.name,
blurb: r.blurb,
formats: r.formats,
endpoint: r.endpoint,
installCommand: r.install(),
...state,
};
})
);
// Installed first, then serving, then alphabetical — the order the UI wants.
return results.sort(
(a, b) =>
Number(b.installed) - Number(a.installed) ||
Number(b.serving) - Number(a.serving) ||
a.name.localeCompare(b.name)
);
}
/**
* Pick the runtime for a catalog artifact. Prefers something already
* installed that can handle the artifact's format, and falls back to the
* best format match so the UI can still show a real install command.
*/
function chooseFor(artifact = {}, detected = [], hardware = {}) {
const format = String(artifact.format ?? '').toLowerCase();
const eligibleIds = new Set(eligible(hardware).map((r) => r.id));
const canRun = (r) => eligibleIds.has(r.id) && (!format || r.formats.includes(format));
const installedIds = new Set(detected.filter((d) => d.installed).map((d) => d.id));
const servingIds = new Set(detected.filter((d) => d.serving).map((d) => d.id));
const candidates = RUNTIMES.filter(canRun);
if (!candidates.length) return null;
const rank = (r) =>
(servingIds.has(r.id) ? 4 : 0) + (installedIds.has(r.id) ? 2 : 0) + (r.id === 'ollama' ? 1 : 0);
return candidates.reduce((best, r) => (rank(r) > rank(best) ? r : best), candidates[0]);
}
/**
* Deep, per-runtime data for its detail view.
*
* Kept separate from detect() on purpose: detect() runs on every scan and must
* stay cheap, while these probes are only worth paying for when the user
* actually opens that runtime's page. Each runtime supplies its own
* `probeDetails`; anything missing degrades to the detect() summary rather
* than inventing rows.
*/
async function details(runtime, hardware = {}) {
const r = get(runtime);
if (!r) return null;
const base = {
id: r.id,
name: r.name,
blurb: r.blurb,
formats: r.formats,
endpoint: r.endpoint,
installCommand: r.install(),
supported: r.supported(hardware),
sections: [],
models: [],
actions: [],
errors: [],
};
let state;
try {
state = await r.detect();
} catch (err) {
base.errors.push(`detect failed: ${err?.message ?? err}`);
state = { installed: false, serving: false };
}
Object.assign(base, state);
if (typeof r.probeDetails === 'function') {
try {
const extra = await r.probeDetails({ hardware, state });
if (extra?.sections) base.sections.push(...extra.sections);
if (extra?.models) base.models = extra.models;
if (extra?.actions) base.actions.push(...extra.actions);
if (extra?.errors) base.errors.push(...extra.errors);
} catch (err) {
base.errors.push(`details failed: ${err?.message ?? err}`);
}
}
return base;
}
/** Commands for a given (runtime, modelRef) pair, ready to show or copy. */
function commandsFor(runtime, ref) {
const r = get(runtime);
if (!r) return null;
return {
runtime: r.id,
name: r.name,
install: r.install(),
pull: typeof r.pull === 'function' ? r.pull(ref) : null,
run: typeof r.run === 'function' ? r.run(ref) : null,
serve: typeof r.serve === 'function' ? r.serve(ref) : null,
};
}
module.exports = {
RUNTIMES,
normalize,
get,
eligible,
detectAll,
details,
chooseFor,
commandsFor,
hasAccelerator,
_internal: { probe, probeHttp, probePythonModule },
};

View file

@ -0,0 +1,204 @@
/* ============================================================
Skin: AgenC Desktop — tokens taken from that app's own
styles.css (dark theme + glass) and its Figma-exported icons.
dark bg #191a20 · surface #15161b · wrapper #101116
text #f2f3f7 · secondary #8a91a1 · border #2b2e37
accent brand #f0a93c · ok #16a34a · warn #f59e0b · err #e5484d
glass rgba(28,30,38,.55) · border rgba(255,255,255,.09)
radii window/surface 20 · control 14 · compact 10 · micro 6
dims sidebar 260 · rows 34h · pad 7/12 · font 13 · chrome 44
Committed dark theme: that is the AgenC look being matched.
============================================================ */
@font-face {
font-family: 'Inter';
src: url('assets/fonts/InterVariable.woff2') format('woff2-variations');
font-weight: 100 900; font-style: normal; font-display: block;
}
:root {
--bg:#191a20; --surface:#15161b; --wrapper:#101116;
--text:#f2f3f7; --text-2:#8a91a1;
--border:#2b2e37; --border-h:#3a3e4a;
--hover:rgba(255,255,255,.06); --section:rgba(255,255,255,.032);
--menu:#22242c; --brand:#f0a93c; --brand-soft:rgba(240,169,60,.24);
--ok:#16a34a; --warn:#f59e0b; --err:#e5484d;
--glass-border:rgba(255,255,255,.09); --hairline:rgba(255,255,255,.08);
--shadow-wrap:0 0 20px rgba(0,0,0,.25); --shadow-lift:0 10px 30px rgba(0,0,0,.45);
--r-window:20px; --r-surface:20px; --r-control:14px; --r-compact:10px; --r-micro:6px;
--chrome-h:44px;
--sans:-apple-system,BlinkMacSystemFont,'Inter',system-ui,'Segoe UI',Roboto,sans-serif;
--mono:ui-monospace,'SF Mono','JetBrains Mono',Menlo,Consolas,monospace;
--ease:cubic-bezier(.2,.8,.2,1);
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; overflow: hidden; }
body {
font-family: var(--sans); font-size: 13px; line-height: 16px; letter-spacing: -.078px;
color: var(--text); background: transparent;
-webkit-font-smoothing: antialiased; user-select: none;
}
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: default; letter-spacing: inherit; }
button:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; border-radius: var(--r-micro); }
/* Figma-exported stroke icons ship with baked colours, so they are tinted
through a CSS mask instead of being recoloured by hand. */
.icn {
display: inline-block; width: 16px; height: 16px; flex: none; background: currentColor;
-webkit-mask: var(--i) center/contain no-repeat; mask: var(--i) center/contain no-repeat;
}
/* ---- one window: glass sidebar fused to a deep-black main pane ---- */
.app {
display: grid; grid-template-columns: 260px minmax(0,1fr); grid-template-rows: minmax(0,1fr);
height: 100vh; border-radius: var(--r-window); background: #0d0e12;
border: 1px solid var(--glass-border); box-shadow: var(--shadow-lift); overflow: hidden;
}
.vsep { display: none; }
body.mac .app { border-radius: 0; border: 0; box-shadow: none; }
.sb {
display: flex; flex-direction: column; min-height: 0;
background: rgba(22,23,29,.72);
backdrop-filter: blur(40px) saturate(160%); -webkit-backdrop-filter: blur(40px) saturate(160%);
border-right: 1px solid var(--hairline); padding: 14px 12px 12px; overflow: clip;
}
.dragstrip { display: none; }
body.mac .dragstrip { display: block; height: 30px; flex-shrink: 0; -webkit-app-region: drag; }
.apptitle { font-size: 17px; font-weight: 700; letter-spacing: -.2px; padding: 6px 12px 14px; -webkit-app-region: drag; }
.sb-scroll { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.sb-scroll::-webkit-scrollbar { width: 0; }
.sb-head { font-size: 12px; color: var(--text-2); padding: 16px 12px 6px; }
.srow {
height: 34px; flex: none; display: flex; align-items: center; gap: 8px;
padding: 7px 12px; border-radius: var(--r-compact); color: var(--text); text-align: left; width: 100%;
}
.srow .icn { color: var(--text-2); }
.srow .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--border-h); margin: 0 4.5px; }
.srow .dot.ok { background: var(--ok); }
.srow .dot.warn { background: var(--warn); }
.srow .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srow .ct { font-size: 11px; color: var(--text-2); font-variant-numeric: tabular-nums; }
.srow:hover { background: var(--hover); }
.srow[aria-current="true"] { background: var(--hover); }
.srow[aria-current="true"] .icn { color: var(--brand); }
.mn { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: #0d0e12; overflow: hidden; }
.tb {
height: var(--chrome-h); flex-shrink: 0; display: flex; align-items: center; gap: 10px;
padding: 0 10px 0 16px; border-bottom: 1px solid var(--section); -webkit-app-region: drag;
}
.tb > * { -webkit-app-region: no-drag; }
.tb .sp { flex: 1; -webkit-app-region: drag; }
.tb-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.tb-title .t { font-size: 14px; font-weight: 600; }
.tb-title .s { font-size: 12px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.seg { display: flex; height: 28px; padding: 2px; gap: 2px; border: 1px solid var(--border); border-radius: var(--r-control); background: var(--section); }
.seg button { padding: 0 12px; font-size: 12px; color: var(--text-2); border-radius: var(--r-compact); }
.seg button[aria-pressed="true"] { background: var(--brand-soft); color: var(--text); }
.search {
display: flex; align-items: center; gap: 7px; width: 170px; height: 28px; flex: none;
border: 1px solid var(--border); border-radius: var(--r-control); background: var(--section); padding: 0 10px;
}
.search::before {
content: ''; width: 13px; height: 13px; flex: none; background: var(--text-2);
-webkit-mask: url('assets/icons/icon-ai-search.svg') center/contain no-repeat;
mask: url('assets/icons/icon-ai-search.svg') center/contain no-repeat;
}
.search input { width: 100%; min-width: 0; border: 0; background: none; outline: 0; font: 400 13px/16px var(--sans); color: var(--text); }
.search input::placeholder { color: var(--text-2); }
.search:focus-within { border-color: var(--border-h); }
.winctl { display: flex; gap: 2px; }
.winctl button { width: 32px; height: 28px; border-radius: var(--r-micro); display: grid; place-items: center; color: var(--text-2); }
.winctl button:hover { background: var(--hover); color: var(--text); }
.winctl #w-close:hover { background: var(--err); color: #fff; }
.g { position: relative; width: 10px; height: 10px; display: block; }
.g-min::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; border-top: 1.2px solid currentColor; }
.g-max::before { content: ''; position: absolute; inset: 0; border: 1.2px solid currentColor; border-radius: 2px; }
.g-close::before, .g-close::after { content: ''; position: absolute; left: -1px; right: -1px; top: 50%; border-top: 1.2px solid currentColor; }
.g-close::before { transform: rotate(45deg); }
.g-close::after { transform: rotate(-45deg); }
.content { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 16px 26px; }
.content::-webkit-scrollbar { width: 0; }
.gtitle { font-size: 12px; color: var(--text-2); padding: 14px 6px 6px; }
.gtitle:first-child { padding-top: 0; }
.group { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-control); overflow: hidden; }
.row { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 8px 12px; border-bottom: 1px solid var(--section); }
.row:last-child { border-bottom: 0; }
.row:hover { background: var(--hover); }
.row .fit { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.row .fit.fits { background: var(--ok); }
.row .fit.tight { background: var(--warn); }
.row .fit.over { background: var(--err); }
.row .info { flex: 1; min-width: 0; }
.row .t1 { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .t1 span { font-weight: 400; color: var(--text-2); margin-left: 6px; }
.row .t2 { font-size: 12px; line-height: 15px; color: var(--text-2); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.row .sz { font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.row.dis .t1, .row.dis .sz { opacity: .45; }
.btn { height: 26px; padding: 0 12px; flex: none; border-radius: var(--r-compact); background: var(--brand); color: #191a20; font-size: 12px; font-weight: 600; }
.btn:hover { filter: brightness(1.06); }
.btn:active { transform: scale(.97); }
.btn.sec { background: var(--section); color: var(--text); border: 1px solid var(--border); font-weight: 500; }
.btn[disabled] { opacity: .35; }
.kv { display: flex; justify-content: space-between; gap: 16px; min-height: 38px; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--section); }
.kv:last-child { border-bottom: 0; }
.kv .k { color: var(--text); }
.kv .v { color: var(--text-2); text-align: right; font-variant-numeric: tabular-nums; }
.cmd { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-bottom: 1px solid var(--section); }
.cmd:last-child { border-bottom: 0; }
.cmd .info { flex: 1; min-width: 0; }
.cmd .t1 { font-weight: 500; }
.cmd code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 12px/16px var(--mono); color: var(--text-2); }
.phases { display: flex; flex-direction: column; gap: 9px; max-width: 340px; padding: 12px 6px; }
.phase { display: flex; align-items: center; gap: 10px; font-size: 12.5px; color: var(--text-2); }
.phase i { width: 7px; height: 7px; border-radius: 50%; background: var(--border); flex: none; }
.phase.done i { background: var(--ok); }
.phase.busy i { background: var(--brand); animation: pulse 1s ease-in-out infinite; }
.phase .t { margin-left: auto; font-size: 11px; font-variant-numeric: tabular-nums; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
.errbox { margin: 6px; padding: 12px 14px; border-radius: var(--r-control); background: rgba(229,72,77,.14); border: 1px solid var(--err); }
.errbox b { display: block; margin-bottom: 3px; font-weight: 600; }
.toast {
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%) translateY(70px);
background: var(--menu); color: var(--text); border: 1px solid var(--border);
font-size: 12px; font-weight: 500; border-radius: var(--r-compact); padding: 7px 14px;
box-shadow: var(--shadow-lift); transition: transform .25s var(--ease); z-index: 50; pointer-events: none;
}
.toast.on { transform: translateX(-50%) translateY(0); }
/* action results — compact table rendered inline under the button that ran it */
.row.result { background: var(--section); align-items: flex-start; }
table.mini { width: 100%; border-collapse: collapse; font-size: 12px; }
table.mini th { text-align: left; font-weight: 600; color: var(--text-2); padding: 3px 12px 3px 0; white-space: nowrap; }
table.mini td { padding: 3px 12px 3px 0; color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; }
table.mini td:first-child { color: var(--text-2); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}
/* provenance chip — a measured score and a guessed one must not look alike */
.prov {
font-size: 10px; font-weight: 600; letter-spacing: .02em;
padding: 2px 7px; border-radius: var(--r-micro); white-space: nowrap; flex: none;
}
.prov.meas { color: var(--ok); background: rgba(22,163,74,.14); }
.prov.est { color: var(--text-2); background: var(--section); }
.fit.unknown { background: transparent; border: 1px dashed currentColor; }

593
desktop/src/renderer/app.js Normal file
View file

@ -0,0 +1,593 @@
'use strict';
/**
* Renderer for the macOS 12 skin. Talks to the main process only through
* window.llm (see preload). No node, no network.
*/
const $ = (sel, root = document) => root.querySelector(sel);
const el = (tag, cls, text) => {
const n = document.createElement(tag);
if (cls) n.className = cls;
if (text != null) n.textContent = text;
return n;
};
const state = {
core: null,
// recommended | installed | settings | runtime:<id>
view: 'recommended',
fit: 'all', // all | fits
query: '',
// Per-runtime deep probes, fetched lazily on first visit and cached here
// so switching back is instant. { [id]: {loading, data, error} }
details: {},
};
/* ------------------------------------------------------------------ */
/* data */
/* ------------------------------------------------------------------ */
const groups = () => state.core?.groups ?? [];
const budget = () => state.core?.budgetGB ?? null;
const gb = (n) => (Number.isFinite(n) ? `${n.toFixed(1).replace(/\.0$/, '')} GB` : '—');
function visible(m) {
if (state.fit === 'fits' && m.fit !== 'fits') return false;
if (!state.query) return true;
const q = state.query.toLowerCase();
return [m.name, m.ref, m.family, m.quant]
.some((v) => v && String(v).toLowerCase().includes(q));
}
/* ------------------------------------------------------------------ */
/* toast + clipboard */
/* ------------------------------------------------------------------ */
let toastTimer = null;
function toast(msg) {
const t = $('#toast');
t.textContent = msg;
t.classList.add('on');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('on'), 1400);
}
async function copy(text, label = 'Copied') {
if (!text) return;
await window.llm.copy(text);
toast(label);
}
/* ------------------------------------------------------------------ */
/* sidebar */
/* ------------------------------------------------------------------ */
function renderSidebarRuntimes(s) {
const host = $('#sb-runtimes');
host.textContent = '';
for (const r of s.runtimes ?? []) {
const b = el('button', 'srow');
b.title = r.blurb;
const dot = el('span', 'dot');
if (r.serving) dot.classList.add('ok');
else if (r.installed) dot.classList.add('warn');
b.append(dot, el('span', 'nm', r.name));
// "0.3.0-dev" → "0.3.0"; long semvers stay readable in the 260px rail
if (r.version) b.append(el('span', 'ct', r.version.split('-')[0].slice(0, 10)));
b.setAttribute('data-view', `runtime:${r.id}`);
b.addEventListener('click', () => switchView(`runtime:${r.id}`));
host.append(b);
}
markCurrent();
}
/** Reflect state.view on whichever sidebar row owns it. */
function markCurrent() {
document.querySelectorAll('.srow[data-view]').forEach((b) => {
b.setAttribute('aria-current', String(b.dataset.view === state.view));
});
}
function renderCounts(s) {
const counts = {
recommended: (s.groups ?? []).reduce((a, g) => a + g.models.length, 0) || null,
installed: (s.runtimes ?? []).reduce((a, r) => a + (r.models?.length ?? 0), 0),
};
for (const [k, v] of Object.entries(counts)) {
const n = document.querySelector(`[data-count="${k}"]`);
if (n) n.textContent = v == null ? '—' : String(v);
}
}
/* ------------------------------------------------------------------ */
/* header */
/* ------------------------------------------------------------------ */
function renderHeader(s) {
const labels = { recommended: 'Recommended', installed: 'Installed', settings: 'Settings' };
let heading = labels[state.view];
if (!heading && state.view.startsWith('runtime:')) {
const id = state.view.slice(8);
heading = (s.runtimes ?? []).find((r) => r.id === id)?.name ?? id;
}
$('#title').textContent = heading ?? 'LLM Checker';
const hw = s.hardware;
const t = s.tally ?? {};
let sub = 'Scanning…';
if (s.phase === 'error') sub = 'Scan failed';
else if (s.phase === 'ready' && hw) {
const gpu = (hw.gpuModel ?? hw.cpuModel ?? '').replace(/^NVIDIA GeForce\s+/i, '');
const parts = [gpu, hw.vramGB && `${hw.vramGB} GB`].filter(Boolean).join(' · ');
const fits = [t.fits && `${t.fits} fit`, t.tight && `${t.tight} tight`, t.over && `${t.over} over`, t.unknown && `${t.unknown} unknown`]
.filter(Boolean).join(' · ');
sub = [parts, fits].filter(Boolean).join(' — ');
}
$('#subtitle').textContent = sub;
// The fit filter and search only mean something on the model list.
$('#seg-fit').style.visibility = state.view === 'recommended' ? 'visible' : 'hidden';
$('#search').parentElement.style.visibility =
state.view === 'recommended' || state.view === 'installed' ? 'visible' : 'hidden';
}
/* ------------------------------------------------------------------ */
/* content blocks */
/* ------------------------------------------------------------------ */
function modelRow(m) {
const row = el('div', 'row');
if (!visible(m)) row.style.display = 'none';
if (m.fit === 'over') row.classList.add('dis');
const fitIndicator = el('span', `fit ${m.fit}`);
fitIndicator.title = m.fit === 'unknown' ? 'Memory compatibility unknown' : `Memory: ${m.fit}`;
fitIndicator.setAttribute('aria-label', fitIndicator.title);
row.append(fitIndicator);
const info = el('div', 'info');
const t1 = el('div', 't1', m.name);
if (m.params || m.quant) t1.append(el('span', null, [m.params, m.quant].filter(Boolean).join(' · ')));
info.append(t1);
if (m.context?.limited) info.append(el('div', 't2', m.context.native ? `Context limited to ${m.context.effective.toLocaleString()} tokens` : 'Native context unverified'));
if (m.purpose) info.append(el('div', 't2', m.purpose));
row.append(info);
// Provenance chip. A measured score and an estimated one must never look
// like the same kind of number — 77% of the catalog is estimated.
const q = m.quality ?? { measured: false };
const chip = el('span', `prov ${q.measured ? 'meas' : 'est'}`);
if (q.measured) {
chip.textContent = q.sizeUnknown ? 'measured · family' : 'measured';
chip.title = [
`${q.metric} = ${q.rawScore}`,
`source: ${q.source}`,
q.independent ? 'independently run' : 'vendor self-reported',
q.sizeUnknown ? 'the board published no size, so this scores the family, not this exact build' : null,
'benchmarks are run at fp16; a quantization penalty is applied',
].filter(Boolean).join('\n');
} else {
chip.textContent = 'estimated';
chip.title = `No benchmark for this model. Quality inferred from ${q.basis ?? 'parameter count'}.`;
}
row.append(chip);
row.append(el('span', 'sz', gb(m.sizeGB)));
const btn = el('button', 'btn', 'Install');
btn.title = m.commands?.pull ?? '';
if (m.fit === 'over' || !m.commands?.pull) btn.disabled = true;
btn.addEventListener('click', () =>
copy(m.commands?.pull ?? m.commands?.run, 'Install command copied'));
row.append(btn);
return row;
}
function phasesBlock(s) {
const p = el('div', 'phases');
const order = [
['hardware', 'Detecting hardware'],
['runtimes', 'Probing local runtimes'],
['models', 'Scoring the catalog'],
];
const reached = { idle: -1, hardware: 0, runtimes: 1, models: 2, ready: 3, error: 3 }[s.phase] ?? -1;
order.forEach(([key, label], i) => {
const row = el('div', `phase ${i < reached ? 'done' : i === reached ? 'busy' : ''}`);
row.append(el('i'), el('span', null, label));
const t = s.timings?.[key];
if (Number.isFinite(t)) row.append(el('span', 't', `${(t / 1000).toFixed(1)} s`));
p.append(row);
});
return p;
}
/* ---------------- views ---------------- */
function viewRecommended(host, s) {
if (s.phase !== 'ready') { host.append(phasesBlock(s)); return; }
for (const g of groups()) {
const shown = g.models.filter(visible);
if (!shown.length && (state.query || state.fit !== 'all')) continue;
host.append(el('div', 'gtitle', `${g.label} — ${g.evaluated ? g.evaluated.toLocaleString() : ''} evaluated`));
const grp = el('div', 'group');
g.models.forEach((m) => grp.append(modelRow(m)));
host.append(grp);
}
}
function viewInstalled(host, s) {
for (const r of s.runtimes ?? []) {
host.append(el('div', 'gtitle',
`${r.name} — ${r.serving ? 'serving' : r.installed ? 'installed' : 'not installed'}`));
const grp = el('div', 'group');
if (r.models?.length) {
for (const m of r.models) {
const row = el('div', 'row');
row.append(el('span', 'fit fits'));
const info = el('div', 'info');
info.append(el('div', 't1', m.ref));
if (m.quant || m.params) {
info.append(el('div', 't2', [m.params, m.quant].filter(Boolean).join(' · ')));
}
row.append(info);
row.append(el('span', 'sz', m.sizeBytes ? gb(m.sizeBytes / 1e9) : ''));
grp.append(row);
}
} else if (r.installed) {
// Installed but nothing loaded — offering an install command here
// would contradict the status it sits under.
const row = el('div', 'row');
const info = el('div', 'info');
info.append(el('div', 't1', r.serving ? 'Running — no models loaded' : 'Installed — not running'));
info.append(el('div', 't2', r.blurb));
row.append(info);
grp.append(row);
} else {
const cmd = el('div', 'cmd');
cmd.append(el('code', null, r.installCommand));
const b = el('button', 'btn sec', 'Copy');
b.addEventListener('click', () => copy(r.installCommand, 'Install command copied'));
cmd.append(b);
grp.append(cmd);
}
host.append(grp);
}
}
async function viewSettings(host, s) {
const hw = s.hardware ?? {};
const rows = [
['Graphics card', hw.gpuModel],
['Video memory', hw.vramGB && `${hw.vramGB} GB`],
['Backend', hw.backend],
['CUDA', hw.cudaVersion && `${hw.cudaVersion} · driver ${hw.driver ?? '—'}`],
['Processor', hw.cpuModel],
['Cores', hw.cores && `${hw.cores}${hw.physicalCores ? ` (${hw.physicalCores} physical)` : ''}`],
['Instruction set', hw.simd],
['System memory', hw.ramGB && `${hw.ramGB} GB`],
['Tier', hw.tier],
['Largest model', hw.maxModelGB && `${hw.maxModelGB} GB`],
['Fingerprint', hw.fingerprint],
['Platform', `${hw.platform ?? ''} ${hw.arch ?? ''}`.trim() || null],
].filter(([, v]) => v);
host.append(el('div', 'gtitle', 'Detected hardware'));
const grp = el('div', 'group');
for (const [k, v] of rows) {
const row = el('div', 'kv');
row.append(el('span', 'k', k), el('span', 'v', String(v)));
grp.append(row);
}
host.append(grp);
host.append(el('div', 'gtitle', 'Actions'));
const act = el('div', 'group');
const row = el('div', 'row');
const info = el('div', 'info');
info.append(el('div', 't1', 'Rescan this machine'));
info.append(el('div', 't2', 'Re-detect hardware, runtimes and the catalog'));
row.append(info);
const b = el('button', 'btn', 'Rescan');
b.addEventListener('click', async () => {
toast('Rescanning…');
try { await window.llm.scan(); } catch { toast('Scan failed'); }
});
row.append(b);
act.append(row);
host.append(act);
// Real actions, not commands to copy. The app exists so none of this
// needs a terminal, so each one runs in-process and renders its result
// right here.
host.append(el('div', 'gtitle', 'Tools'));
const adv = el('div', 'group');
let actions = [];
try { actions = await window.llm.listActions(); } catch { /* rendered empty below */ }
for (const a of actions) {
const row = el('div', 'row');
const inf = el('div', 'info');
inf.append(el('div', 't1', a.label));
inf.append(el('div', 't2', `${a.blurb} · ${a.estimate}`));
row.append(inf);
const btn = el('button', 'btn', a.destructive ? 'Run…' : 'Run');
btn.addEventListener('click', async () => {
if (btn.disabled) return;
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'Running…';
const out = await window.llm.runAction(a.id);
btn.disabled = false;
btn.textContent = original;
// Replace any previous result for this action.
row.nextElementSibling?.classList.contains('result') && row.nextElementSibling.remove();
const res = el('div', 'row result');
if (!out.ok) {
res.append(el('div', 'info', `Failed: ${out.error}`));
} else if (out.kind === 'table') {
const box = el('div', 'info');
const tbl = el('table', 'mini');
const head = el('tr');
out.columns.forEach((c) => head.append(el('th', null, c)));
tbl.append(head);
out.rows.forEach((r) => {
const tr = el('tr');
r.forEach((c) => tr.append(el('td', null, String(c))));
tbl.append(tr);
});
box.append(tbl);
res.append(box);
} else {
res.append(el('div', 'info', out.text ?? 'Done.'));
}
row.after(res);
toast(`${a.label} — ${out.ms} ms`);
});
row.append(btn);
adv.append(row);
}
host.append(adv);
host.append(el('div', 'gtitle', 'About'));
const about = el('div', 'group');
try {
const inf = await window.llm.appInfo();
for (const [k, v] of [
['Version', inf.version],
['Electron', inf.electron],
['Node', inf.node],
['Platform', `${inf.platform} ${inf.arch}`],
]) {
const kv = el('div', 'kv');
kv.append(el('span', 'k', k), el('span', 'v', String(v)));
about.append(kv);
}
} catch { /* about block is best-effort */ }
host.append(about);
}
/**
* One renderer for all six runtime pages. Each runtime supplies its own
* `sections` from the main process, so the shape stays uniform while the
* content is specific — an honest empty state where a runtime has nothing,
* never a padded row.
*/
function viewRuntime(host, s, id) {
const summary = (s.runtimes ?? []).find((r) => r.id === id);
const entry = state.details[id];
const d = entry?.data;
// Status is known from the cheap scan, so it paints before the deep probe.
const src = d ?? summary;
if (!src) { host.append(el('div', 'gtitle', 'Unknown runtime')); return; }
host.append(el('div', 'gtitle', 'Status'));
const st = el('div', 'group');
const statusRows = [
['State', src.serving ? 'Running' : src.installed ? 'Installed, not running' : 'Not installed'],
['Version', src.version],
['Formats', (src.formats ?? []).join(', ')],
['Endpoint', src.endpoint],
].filter(([, v]) => v);
for (const [k, v] of statusRows) {
const row = el('div', 'kv');
row.append(el('span', 'k', k), el('span', 'v', String(v)));
st.append(row);
}
host.append(st);
if (!src.installed) {
host.append(el('div', 'gtitle', 'Install'));
const g = el('div', 'group');
const cmd = el('div', 'cmd');
cmd.append(el('code', null, src.installCommand));
const b = el('button', 'btn sec', 'Copy');
b.addEventListener('click', () => copy(src.installCommand, 'Install command copied'));
cmd.append(b);
g.append(cmd);
host.append(g);
return;
}
if (entry?.loading && !d) {
host.append(el('div', 'gtitle', 'Details'));
const g = el('div', 'group');
const row = el('div', 'row');
row.append(el('div', 'info', 'Probing this runtime…'));
g.append(row);
host.append(g);
return;
}
if (entry?.error) {
const e = el('div', 'errbox');
e.append(el('b', null, 'Could not read this runtime'), document.createTextNode(entry.error));
host.append(e);
}
// Sections come straight from the runtime's own probeDetails().
for (const sec of d?.sections ?? []) {
host.append(el('div', 'gtitle', sec.title));
const g = el('div', 'group');
if (sec.rows?.length) {
for (const r of sec.rows) {
const row = el('div', 'kv');
row.append(el('span', 'k', r.label), el('span', 'v', String(r.value)));
if (r.hint) row.title = r.hint;
g.append(row);
}
} else {
const row = el('div', 'row');
row.append(el('div', 'info', sec.empty ?? 'Nothing to show.'));
g.append(row);
}
host.append(g);
}
const models = d?.models ?? summary?.models ?? [];
host.append(el('div', 'gtitle', `Models — ${models.length}`));
const mg = el('div', 'group');
if (models.length) {
for (const m of models) {
const row = el('div', 'row');
row.append(el('span', `fit ${m.loaded ? 'fits' : ''}`.trim()));
const info = el('div', 'info');
info.append(el('div', 't1', m.ref ?? m.name));
const meta = [m.params, m.quant, m.family, m.contextLength && `${m.contextLength} ctx`]
.filter(Boolean).join(' · ');
if (meta) info.append(el('div', 't2', meta));
row.append(info);
if (m.sizeBytes) row.append(el('span', 'sz', gb(m.sizeBytes / 1e9)));
mg.append(row);
}
} else {
const row = el('div', 'row');
row.append(el('div', 'info',
src.serving ? 'Running, but no models loaded.' : 'No models found for this runtime.'));
mg.append(row);
}
host.append(mg);
host.append(el('div', 'gtitle', 'Actions'));
const ag = el('div', 'group');
for (const a of d?.actions ?? []) {
const row = el('div', 'cmd');
const info = el('div', 'info');
info.append(el('div', 't1', a.label));
row.append(info);
if (a.command) row.append(el('code', null, a.command));
const b = el('button', 'btn sec', 'Copy');
b.addEventListener('click', () => copy(a.command, 'Copied'));
row.append(b);
ag.append(row);
}
const refresh = el('div', 'row');
const ri = el('div', 'info');
ri.append(el('div', 't1', 'Re-probe this runtime'));
ri.append(el('div', 't2', 'Runs the deep checks again'));
refresh.append(ri);
const rb = el('button', 'btn', entry?.loading ? 'Probing…' : 'Refresh');
if (entry?.loading) rb.disabled = true;
rb.addEventListener('click', () => loadRuntimeDetails(id, { force: true }));
refresh.append(rb);
ag.append(refresh);
host.append(ag);
}
const VIEWS = { recommended: viewRecommended, installed: viewInstalled, settings: viewSettings };
function renderContent() {
const host = $('#content');
host.textContent = '';
const s = state.core;
if (!s) return;
if (s.phase === 'error') {
const e = el('div', 'errbox');
e.append(el('b', null, 'The scan could not finish'), document.createTextNode(s.error ?? ''));
host.append(e);
return;
}
if (state.view.startsWith('runtime:')) {
viewRuntime(host, s, state.view.slice(8));
return;
}
(VIEWS[state.view] ?? viewRecommended)(host, s);
}
/* ------------------------------------------------------------------ */
/* wiring */
/* ------------------------------------------------------------------ */
function switchView(view) {
state.view = view;
markCurrent();
renderHeader(state.core ?? {});
renderContent();
// Opening a runtime page triggers its deep probe the first time only.
const id = view.startsWith('runtime:') ? view.slice(8) : null;
if (id && !state.details[id]) loadRuntimeDetails(id);
}
async function loadRuntimeDetails(id, { force = false } = {}) {
if (state.details[id]?.loading) return;
if (state.details[id]?.data && !force) return;
state.details[id] = { loading: true, data: state.details[id]?.data ?? null, error: null };
if (state.view === `runtime:${id}`) renderContent();
try {
const data = await window.llm.runtimeDetails(id);
state.details[id] = { loading: false, data, error: null };
} catch (err) {
state.details[id] = { loading: false, data: null, error: err?.message ?? String(err) };
}
if (state.view === `runtime:${id}`) renderContent();
}
function apply(s) {
state.core = s;
renderSidebarRuntimes(s);
renderCounts(s);
renderHeader(s);
renderContent();
}
document.addEventListener('DOMContentLoaded', async () => {
// Platform chrome: macOS keeps native traffic lights; everywhere else the
// window is frameless, so show our own controls (top-right, line glyphs).
if (window.llm.platform === 'darwin') {
document.body.classList.add('mac');
} else {
const ctl = document.getElementById('winctl');
ctl.hidden = false;
document.getElementById('w-min').addEventListener('click', () => window.llm.winMinimize());
document.getElementById('w-max').addEventListener('click', () => window.llm.winMaximize());
document.getElementById('w-close').addEventListener('click', () => window.llm.winClose());
}
window.llm.onState(apply);
apply(await window.llm.snapshot());
document.querySelectorAll('.srow[data-view]').forEach((b) => {
b.addEventListener('click', () => switchView(b.dataset.view));
});
document.querySelectorAll('#seg-fit button').forEach((b) => {
b.addEventListener('click', () => {
document.querySelectorAll('#seg-fit button').forEach((x) => x.setAttribute('aria-pressed', 'false'));
b.setAttribute('aria-pressed', 'true');
state.fit = b.dataset.fit;
renderContent();
});
});
$('#search').addEventListener('input', (ev) => {
state.query = ev.target.value.trim();
renderContent();
});
});

View file

@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View file

@ -0,0 +1,7 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ai-search-02">
<path id="Vector" d="M13.3333 13.75L16.6666 17.0833" stroke="#99A0AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M15 9.58333C15 12.805 12.3883 15.4167 9.16667 15.4167C5.94501 15.4167 3.33333 12.805 3.33333 9.58333C3.33333 6.3617 5.94501 3.75003 9.16667 3.75003" stroke="#99A0AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M12.9167 2.91669L13.1316 3.49755C13.4134 4.25921 13.5543 4.64004 13.8322 4.91785C14.11 5.19567 14.4908 5.33658 15.2525 5.61843L15.8333 5.83336L15.2525 6.04829C14.4908 6.33013 14.11 6.47106 13.8322 6.74887C13.5543 7.02668 13.4134 7.40751 13.1316 8.16917L12.9167 8.75L12.7017 8.16917C12.4199 7.40751 12.279 7.02668 12.0012 6.74887C11.7233 6.47106 11.3425 6.33013 10.5808 6.04829L10 5.83336L10.5808 5.61843C11.3425 5.33658 11.7233 5.19567 12.0012 4.91785C12.279 4.64004 12.4199 4.25921 12.7017 3.49755L12.9167 2.91669Z" stroke="#99A0AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,8 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="archive">
<path id="Vector" d="M10.8333 1.66667H9.16667C6.02397 1.66667 4.45262 1.66667 3.47631 2.64297C2.5 3.61929 2.5 5.19063 2.5 8.33333V11.6667C2.5 14.8093 2.5 16.3807 3.47631 17.357C4.45262 18.3333 6.02397 18.3333 9.16667 18.3333H10.8333C13.976 18.3333 15.5474 18.3333 16.5237 17.357C17.5 16.3807 17.5 14.8093 17.5 11.6667V8.33333C17.5 5.19063 17.5 3.61929 16.5237 2.64297C15.5474 1.66667 13.976 1.66667 10.8333 1.66667Z" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path id="Vector_2" d="M17.5 10H2.5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path id="Vector_3" d="M12.5 5.83333H7.5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path id="Vector_4" d="M12.5 14.1667H7.5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View file

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="5.8335" height="9.546" viewBox="0 0 5.8335 9.546" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M3.7125 4.773L0 1.0605L1.0605 0L5.8335 4.773L1.0605 9.546L0 8.4855L3.7125 4.773Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 305 B

View file

@ -0,0 +1,7 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="clock-04">
<path id="Vector" d="M10 18.3333C14.6023 18.3333 18.3333 14.6023 18.3333 10C18.3333 5.39762 14.6023 1.66667 10 1.66667C6.2686 1.66667 3.14522 4.11908 2.08333 7.5H4.16667" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M10 6.66667V10L11.6667 11.6667" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M1.66667 10C1.66667 10.2811 1.67933 10.5591 1.70412 10.8333M7.5 18.3333C7.21533 18.2397 6.93726 18.1303 6.66667 18.0065M2.67449 14.1667C2.51379 13.857 2.37044 13.5361 2.24581 13.2052M4.02602 16.0887C4.28075 16.3632 4.55255 16.6201 4.83963 16.8577" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View file

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4.25" y="4.25" width="11.5" height="8" rx="1.5" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M3.5 15.75H16.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8.5 12.25V13.5M11.5 12.25V13.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

View file

@ -0,0 +1,8 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="document-attachment">
<path id="Vector" d="M17.0833 8.75V8.33333C17.0833 5.19063 17.0832 3.61929 16.107 2.64297C15.1307 1.66667 13.5593 1.66667 10.4166 1.66667H9.58333C6.44069 1.66667 4.86936 1.66667 3.89305 2.64297C2.91674 3.61927 2.91673 5.1906 2.9167 8.33328L2.91667 12.0833C2.91664 14.8228 2.91663 16.1927 3.67323 17.1146C3.81177 17.2834 3.96655 17.4382 4.13535 17.5767C5.05731 18.3333 6.42708 18.3333 9.16658 18.3333" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M6.25 5.83333H13.75" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M6.25 10H11.25" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_4" d="M17.0833 16.6667V14.1667C17.0833 12.9755 15.9641 11.6667 14.5833 11.6667C13.2026 11.6667 12.0833 12.9755 12.0833 14.1667V17.0833C12.0833 17.7737 12.643 18.3333 13.3333 18.3333C14.0237 18.3333 14.5833 17.7737 14.5833 17.0833V14.1667" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M3 4H21V6H3V4ZM3 11H15V13H3V11ZM3 18H21V20H3V18Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View file

@ -0,0 +1,6 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="settings-02">
<path id="Vector" d="M12.9167 10C12.9167 11.6108 11.6108 12.9167 10 12.9167C8.38917 12.9167 7.08333 11.6108 7.08333 10C7.08333 8.38917 8.38917 7.08333 10 7.08333C11.6108 7.08333 12.9167 8.38917 12.9167 10Z" stroke="black" stroke-width="1.25"/>
<path id="Vector_2" d="M17.5092 11.7471C17.9441 11.6298 18.1616 11.5712 18.2474 11.459C18.3333 11.3469 18.3333 11.1665 18.3333 10.8058V9.19434C18.3333 8.83359 18.3333 8.65317 18.2474 8.54109C18.1615 8.42892 17.9441 8.37025 17.5092 8.25299C15.8838 7.81466 14.8666 6.11543 15.2861 4.50073C15.4014 4.05666 15.4591 3.83464 15.404 3.70441C15.3489 3.57419 15.1909 3.48445 14.8748 3.30497L13.4375 2.48895C13.1273 2.31283 12.9722 2.22477 12.8331 2.24352C12.6938 2.26227 12.5368 2.41895 12.2227 2.73228C11.0067 3.9454 8.99467 3.94535 7.77862 2.7322C7.46452 2.41886 7.30748 2.2622 7.16827 2.24344C7.02907 2.22469 6.87397 2.31275 6.56377 2.48886L5.12653 3.3049C4.81044 3.48436 4.65239 3.5741 4.59732 3.7043C4.54223 3.8345 4.59989 4.05656 4.71521 4.50067C5.13447 6.11542 4.11643 7.8147 2.49085 8.253C2.05593 8.37025 1.83847 8.42892 1.75257 8.541C1.66667 8.65317 1.66667 8.83359 1.66667 9.19434V10.8058C1.66667 11.1665 1.66667 11.3469 1.75257 11.459C1.83846 11.5712 2.05593 11.6298 2.49085 11.7471C4.11617 12.1854 5.1334 13.8847 4.71393 15.4993C4.59858 15.9434 4.54089 16.1654 4.59597 16.2957C4.65105 16.4259 4.8091 16.5157 5.12521 16.6951L6.56246 17.5112C6.87267 17.6873 7.02778 17.7753 7.167 17.7566C7.30622 17.7378 7.46323 17.5811 7.77726 17.2678C8.99392 16.0537 11.0073 16.0536 12.2241 17.2677C12.5381 17.5811 12.6951 17.7378 12.8343 17.7565C12.9735 17.7753 13.1287 17.6872 13.4388 17.5111L14.8761 16.695C15.1922 16.5156 15.3503 16.4258 15.4053 16.2956C15.4604 16.1653 15.4028 15.9433 15.2873 15.4993C14.8677 13.8847 15.8841 12.1855 17.5092 11.7471Z" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,7 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="wrench-01">
<path id="Vector" d="M20.3584 13.3567C19.1689 14.546 16.9308 14.4998 13.4992 14.4998C11.2914 14.4998 9.50138 12.7071 9.50024 10.4993C9.50024 7.07001 9.454 4.83065 10.6435 3.64138C11.8329 2.45212 12.3583 2.50027 17.6274 2.50027C18.1366 2.49809 18.3929 3.11389 18.0329 3.47394L15.3199 6.18714C14.6313 6.87582 14.6294 7.99233 15.3181 8.68092C16.0068 9.36952 17.1234 9.36959 17.8122 8.68109L20.5259 5.96855C20.886 5.60859 21.5019 5.86483 21.4997 6.37395C21.4997 11.6422 21.5479 12.1675 20.3584 13.3567Z" stroke="black" stroke-width="1.5"/>
<path id="Vector_2" d="M13.5 14.5L7.32842 20.6716C6.22386 21.7761 4.433 21.7761 3.32843 20.6716C2.22386 19.567 2.22386 17.7761 3.32843 16.6716L9.5 10.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path id="Vector_3" d="M5.50896 18.5H5.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,6 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="zsh">
<path id="Vector" d="M10 17.5C13.1247 17.5 14.6872 17.5 15.7824 16.7042C16.1362 16.4472 16.4472 16.1362 16.7042 15.7824C17.5 14.6872 17.5 13.1247 17.5 10C17.5 6.87522 17.5 5.31283 16.7042 4.21756C16.4472 3.86383 16.1362 3.55276 15.7824 3.29576C14.6872 2.5 13.1247 2.5 10 2.5C6.87522 2.5 5.31283 2.5 4.21756 3.29576C3.86383 3.55276 3.55276 3.86383 3.29576 4.21756C2.5 5.31283 2.5 6.87522 2.5 10C2.5 13.1247 2.5 14.6872 3.29576 15.7824C3.55276 16.1362 3.86383 16.4472 4.21756 16.7042C5.31283 17.5 6.87522 17.5 10 17.5Z" stroke="#99A0AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M13.3333 5.83333L6.66667 14.1667" stroke="#99A0AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 938 B

View file

@ -0,0 +1,69 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Fully local renderer: no remote anything. -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self';">
<title>LLM Checker</title>
<link rel="stylesheet" href="app.css">
</head>
<body>
<div class="app">
<!-- ===== sidebar (0:2177 / 0:2179) ===== -->
<nav class="sb">
<!-- macOS: reserves room for the real traffic lights (hiddenInset). -->
<div class="dragstrip" id="dragstrip"></div>
<div class="apptitle">LLM Checker</div>
<div class="sb-scroll">
<button class="srow" data-view="recommended" aria-current="true">
<span class="icn" style="--i:url('assets/icons/icon-ai-search.svg')"></span>
<span class="nm">Recommended</span><span class="ct" data-count="recommended">—</span>
</button>
<button class="srow" data-view="installed">
<span class="icn" style="--i:url('assets/icons/icon-archive.svg')"></span>
<span class="nm">Installed</span><span class="ct" data-count="installed">—</span>
</button>
<button class="srow" data-view="settings">
<span class="icn" style="--i:url('assets/icons/icon-settings.svg')"></span>
<span class="nm">Settings</span>
</button>
<div class="sb-head">Runtimes</div>
<div id="sb-runtimes"></div>
</div>
</nav>
<div class="vsep"></div>
<!-- ===== main ===== -->
<main class="mn">
<div class="tb">
<div class="tb-title">
<div class="t" id="title">Recommended</div>
<div class="s" id="subtitle">Scanning…</div>
</div>
<div class="sp"></div>
<div class="seg" id="seg-fit">
<button data-fit="all" aria-pressed="true">All</button>
<button data-fit="fits" aria-pressed="false">Fits</button>
</div>
<label class="search"><input id="search" type="text" placeholder="Search"></label>
<!-- Frameless window controls — hidden on macOS (real chrome there) -->
<div class="winctl" id="winctl" hidden>
<button id="w-min" title="Minimize"><span class="g g-min"></span></button>
<button id="w-max" title="Maximize"><span class="g g-max"></span></button>
<button id="w-close" title="Close"><span class="g g-close"></span></button>
</div>
</div>
<div class="content" id="content"></div>
</main>
</div>
<div class="toast" id="toast">Copied</div>
<script src="app.js"></script>
</body>
</html>

View file

@ -0,0 +1,306 @@
'use strict';
/**
* Runtime registry + card shaping tests. No Electron needed — these exercise
* the pure logic that the main process depends on.
*
* node test/runtimes.test.js
*/
const assert = require('assert');
const runtimes = require('../src/main/runtimes');
const {
classifyFit, toCard, harvest, harvestCategories, CATEGORIES, tidyReason, FIT,
} = require('../src/main/core');
let pass = 0;
let fail = 0;
const results = [];
function test(name, fn) {
try {
fn();
pass += 1;
results.push(` ok ${name}`);
} catch (err) {
fail += 1;
results.push(` FAIL ${name}\n ${err.message}`);
}
}
async function testAsync(name, fn) {
try {
await fn();
pass += 1;
results.push(` ok ${name}`);
} catch (err) {
fail += 1;
results.push(` FAIL ${name}\n ${err.message}`);
}
}
/* ---------------- normalisation ---------------- */
test('every advertised runtime resolves', () => {
for (const id of ['ollama', 'llama.cpp', 'lmstudio', 'vllm', 'mlx', 'transformers']) {
assert.strictEqual(runtimes.normalize(id), id, `${id} did not resolve to itself`);
}
});
test('aliases resolve to their canonical id', () => {
assert.strictEqual(runtimes.normalize('llamacpp'), 'llama.cpp');
assert.strictEqual(runtimes.normalize('llama-cpp'), 'llama.cpp');
assert.strictEqual(runtimes.normalize('lm-studio'), 'lmstudio');
assert.strictEqual(runtimes.normalize('mlx_lm'), 'mlx');
assert.strictEqual(runtimes.normalize('huggingface'), 'transformers');
});
test('unknown runtimes return null, never a silent ollama', () => {
// This is the CLI bug this module exists to avoid: normalizeRuntime()
// in src/runtime/runtime-support.js rewrites llama.cpp -> ollama.
assert.strictEqual(runtimes.normalize('bogus'), null);
assert.strictEqual(runtimes.normalize(''), null);
assert.strictEqual(runtimes.normalize(undefined), null);
});
test('auto is preserved rather than collapsed to a concrete runtime', () => {
assert.strictEqual(runtimes.normalize('auto'), 'auto');
assert.strictEqual(runtimes.get('auto'), null);
});
/* ---------------- eligibility ---------------- */
test('mlx is offered only on Apple Silicon', () => {
const ids = runtimes.eligible({}).map((r) => r.id);
const expected = process.platform === 'darwin' && process.arch === 'arm64';
assert.strictEqual(ids.includes('mlx'), expected);
});
test('vllm needs an accelerator and is never offered on macOS', () => {
const withGpu = runtimes.eligible({ gpu: { vramGB: 12 } }).map((r) => r.id);
const without = runtimes.eligible({ gpu: { vramGB: 0 } }).map((r) => r.id);
if (process.platform === 'darwin') {
assert.ok(!withGpu.includes('vllm'), 'vllm must not appear on macOS');
} else {
assert.ok(withGpu.includes('vllm'), 'vllm should appear with a GPU');
assert.ok(!without.includes('vllm'), 'vllm should not appear without one');
}
});
test('ollama and llama.cpp are available everywhere', () => {
const ids = runtimes.eligible({}).map((r) => r.id);
assert.ok(ids.includes('ollama'));
assert.ok(ids.includes('llama.cpp'));
});
/* ---------------- commands ---------------- */
test('each runtime produces a real, distinct pull command', () => {
const ollama = runtimes.commandsFor('ollama', 'mistral:7b');
const llama = runtimes.commandsFor('llama.cpp', 'TheBloke/Mistral-7B-GGUF');
const mlx = runtimes.commandsFor('mlx', 'mlx-community/Mistral-7B');
assert.match(ollama.pull, /^ollama pull /);
assert.match(llama.pull, /huggingface-cli download/);
assert.match(mlx.run, /mlx_lm/);
// The bug being guarded: llama.cpp must NOT emit an ollama command.
assert.ok(!llama.pull.includes('ollama'), 'llama.cpp leaked an ollama command');
assert.ok(!mlx.run.includes('ollama'), 'mlx leaked an ollama command');
});
test('llama.cpp exposes a serve command, ollama does not', () => {
assert.ok(runtimes.commandsFor('llama.cpp', 'x').serve);
assert.strictEqual(runtimes.commandsFor('ollama', 'x').serve, null);
});
test('commandsFor on an unknown runtime returns null', () => {
assert.strictEqual(runtimes.commandsFor('nope', 'x'), null);
});
/* ---------------- format routing ---------------- */
test('gguf routes to a gguf-capable runtime', () => {
const chosen = runtimes.chooseFor({ format: 'gguf' }, [], { gpu: { vramGB: 12 } });
assert.ok(chosen, 'no runtime chosen for gguf');
assert.ok(chosen.formats.includes('gguf'), `${chosen.id} cannot run gguf`);
});
test('safetensors never routes to ollama', () => {
const chosen = runtimes.chooseFor({ format: 'safetensors' }, [], { gpu: { vramGB: 12 } });
assert.ok(chosen, 'no runtime chosen for safetensors');
assert.ok(chosen.formats.includes('safetensors'));
assert.notStrictEqual(chosen.id, 'ollama');
});
test('an installed runtime outranks an uninstalled one', () => {
const detected = [
{ id: 'ollama', installed: false, serving: false },
{ id: 'llama.cpp', installed: true, serving: true },
];
const chosen = runtimes.chooseFor({ format: 'gguf' }, detected, { gpu: { vramGB: 12 } });
assert.strictEqual(chosen.id, 'llama.cpp');
});
/* ---------------- fit classification ---------------- */
test('fit buckets follow the VRAM budget', () => {
assert.strictEqual(classifyFit(5.1, 12), FIT.GOOD); // 42%
assert.strictEqual(classifyFit(11.5, 12), FIT.TIGHT); // 96% including KV cache
assert.strictEqual(classifyFit(42.5, 12), FIT.OVER); // 354%
assert.strictEqual(classifyFit(12, 12), FIT.TIGHT); // exactly at budget
});
test('unknown sizes do not get flagged as over budget', () => {
assert.strictEqual(classifyFit(null, 12), FIT.UNKNOWN);
assert.strictEqual(classifyFit(5, null), FIT.UNKNOWN);
});
/* ---------------- harvesting ---------------- */
test('harvest flattens the flat buckets, including incompatible', () => {
const got = harvest({
recommended: [{ name: 'a' }],
compatible: [{ name: 'b' }],
incompatible: [{ name: 'c' }],
marginal: [{ name: 'd' }],
});
assert.deepStrictEqual(got.map((m) => m.name).sort(), ['a', 'b', 'c', 'd']);
});
test('harvest reads model_name, the field the ranked entries actually use', () => {
const got = harvest({ compatible: [{ model_name: 'qwen2.5-coder' }] });
assert.strictEqual(got.length, 1);
});
/* ---------------- category grouping ---------------- */
test('harvestCategories reads bestModels two levels down', () => {
const groups = harvestCategories({
recommendations: {
recommendations: {
coding: { bestModels: [{ model_name: 'qwen2.5-coder' }], totalCandidates: 10799 },
general: { bestModels: [{ model_name: 'mistral' }], totalCandidates: 10799 },
},
},
});
const keys = groups.map((g) => g.key);
assert.ok(keys.includes('coding') && keys.includes('general'));
// General leads the board; coding follows.
assert.strictEqual(keys[0], 'general');
assert.strictEqual(groups[0].evaluated, 10799);
});
test('harvestCategories drops categories with no models', () => {
const groups = harvestCategories({
recommendations: { recommendations: { coding: { bestModels: [] }, general: { bestModels: [{ model_name: 'x' }] } } },
});
assert.deepStrictEqual(groups.map((g) => g.key), ['general']);
});
test('harvestCategories tolerates a missing tree', () => {
assert.deepStrictEqual(harvestCategories({}), []);
assert.deepStrictEqual(harvestCategories(), []);
});
test('every category carries a label, emoji and colour for the column head', () => {
for (const c of CATEGORIES) {
assert.ok(c.label && c.emoji && c.colour, `${c.key} is missing display data`);
}
});
test('tidyReason shortens the float noise the core emits', () => {
const out = tidyReason('fits in 6.108751999999999/12GB, Q6_K, 7B is sweet spot');
assert.ok(out.includes('6.1/12GB'), out);
assert.ok(!out.includes('6.10875'), out);
});
test('harvest de-duplicates case-insensitively', () => {
const got = harvest({ recommended: [{ name: 'Mistral' }], compatible: [{ name: 'mistral' }] });
assert.strictEqual(got.length, 1);
});
test('harvest tolerates an empty analysis', () => {
assert.deepStrictEqual(harvest({}), []);
assert.deepStrictEqual(harvest(), []);
});
/* ---------------- card shaping ---------------- */
test('toCard carries size, quant and a runtime-correct command', () => {
const c = toCard(
{ name: 'mistral', parameters: '7B', sizeGB: 5.1, quantization: 'Q5_K_M', format: 'gguf', score: 92 },
12, [{ id: 'ollama', installed: true, serving: true }], { gpu: { vramGB: 12 } }
);
assert.strictEqual(c.name, 'mistral');
assert.strictEqual(c.fit, FIT.GOOD);
assert.strictEqual(c.quant, 'Q5_K_M');
assert.strictEqual(c.runtime, 'ollama');
assert.match(c.commands.pull, /ollama pull/);
});
test('toCard reads the real ranked-entry shape, not the field names it resembles', () => {
// Verbatim shape from recommendations.recommendations.coding.bestModels[0].
const c = toCard({
model_name: 'qwen2.5-coder',
model_identifier: 'qwen2.5-coder:7b-base-q6_K',
categoryScore: 82.7,
pulls: 15000000,
size: 7, // parameter count in billions, NOT gigabytes
estimatedRAM: 6.108751999999999, // the real VRAM figure
quantization: 'Q6_K',
artifactFormat: 'gguf',
reasoning: 'fits in 6.108751999999999/12GB, Q6_K, 7B is sweet spot',
runtime: 'ollama',
}, 12, [{ id: 'ollama', installed: true, serving: true }], { gpu: { vramGB: 12 } });
assert.strictEqual(c.name, 'qwen2.5-coder');
assert.strictEqual(c.params, '7B', 'size must be read as a parameter count');
assert.ok(Math.abs(c.sizeGB - 6.1087) < 0.001, 'sizeGB must come from estimatedRAM');
assert.strictEqual(c.fit, FIT.GOOD, '6.1 of 12 GB is a comfortable fit');
assert.strictEqual(c.ref, 'qwen2.5-coder:7b-base-q6_K');
assert.match(c.commands.pull, /qwen2\.5-coder:7b-base-q6_K/);
assert.ok(c.purpose.includes('6.1/12GB'), 'reasoning should be tidied');
});
test('a 70B model on a 12 GB card lands over budget', () => {
const c = toCard(
{ model_name: 'Llama 3.3 70B', size: 70, estimatedRAM: 24, artifactFormat: 'gguf' },
12, [], { gpu: { vramGB: 12 } }
);
assert.strictEqual(c.fit, FIT.OVER);
assert.strictEqual(c.params, '70B');
});
test('a safetensors model gets a non-ollama command set', () => {
const c = toCard(
{ name: 'llama-3', format: 'safetensors', sizeGB: 8 },
12, [], { gpu: { vramGB: 12 } }
);
assert.notStrictEqual(c.runtime, 'ollama');
assert.ok(!c.commands.pull.startsWith('ollama'));
});
/* ---------------- detection ---------------- */
(async () => {
await testAsync('detectAll never throws and reports every eligible runtime', async () => {
const hw = { gpu: { vramGB: 12 }, summary: { backend: 'NVIDIA CUDA' } };
const t0 = Date.now();
const found = await runtimes.detectAll(hw);
const elapsed = Date.now() - t0;
assert.strictEqual(found.length, runtimes.eligible(hw).length);
for (const r of found) {
assert.ok(typeof r.installed === 'boolean', `${r.id} missing installed`);
assert.ok(typeof r.serving === 'boolean', `${r.id} missing serving`);
assert.ok(r.installCommand, `${r.id} missing install command`);
}
// Probes run concurrently; a serial sweep would be ~N x the timeout.
assert.ok(elapsed < 6000, `detection took ${elapsed} ms — is it running serially?`);
});
console.log('\nruntime registry\n');
results.forEach((r) => console.log(r));
console.log(`\n${pass} passed, ${fail} failed\n`);
process.exit(fail ? 1 : 0);
})();

View file

@ -0,0 +1,49 @@
# Ranking contract validation
The September 2026 ranking audit identified eight paths that could produce an
incorrect recommendation. These fixes apply to the deterministic selector,
registry recommendations, and the desktop source now tracked in this repository.
| Case | Corrected behavior | Regression coverage |
| --- | --- | --- |
| Artifact precision and size | FP16/BF16/FP32 retain their precision. A fixed Q8 file cannot become Q5 to fit a budget. Complete shard sets contribute their combined size; incomplete sets are excluded. Unknown precision without a known size is ineligible. | `ranking-contract.test.js`, `selector-memory-sizing.test.js`, `deterministic-model-pool-check.js` |
| Model capabilities | Embeddings and rerankers are excluded from generation categories; missing capability data does not imply generation. | `ranking-contract.test.js` |
| Benchmark identity | Base, Instruct, size, revision, thinking, and context-extension variants remain distinct. Unsized family measurements cannot score a smaller checkpoint. Quantized repos require an explicit quantization alias to inherit upstream measurements. | `quality-evals.test.js`, `ranking-contract.test.js` |
| Accelerator scoring | ROCm, Vulkan and SYCL have explicit estimate profiles. CPU-only mode suppresses accelerator scoring. | `ranking-contract.test.js`, `cpu-only-mode.test.js` |
| Desktop memory fit | Cards use the core's budget, including shared memory. Missing size/budget is unknown. The desktop retains the scored runtime and artifact. | `desktop-core.test.js`, `desktop/test/runtimes.test.js` |
| Context | Explicit requests reject insufficient or unknown native windows. Preferred contexts report the effective window and limitations; memory and generated llama.cpp/vLLM commands use that window. | `ranking-contract.test.js` |
| Q8 quality | A zero quantization penalty stays zero for measured and estimated quality. | `ranking-contract.test.js` |
| Desktop updates | The app requires the current ranking contract, refreshes every registered benchmark source, closes database connections, invalidates cached quality data and recalculates rankings. Packaging includes the core, analyzer and CLI. | `desktop-core.test.js`, packaged application validation |
Additional artifact guards prevent an empty vLLM ranking from falling back to
Ollama files, and prevent HF filenames from becoming Ollama pull tags.
## Real execution
Validated on Linux with a Ryzen 9 9900X, 30 GB reported RAM and RTX 5070 with
12 GB VRAM. A packaged Electron 44.0.0 / Node 24.18.1 application reached ready
state and displayed all seven recommendation categories. The renderer bridge
successfully refreshed all six public sources and recalculated the ranking:
HF Open LLM (2,622 rows), LMArena (798), BigCodeBench (279), EvalPlus (428),
LiveBench (1,548), and MMMU (256).
The live benchmark snapshot now assigns Qwen2.5-7B base its own MMLU-PRO score
43.6503, and Qwen2.5-7B-Instruct its own 42.8690. The Instruct-1M context
extension's 35.0482 score is separate. DeepSeek-R1 at 14B does not inherit the
unsized R1-0528 result.
Generated download/run commands completed real CPU inference with
`ggml-org/models/tinyllamas/stories260K.gguf` under llama.cpp and
`HuggingFaceTB/SmolLM2-135M` under Transformers 5.16.1 / Torch 2.14.0.
The llama.cpp command used the requested 512-token context.
The shared-memory example (26 GB RAM, 0.25 GB GPU aperture, 13 GB effective
memory) and AMD/ROCm/Vulkan/SYCL cases use controlled hardware inputs. They
verify classification logic, not physical performance on those accelerators.
Memory and speed predictions remain estimates; this validation does not claim
that every catalog model or runtime build has been physically benchmarked.
The full root suite includes 65 test files. Quality ingestion and matching run
against native SQLite and WASM; the desktop bridge and ranking contract were
also exercised on Node 18. Core production dependencies and desktop dependencies
passed `npm audit` with no reported vulnerabilities at validation time.

View file

@ -0,0 +1,49 @@
// Family names define comparison cohorts. They are not checkpoint identities.
function checkpointIdentity(name, paramsB, model = {}) {
const artifact = model.artifact || {};
const tags = [...(model.repoTags || []), ...(artifact.repo_tags || [])];
const quantizedBase = tags.find(tag => /^base_model:quantized:/i.test(tag));
let reference = quantizedBase ? quantizedBase.replace(/^base_model:quantized:/i, '') :
(artifact.source_id !== 'ollama' && artifact.repo_id?.includes('/') ? artifact.repo_id : name);
reference = String(reference || '').toLowerCase().replace(/^https:\/\/huggingface.co\//, '').replace(/\/$/, '');
const slash = reference.indexOf('/');
const provider = slash >= 0 ? reference.slice(0, slash) : null;
let label = slash >= 0 ? reference.slice(slash + 1) : reference;
const declaredSize = label.match(/(?:^|[-_:])(\d+(?:\.\d+)?)\s*(b|m)(?:\b|_)/i);
const size = declaredSize ? Number(declaredSize[1]) / (declaredSize[2] === 'm' ? 1000 : 1) : Number(paramsB);
const roleMatch = label.match(/(?:^|[-_:])(instruct|chat|it|base)(?:\b|_)/);
const isOllama = String(model.source || artifact.source_id || '').includes('ollama') || (!provider && reference.includes(':'));
const metadataRole = !provider && (model.tags || []).includes('instruct') ? 'instruct' : isOllama ? 'unknown' : 'base';
const role = roleMatch ? (roleMatch[1] === 'it' ? 'instruct' : roleMatch[1]) : metadataRole;
label = label
.replace(/\.(gguf|safetensors|bin)$/, '')
.replace(/(?:^|[-_.:])(?:i?q\d[\w]*|fp(?:8|16|32)|bf16|f16|int[248]|gguf|awq|gptq|hf)(?=$|[-_.:])/g, '-')
.replace(/\b\d+(?:\.\d+)?[bm]\b/, '')
.replace(/\b(instruct|chat|it|base|latest)\b/g, '')
.replace(/^(meta|google|microsoft|mistralai|alibaba|nvidia|ibm|openai|anthropic)-/, '')
.replace(/[^a-z0-9.]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
.replace(/-(?=\d)/g, '');
const conflictingSize = declaredSize && Number(paramsB) > 0 && Math.abs(Number(paramsB) - size) / Math.max(Number(paramsB), size) > 0.2;
return { label, provider, role, size: !conflictingSize && size > 0 && Number.isFinite(size) ? size : null };
}
function sameCheckpoint(wanted, measured) {
// Boards often omit the org. Only known publisher namespaces can bridge
// that omission; a community repo with the same basename is not an alias.
const publishers = {
qwen: /^qwen/, 'meta-llama': /^(llama|codellama)/,
google: /^gemma/, microsoft: /^phi/, 'deepseek-ai': /^deepseek/,
mistralai: /^(mistral|mixtral|codestral|ministral)/,
huggingfacetb: /^smollm/, 'ibm-granite': /^granite/,
tiiuae: /^falcon/, allenai: /^olmo/
};
if (Boolean(wanted.provider) !== Boolean(measured.provider)) {
const qualified = wanted.provider ? wanted : measured;
if (!publishers[qualified.provider]?.test(qualified.label)) return false;
}
return wanted.label === measured.label && wanted.role === measured.role &&
wanted.size != null && measured.size != null && wanted.size === measured.size &&
(!wanted.provider || !measured.provider || wanted.provider === measured.provider);
}
module.exports = { checkpointIdentity, sameCheckpoint };

View file

@ -1,4 +1,5 @@
'use strict';
const { checkpointIdentity, sameCheckpoint } = require('./checkpoint-identity');
/**
* Public benchmark scores for model QUALITY.
@ -16,10 +17,9 @@
* is keyed to a machine and runtime rather than a public evaluation. The
* quality tables here remain independent of catalog and speed refreshes.
*
* The join key is (family_key, params_b), never a variant id: a leaderboard
* measures an fp16 base checkpoint, while a variant is a local Q4 build of it.
* Keeping those apart is what lets us apply an honest quantization penalty
* instead of pretending the quantized build scored what the base did.
* Measurements join by checkpoint, role, revision and declared size. Family keys
* only define comparison cohorts; a quantized repo needs a verified base-model
* alias to inherit another provider's checkpoint measurements.
*/
const SCHEMA = `
@ -528,6 +528,7 @@ class QualityEvals {
}
});
this._pctCache = new Map();
this._identityRows = null;
return { source: sourceId, rows: rows.length, fetchedAt: now };
}
@ -539,43 +540,26 @@ class QualityEvals {
* "unknown" and say so, rather than substituting an estimate and
* presenting it as a measurement.
*/
lookup(catalogName, paramsB, category = null) {
const fk = familyKey(catalogName);
const rows = this.db.prepare(`
SELECT q.*, s.display_name AS source_name, s.homepage_url, s.independent
FROM quality_evals q JOIN quality_sources s ON s.id = q.source_id
WHERE q.family_key = ?
`).all(fk.family).filter((r) => !category || r.category === category);
if (!rows.length) return null;
// Prefer rows whose published size matches the build being scored.
const sized = rows.filter((r) => sizeMatches(r.params_b, paramsB));
// Some boards (LiveBench in particular) publish a model name with no
// parameter count at all — 'deepseek-r1' rather than
// 'DeepSeek-R1-14B'. Refusing those loses real measurements: deepseek-r1
// led the reasoning column on a size ESTIMATE while its actual score
// sat unused in the table. Those rows are admitted, flagged
// sizeUnknown so the UI can say the score is for the family rather
// than this exact build.
//
// Rows that DO publish a size and simply disagree stay rejected — that
// is the case where attributing a 70B score to a 7B build would be a
// fabrication.
const unsized = rows.filter((r) => r.params_b == null);
const pool = sized.length ? sized : unsized;
if (!pool.length) return null;
const wanted = category ? pool.filter((r) => r.category === category) : pool;
lookup(catalogName, paramsB, category = null, model = {}) {
const identity = checkpointIdentity(catalogName, paramsB, model);
const fk = familyKey(model.artifact?.repo_id || catalogName);
if (!this._identityRows) {
this._identityRows = this.db.prepare(`
SELECT q.*, s.display_name AS source_name, s.homepage_url, s.independent
FROM quality_evals q JOIN quality_sources s ON s.id = q.source_id
`).all().map(row => ({ row, identity: checkpointIdentity(row.bench_model_name, row.params_b) }));
}
const wanted = this._identityRows.filter(({ row, identity: measured }) =>
(!category || row.category === category) && sameCheckpoint(identity, measured)
).map(({ row }) => row);
if (!wanted.length) return null;
return {
family: fk.family,
paramsB,
// True when the score describes the family and not a size-matched
// checkpoint. Callers must surface this rather than hide it.
sizeUnknown: !sized.length,
// Retained for output compatibility; family-only hits are ineligible.
sizeUnknown: false,
match: 'checkpoint',
evals: wanted.map((r) => ({
source: r.source_id,
sourceName: r.source_name,

View file

@ -1,6 +1,7 @@
const ModelDatabase = require('./model-database');
const { isSupportedHuggingFaceModel } = require('./registry-ingestors');
const DeterministicModelSelector = require('../models/deterministic-selector');
const { normalizePrecision, memoryBudgetGB } = require('../models/ranking-contract');
const { applyCpuOnlyOverride } = require('../hardware/cpu-only');
const { runtimeSupportedOnHardware, normalizeRuntime } = require('../runtime/runtime-support');
@ -93,15 +94,35 @@ function inferFamily(identifier = '') {
function normalizeQuantization(row = {}) {
const raw = row.quantization || row.precision || '';
if (raw) return raw;
if (row.format === 'safetensors' || row.format === 'pytorch' || row.format === 'pytorch_bin') {
return 'FP16';
}
return 'Q4_K_M';
return normalizePrecision(raw);
}
function isShardedWeightFile(filename = '') {
return /-\d{5,}-of-\d{5,}\.(safetensors|bin)$/i.test(String(filename || ''));
return /-\d{5,}-of-\d{5,}\.(safetensors|bin|gguf)$/i.test(String(filename || ''));
}
function groupWeightShards(rows) {
const groups = new Map();
const output = [];
for (const row of rows) {
const file = row.filename || row.artifact_name || '';
const match = file.match(/^(.*)-(\d{5,})-of-(\d{5,})\.(safetensors|bin|gguf)$/i);
if (!match) { output.push(row); continue; }
const key = [row.source_id, row.repo_id, match[1], match[3], match[4], row.quantization, row.revision].join('|');
if (!groups.has(key)) groups.set(key, { count: Number(match[3]), rows: new Map() });
groups.get(key).rows.set(Number(match[2]), row);
}
for (const group of groups.values()) {
const shards = [...group.rows.entries()].sort((a, b) => a[0] - b[0]);
// Partial catalogs cannot establish a runnable download or a complete size.
if (shards.length !== group.count || shards.some(([index], i) => index !== i + 1)) continue;
const first = shards[0][1];
const sizes = shards.map(([, row]) => Number(row.size_gb));
output.push({ ...first, shard_files: shards.map(([, row]) => row.filename || row.artifact_name),
shards_complete: true,
size_gb: sizes.every(size => size > 0 && Number.isFinite(size)) ? sizes.reduce((a, b) => a + b, 0) : null });
}
return output;
}
function choosePreferredRuntime(runtimeSupport = [], format = '', sourceId = '') {
@ -109,9 +130,10 @@ function choosePreferredRuntime(runtimeSupport = [], format = '', sourceId = '')
const normalizedFormat = String(format || '').toLowerCase();
const source = String(sourceId || '').toLowerCase();
if (source === 'ollama' || runtimes.includes('ollama')) return 'ollama';
if (source === 'ollama' || normalizedFormat === 'ollama') return 'ollama';
if (normalizedFormat === 'gguf') return 'llama.cpp';
if (normalizedFormat === 'mlx' || runtimes.includes('mlx')) return 'mlx';
if (normalizedFormat === 'gguf' || runtimes.includes('llama.cpp')) return 'llama.cpp';
if (runtimes.includes('llama.cpp')) return 'llama.cpp';
if (runtimes.includes('vllm')) return 'vllm';
if (runtimes.includes('transformers')) return 'transformers';
return runtimes[0] || 'transformers';
@ -125,7 +147,7 @@ function artifactToSelectorModel(row) {
tags: [...toArray(row.repo_tags), ...toArray(row.repo_tasks), ...toArray(row.tasks)]
})) return null;
const shardedFile = row.source_id === 'huggingface' && isShardedWeightFile(row.filename || row.artifact_name);
const identifier = shardedFile
const identifier = shardedFile && row.format !== 'gguf'
? (row.canonical_model_id || row.repo_id)
: (row.artifact_name || row.filename || row.canonical_model_id || row.repo_id);
const displayName = row.canonical_model_id || row.repo_display_name || identifier;
@ -189,12 +211,10 @@ function artifactToSelectorModel(row) {
.filter(Boolean)
.map((tag) => String(tag).toLowerCase());
// A sharded weight file's size is only ONE shard, not the whole model. Don't
// let it stand in for the model's memory (that made a 56B model look like
// ~4.6GB and "fit" tiny hardware); leave size unset so memory estimates from
// the (total) parameter count instead.
// Only a complete shard group supplies an observed model size. Individual
// shards must never stand in for the full resident weight set.
const rawSizeGB = Number(row.size_gb);
const sizeGB = (!shardedFile && Number.isFinite(rawSizeGB) && rawSizeGB > 0) ? rawSizeGB : NaN;
const sizeGB = ((!shardedFile || row.shards_complete) && Number.isFinite(rawSizeGB) && rawSizeGB > 0) ? rawSizeGB : NaN;
const sizeByQuant = Number.isFinite(sizeGB) && sizeGB > 0
? { [quant]: sizeGB }
: {};
@ -210,7 +230,7 @@ function artifactToSelectorModel(row) {
availableQuantizations: [quant],
sizeGB: Number.isFinite(sizeGB) && sizeGB > 0 ? sizeGB : undefined,
sizeByQuant,
ctxMax: Number(row.context_length) > 0 ? Number(row.context_length) : 4096,
ctxMax: Number(row.context_length) > 0 ? Number(row.context_length) : null,
tags,
repoTags,
description,
@ -367,6 +387,7 @@ function candidateToRecommendation(candidate) {
components: candidate.components,
qualitySource: candidate.meta.qualitySource || { kind: 'estimated', basis: 'parameter count' },
memory: candidate.memory,
context: candidate.context,
speed: candidate.speed
};
}
@ -421,7 +442,9 @@ function normalizeHardwareForSelector(hardware = {}, options = {}) {
...(hardware.acceleration || {}),
supports_metal: isMetal,
supports_cuda: isCuda,
supports_rocm: isRocm
supports_rocm: isRocm,
supports_vulkan: bestBackend === 'vulkan',
supports_sycl: bestBackend === 'sycl'
},
usableMemGB: Number(summary.effectiveMemory) > 0 ? Number(summary.effectiveMemory) : undefined
};
@ -492,7 +515,8 @@ class RegistryRecommender {
localOnly: options.localOnly !== false,
limit: poolLimit
});
const modelPool = dedupeRecommendationPool(rows.map(artifactToSelectorModel).filter(Boolean));
const modelPool = dedupeRecommendationPool(groupWeightShards(rows).map(artifactToSelectorModel)
.filter(model => model && (runtimeFilter !== 'ollama' || model.source === 'ollama')));
const normalizedRuntime = runtimeFilter || 'auto';
@ -646,13 +670,7 @@ class RegistryRecommender {
);
const objective = this.selector.normalizeOptimizationObjective(optimizeFor);
const ctx = targetCtx || this.selector.targetContexts[category] || this.selector.targetContexts.general;
const totalMem = normalizedHardware?.memory?.totalGB ?? normalizedHardware?.memory?.total ?? 8;
const usableMem = typeof normalizedHardware.usableMemGB === 'number'
? normalizedHardware.usableMemGB
: Math.max(1, Math.min(0.8 * totalMem, totalMem - 2));
const isUnified = Boolean(normalizedHardware?.gpu?.unified) || normalizedHardware?.gpu?.type === 'apple_silicon';
const vram = normalizedHardware?.gpu?.vramGB ?? normalizedHardware?.gpu?.vram ?? 0;
const budget = isUnified ? usableMem : (vram || usableMem);
const budget = memoryBudgetGB(normalizedHardware);
const filtered = this.selector.filterByCategory(modelPool, category, { includeUncensored });
const candidates = [];
let totalEvaluated = 0;
@ -679,7 +697,8 @@ class RegistryRecommender {
ctx,
budget,
objective,
runtime
runtime,
{ contextPolicy: targetCtx ? 'required' : 'preferred' }
);
if (candidate) candidates.push(candidate);
}
@ -713,5 +732,6 @@ module.exports = {
candidateToRecommendation,
normalizeHardwareForSelector,
choosePreferredRuntime,
dedupeRecommendationPool
dedupeRecommendationPool,
groupWeightShards
};

View file

@ -2597,7 +2597,7 @@ class LLMChecker {
const hasRegistryRecommendations = Object.values(recommendations)
.some((group) => Array.isArray(group.bestModels) && group.bestModels.length > 0);
if (hasRegistryRecommendations) {
if (hasRegistryRecommendations || !['auto', 'ollama'].includes(selectedRuntime)) {
const summary = this.intelligentRecommender.generateRecommendationSummary(
recommendations,
hardware,
@ -2629,6 +2629,10 @@ class LLMChecker {
}
}
}
if (!['auto', 'ollama'].includes(selectedRuntime)) {
throw new Error(`The Ollama fallback catalog cannot supply ${selectedRuntime} artifacts`);
}
// Prefer the synced SQLite catalog so `llm-checker sync` updates recommendations immediately.
const ollamaData = await this.loadOllamaModelData();

View file

@ -189,7 +189,8 @@ Respond with JSON only, no additional text.`;
hardware,
category,
ctx || this.deterministicSelector.targetContexts[category],
budget
budget, 'balanced', 'ollama',
{ contextPolicy: ctx ? 'required' : 'preferred' }
);
if (result) {
candidates.push(result);

View file

@ -5,6 +5,7 @@
* for a given machine and task category.
*/
const { normalizePrecision, precisionProfile, capabilitiesOf, detectedBackend, memoryBudgetGB, classifyFit } = require('./ranking-contract');
const fs = require('fs');
const path = require('path');
const os = require('os');
@ -94,6 +95,9 @@ class DeterministicModelSelector {
this.backendK = {
'metal': 160, // Apple Metal
'cuda': 220, // NVIDIA CUDA
'rocm': 220,
'vulkan': 160,
'sycl': 160,
'cpu_x86': 70, // CPU x86_64
'cpu_arm': 90 // CPU ARM64
};
@ -220,6 +224,7 @@ class DeterministicModelSelector {
const memoryHeadroomGB = inferredUnified ? 1.5 : 2;
const usableMemGB =
toNumber(input.usableMemGB) ??
toNumber(input.summary?.effectiveMemory) ??
Math.max(1, Math.min(utilizationFactor * totalMemGB, totalMemGB - memoryHeadroomGB));
const gpuCount =
@ -276,6 +281,7 @@ class DeterministicModelSelector {
}
const normalizedAcceleration = {
...acceleration,
supports_metal:
typeof acceleration.supports_metal === 'boolean'
? acceleration.supports_metal
@ -787,28 +793,9 @@ class DeterministicModelSelector {
const modalities = this.inferModalities(ollamaModel, variantTag);
const modelTags = this.inferTagsForVariant(derivedTags, variant, variantTag);
const sizeByQuant = {};
const variantIsCloud = this.isCloudVariantTag(variantTag);
for (const sibling of variants) {
const siblingTag = sibling.tag || fallbackTag;
if (this.isCloudVariantTag(siblingTag) !== variantIsCloud) continue;
const siblingQuant = this.resolveVariantQuantization(sibling, siblingTag);
const siblingParams = this.resolveVariantParamsB(ollamaModel, sibling, siblingQuant);
// Keep quantization map parameter-aware: don't blend 8B/70B/405B sizes.
if (Math.abs(siblingParams - paramsB) > 0.25) continue;
const siblingSize = this.extractVariantSizeGB(sibling, siblingParams);
if (!Number.isFinite(sizeByQuant[siblingQuant]) || siblingSize < sizeByQuant[siblingQuant]) {
sizeByQuant[siblingQuant] = siblingSize;
}
}
const availableQuantizations = this.getQuantizationCandidates({
availableQuantizations: this.extractAvailableQuantizations(ollamaModel, variants),
sizeByQuant
});
if (Number.isFinite(variantSizeGB) && variantSizeGB > 0) sizeByQuant[quant] = variantSizeGB;
const availableQuantizations = [quant];
const source = ollamaModel.source || 'ollama_database';
const registry = ollamaModel.registry || 'ollama.com';
@ -848,6 +835,8 @@ class DeterministicModelSelector {
sizeGB: variantSizeGB,
modalities,
tags: modelTags,
capabilities: [ollamaModel.primary_category, ...['capabilities', 'categories', 'use_cases']
.flatMap(key => Array.isArray(ollamaModel[key]) ? ollamaModel[key] : [])].filter(Boolean),
sourceTags: Array.isArray(ollamaModel.tags) ? ollamaModel.tags : [],
description: ollamaModel.description || '',
detailed_description: ollamaModel.detailed_description || '',
@ -1126,15 +1115,7 @@ class DeterministicModelSelector {
inferParamsFromArtifactSizeGB(sizeGB, quant = 'Q4_K_M') {
const normalizedQuant = this.normalizeQuantization(quant);
const bytesPerParam = {
'Q8_0': 1.05,
'Q6_K': 0.80,
'Q5_K_M': 0.68,
'Q4_K_M': 0.58,
'Q3_K': 0.48,
'Q2_K': 0.37
};
const bpp = bytesPerParam[normalizedQuant] || 0.58;
const bpp = precisionProfile(normalizedQuant).bytes || 2;
const inferred = sizeGB / bpp;
return Math.max(0.5, Math.round(inferred * 2) / 2);
}
@ -1146,6 +1127,8 @@ class DeterministicModelSelector {
resolveVariantQuantization(variant = {}, variantTag = '') {
const tagQuant = this.extractQuantizationFromTag(variantTag);
if (tagQuant) {
const declared = this.normalizeQuantization(variant.quantization || variant.quant);
if (/^Q[2-8]$/.test(tagQuant) && declared.startsWith(`${tagQuant}_`)) return declared;
return this.normalizeQuantization(tagQuant);
}
@ -1218,19 +1201,12 @@ class DeterministicModelSelector {
}
extractQuantizationFromTag(tag = '') {
const match = String(tag).match(/\b(q\d+[_\w]*)\b/i);
const match = String(tag).match(/\b((?:i?q\d+[_\w]*|bf16|fp(?:16|32|8)|f(?:16|32)))\b/i);
return match ? match[1].toUpperCase() : null;
}
normalizeQuantization(quant = 'Q4_K_M') {
const q = String(quant).toUpperCase();
if (q.startsWith('Q8')) return 'Q8_0';
if (q.startsWith('Q6')) return 'Q6_K';
if (q.startsWith('Q5')) return 'Q5_K_M';
if (q.startsWith('Q4')) return 'Q4_K_M';
if (q.startsWith('Q3')) return 'Q3_K';
if (q.startsWith('Q2')) return 'Q2_K';
return 'Q4_K_M';
normalizeQuantization(quant) {
return normalizePrecision(quant);
}
extractVariantSizeGB(variant, paramsB) {
@ -1487,13 +1463,7 @@ class DeterministicModelSelector {
// Phase 1: Estimation filter
const candidates = [];
const totalMem = hardware?.memory?.totalGB ?? hardware?.memory?.total ?? 8;
const usableMem = typeof hardware.usableMemGB === 'number'
? hardware.usableMemGB
: Math.max(1, Math.min(0.8 * totalMem, totalMem - 2));
const isUnified = Boolean(hardware?.gpu?.unified) || hardware?.gpu?.type === 'apple_silicon';
const vram = hardware?.gpu?.vramGB ?? hardware?.gpu?.vram ?? 0;
const budget = isUnified ? usableMem : (vram || usableMem);
const budget = memoryBudgetGB(hardware);
for (const model of filtered) {
const result = this.evaluateModel(
@ -1503,7 +1473,8 @@ class DeterministicModelSelector {
targetCtx,
budget,
optimizationObjective,
normalizedRuntime
normalizedRuntime,
{ contextPolicy: Number(options.targetCtx) > 0 ? 'required' : 'preferred' }
);
if (result) {
candidates.push(result);
@ -1575,8 +1546,10 @@ class DeterministicModelSelector {
return false;
}
// Guard against malformed external pool rows (a missing tags/modalities
// /name field used to throw and silently nuke the whole category).
const capability = capabilitiesOf(model);
if (category === 'embeddings') return capability.embedding && !capability.reranking;
if (category === 'reranking') return capability.reranking;
if (!capability.generation) return false;
const tags = Array.isArray(model.tags) ? model.tags : [];
const modalities = Array.isArray(model.modalities) ? model.modalities : [];
const name = String(model.name || model.model_identifier || '').toLowerCase();
@ -1610,13 +1583,18 @@ class DeterministicModelSelector {
});
}
evaluateModel(model, hardware, category, targetCtx, budget, optimizeFor = 'balanced', runtime = 'ollama') {
evaluateModel(model, hardware, category, targetCtx, budget, optimizeFor = 'balanced', runtime = 'ollama', options = {}) {
const nativeContext = Number(model.ctxMax);
const requiredContext = options.contextPolicy !== 'preferred';
if (requiredContext && (!(nativeContext > 0) || nativeContext < targetCtx)) return null;
const effectiveContext = nativeContext > 0 ? Math.min(nativeContext, targetCtx) : targetCtx;
if (!(budget > 0) || !Number.isFinite(budget)) return null;
// 1. Select best fitting quantization
const bestQuant = this.selectBestQuantization(model, budget, targetCtx);
const bestQuant = this.selectBestQuantization(model, budget, effectiveContext);
if (!bestQuant) return null;
// 2. Calculate required memory
const memoryEstimate = this.estimateMemoryBreakdown(model, bestQuant.quant, targetCtx);
const memoryEstimate = this.estimateMemoryBreakdown(model, bestQuant.quant, effectiveContext);
const requiredGB = memoryEstimate.requiredGB;
if (requiredGB > budget) return null;
@ -1659,6 +1637,8 @@ class DeterministicModelSelector {
return {
meta: model,
context: { requested: targetCtx, effective: effectiveContext, native: nativeContext > 0 ? nativeContext : null,
policy: requiredContext ? 'required' : 'preferred', limited: !(nativeContext >= targetCtx) },
quant: bestQuant.quant,
requiredGB: Math.round(requiredGB * 10) / 10,
estTPS: speedEstimate.estimatedTPS,
@ -1666,6 +1646,9 @@ class DeterministicModelSelector {
runtime: speedEstimate.runtime,
rationale,
memory: {
requiredGB,
budgetGB: budget,
fit: classifyFit(requiredGB, budget),
modelMemGB: Math.round(memoryEstimate.modelMemGB * 100) / 100,
kvCacheGB: Math.round(memoryEstimate.kvCacheGB * 100) / 100,
runtimeOverheadGB: Math.round(memoryEstimate.runtimeOverheadGB * 100) / 100,
@ -1676,6 +1659,7 @@ class DeterministicModelSelector {
},
speed: {
backend: speedEstimate.backend,
basis: speedEstimate.basis,
targetTPS: speedEstimate.targetTPS,
estimatedTPS: speedEstimate.estimatedTPS,
runtime: speedEstimate.runtime,
@ -1687,69 +1671,20 @@ class DeterministicModelSelector {
}
getQuantizationCandidates(model) {
const normalizedAvailable = Array.isArray(model?.availableQuantizations)
? model.availableQuantizations.map((quant) => this.normalizeQuantization(quant))
: [];
const fromSizeMap = model?.sizeByQuant && typeof model.sizeByQuant === 'object'
? Object.keys(model.sizeByQuant).map((quant) => this.normalizeQuantization(quant))
: [];
const seeded = (fromSizeMap.length > 0
? [...new Set(fromSizeMap)]
: [...new Set(normalizedAvailable)])
.filter(Boolean);
let candidates = seeded.length > 0 ? seeded : [...this.quantHierarchy];
// If we have at least one known quantization, allow extrapolating to
// *more compressed* levels as an explicit feasibility assumption.
if (seeded.length > 0) {
const expanded = new Set();
for (const quant of seeded) {
const idx = this.quantHierarchy.indexOf(quant);
if (idx === -1) {
expanded.add(quant);
continue;
}
for (let i = idx; i < this.quantHierarchy.length; i++) {
expanded.add(this.quantHierarchy[i]);
}
}
candidates = [...expanded];
}
return candidates.sort((a, b) => {
const aIdx = this.quantHierarchy.indexOf(a);
const bIdx = this.quantHierarchy.indexOf(b);
const safeA = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
const safeB = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
return safeA - safeB;
});
// A model/tag/file is an immutable artifact. Alternative files are separate rows.
if (model?.quant) return [this.normalizeQuantization(model.quant)];
const available = model?.availableQuantizations || Object.keys(model?.sizeByQuant || {});
if (!available.length && Number(model?.sizeGB) > 0) return ['UNKNOWN'];
return [...new Set(available.map(normalizePrecision))]
.sort((a, b) => (precisionProfile(b).bytes || 0) - (precisionProfile(a).bytes || 0));
}
selectBestQuantization(model, budget, targetCtx) {
const quantizationCandidates = this.getQuantizationCandidates(model);
// Try quantizations from best to worst quality
for (const quant of quantizationCandidates) {
for (const quant of this.getQuantizationCandidates(model)) {
const requiredGB = this.estimateRequiredGB(model, quant, targetCtx);
if (requiredGB <= budget) {
return { quant, sizeGB: requiredGB };
}
if (requiredGB <= budget) return { quant, sizeGB: requiredGB };
}
// If nothing fits at target context, try halving context once
const halfCtx = Math.floor(targetCtx / 2);
if (halfCtx >= 1024) {
for (const quant of quantizationCandidates) {
const requiredGB = this.estimateRequiredGB(model, quant, halfCtx);
if (requiredGB <= budget) {
return { quant, sizeGB: requiredGB };
}
}
}
return null; // Model doesn't fit
return null;
}
resolveMemoryParameterProfile(model = {}) {
@ -1757,18 +1692,8 @@ class DeterministicModelSelector {
}
estimateMemoryBreakdown(model, quant, ctx) {
// Bytes per parameter by quantization level (calibrated to real Ollama sizes)
// 7B Q4_K_M=~4.5GB, 14B Q4_K_M=~9GB, 32B Q4_K_M=~19GB
const bytesPerParam = {
'Q8_0': 1.05,
'Q6_K': 0.80,
'Q5_K_M': 0.68,
'Q4_K_M': 0.58,
'Q3_K': 0.48,
'Q2_K': 0.37
};
const normalizedQuant = this.normalizeQuantization(quant);
const bpp = bytesPerParam[normalizedQuant] || 0.63;
const bpp = precisionProfile(normalizedQuant).bytes;
const sizeByQuant = model?.sizeByQuant && typeof model.sizeByQuant === 'object' ? model.sizeByQuant : {};
const observedFromSizeMap = Number(sizeByQuant[normalizedQuant]);
const directVariantMatch =
@ -1790,7 +1715,7 @@ class DeterministicModelSelector {
parameterProfile.isMoE && Number.isFinite(parameterProfile.totalParamsB) && parameterProfile.totalParamsB > 0
? parameterProfile.totalParamsB
: parameterProfile.effectiveParamsB;
const modeledWeightGB = weightParamsB * bpp;
const modeledWeightGB = bpp == null ? Infinity : weightParamsB * bpp;
// A real observed artifact size always wins for weight memory — never let
// an MoE "sparse inference" assumption discard a measured on-disk size.
const useObservedArtifactSize = Number.isFinite(observedWeightGB) && observedWeightGB > 0;
@ -1831,7 +1756,7 @@ class DeterministicModelSelector {
const measured = this.lookupMeasuredQuality(model, category);
if (measured) {
let Qm = measured.score;
Qm += this.quantPenalties[quant] || -5; // benchmarks are run at fp16
Qm += this.quantPenalties[quant] ?? precisionProfile(quant).penalty; // benchmarks are run at fp16
Qm += this.calculateFreshnessAdjustment(model);
model.qualitySource = measured.provenance;
return Math.max(0, Math.min(100, Qm));
@ -1846,7 +1771,7 @@ class DeterministicModelSelector {
Q += familyBump;
// Quantization penalty
const quantPenalty = this.quantPenalties[quant] || -5;
const quantPenalty = this.quantPenalties[quant] ?? precisionProfile(quant).penalty;
Q += quantPenalty;
// Freshness/deprecation adjustment
@ -1884,6 +1809,12 @@ class DeterministicModelSelector {
* says nothing about vision, so it is never borrowed for it. A category
* with no matching benchmark returns null rather than a nearby proxy.
*/
invalidateQualityEvals() {
this.qualityConnection?.close();
this.qualityConnection = null;
this.qualityEvals = undefined;
}
lookupMeasuredQuality(model, category) {
// Attach lazily: the selector is constructed in several places that
// have no database handle, so it opens the shared catalog read-only on
@ -1903,6 +1834,7 @@ class DeterministicModelSelector {
quality.stats();
connection.prepare('SELECT 1 FROM catalog_families LIMIT 1').get();
this.qualityEvals = quality;
this.qualityConnection = connection;
}
} catch { connection?.close(); /* no benchmark data available */ }
}
@ -1928,7 +1860,7 @@ class DeterministicModelSelector {
let hit;
try {
hit = this.qualityEvals.lookup(model.name, model.paramsB, category);
hit = this.qualityEvals.lookup(model.model_identifier || model.name, model.paramsB, category, model);
} catch { return null; }
if (!hit?.evals?.length) return null;
@ -2004,7 +1936,7 @@ class DeterministicModelSelector {
return 0;
case 'multimodal':
if (model.modalities.includes('vision')) return 6;
if (model.modalities?.includes('vision')) return 6;
return 0;
case 'general':
@ -2039,14 +1971,8 @@ class DeterministicModelSelector {
}
estimateSpeedProfile(hardware, model, quant, category, runtime = 'ollama') {
// Determine backend
const cpuArchitecture = String(hardware.cpu?.architecture || '').toLowerCase();
const isArmCpu = /(?:arm64|aarch64|apple\s+silicon)/.test(cpuArchitecture);
let backend = 'cpu_x86';
if (hardware.acceleration.supports_metal) backend = 'metal';
else if (hardware.acceleration.supports_cuda) backend = 'cuda';
else if (isArmCpu) backend = 'cpu_arm';
const backend = detectedBackend(hardware);
// Base speed calculation
const K = this.backendK[backend];
const denseParamsB = Number.isFinite(this.parseBillionsValue(model.paramsB))
@ -2059,12 +1985,12 @@ class DeterministicModelSelector {
let base = K / effectiveParamsB;
// Quantization multiplier
const quantMultiplier = this.quantSpeedMultipliers[quant] || 1.0;
const quantMultiplier = this.quantSpeedMultipliers[quant] ?? precisionProfile(quant).speed;
base *= quantMultiplier;
// Threading multiplier
if (hardware.cpu.cores >= 8) base *= 1.1;
if (hardware.acceleration.supports_metal || hardware.acceleration.supports_cuda) base *= 1.2;
if (!backend.startsWith('cpu_')) base *= 1.2;
const acceleratorScale = this.calculateAcceleratorSpeedScale(hardware, backend);
base *= acceleratorScale.multiplier;
@ -2087,6 +2013,7 @@ class DeterministicModelSelector {
return {
backend,
basis: 'backend_profile_estimate',
targetTPS: target,
estimatedTPS,
score,
@ -2097,7 +2024,7 @@ class DeterministicModelSelector {
}
calculateAcceleratorSpeedScale(hardware = {}, backend = 'cpu_x86') {
if (backend !== 'cuda' && backend !== 'metal') {
if (!['cuda', 'metal', 'rocm', 'vulkan', 'sycl'].includes(backend)) {
return { multiplier: 1, reason: null };
}
@ -2121,7 +2048,7 @@ class DeterministicModelSelector {
else if (acceleratorMemoryGB >= 48) multiplier *= 1.7;
else if (acceleratorMemoryGB >= 24) multiplier *= 1.15;
if (backend === 'cuda' && gpuCount > 1) {
if (['cuda', 'rocm'].includes(backend) && gpuCount > 1) {
multiplier *= Math.min(1.8, 1 + ((gpuCount - 1) * 0.25));
}
@ -2145,9 +2072,8 @@ class DeterministicModelSelector {
const ctxMax = Number(model?.ctxMax) || 0;
if (ctxMax >= targetCtx) return 100;
if (ctxMax >= targetCtx * 0.5) return 70;
// Context is NOT pre-filtered: a model that cannot serve the requested
// context still scores here (0 for this component) and stays eligible,
// weighted down rather than excluded.
// Required contexts are filtered before scoring. Preferred contexts
// contribute a lower score when the model window is shorter.
return 0;
}
@ -2333,7 +2259,7 @@ class DeterministicModelSelector {
// Special attributes
if (model.tags.includes('coder')) parts.push('coder-tuned');
if (model.modalities.includes('vision')) parts.push('vision-capable');
if (model.modalities?.includes('vision')) parts.push('vision-capable');
if (model.isDeprecated) parts.push('deprecated penalized');
else if (model.isStale) parts.push('stale penalized');
else if (model.freshnessScore >= 90) parts.push('fresh release');
@ -2370,8 +2296,8 @@ class DeterministicModelSelector {
}
// Backend
if (hardware.acceleration.supports_metal) parts.push('Metal backend');
else if (hardware.acceleration.supports_cuda) parts.push('CUDA backend');
const backend = speedEstimate?.backend || detectedBackend(hardware);
parts.push(`${backend.replace('_', ' ').toUpperCase()} speed estimate`);
return parts.join(', ');
}
@ -2543,6 +2469,10 @@ class DeterministicModelSelector {
tags: candidate.meta.tags || [],
quantization: candidate.quant,
estimatedRAM: candidate.requiredGB,
memory: candidate.memory,
context: candidate.context,
speed: candidate.speed,
artifact: candidate.meta.artifact,
reasoning: candidate.rationale,
runtime: candidate.runtime || candidate.speed?.runtime || 'ollama',
installCommand: candidate.meta.installCommand || provenance.install_command || '',
@ -2649,7 +2579,7 @@ class DeterministicModelSelector {
const tags = model.tags || [];
if (tags.includes('coder') || name.includes('code')) return 'coding';
if (tags.includes('vision') || (model.modalities && model.modalities.includes('vision'))) return 'multimodal';
if (tags.includes('vision') || (model.modalities && model.modalities?.includes('vision'))) return 'multimodal';
if (tags.includes('embed')) return 'embeddings';
if (name.includes('creative') || name.includes('wizard')) return 'creative';

View file

@ -0,0 +1,71 @@
// Shared by the selector and desktop. Bump when the public ranking contract changes.
const RANKING_CONTRACT_VERSION = 1;
function normalizePrecision(value) {
const precision = String(value || 'UNKNOWN').trim().toUpperCase();
return ({ F16: 'FP16', FLOAT16: 'FP16', FLOAT32: 'FP32', F32: 'FP32',
BFLOAT16: 'BF16' })[precision] || precision;
}
// Profiles describe estimates; they never rename or manufacture an artifact.
function precisionProfile(value) {
const precision = normalizePrecision(value);
if (precision === 'FP32') return { bytes: 4, penalty: 0, speed: 0.35 };
if (['FP16', 'BF16'].includes(precision)) return { bytes: 2, penalty: 0, speed: 0.55 };
const bits = precision.match(/^(?:I?Q|INT|FP)([2-8])(?:\b|_)/)?.[1];
return ({
8: { bytes: 1.05, penalty: 0, speed: 0.8 },
6: { bytes: 0.80, penalty: -1, speed: 0.95 },
5: { bytes: 0.68, penalty: -2, speed: 1 },
4: { bytes: 0.58, penalty: -5, speed: 1.15 },
3: { bytes: 0.48, penalty: -8, speed: 1.25 },
2: { bytes: 0.37, penalty: -12, speed: 1.35 }
})[bits] || { bytes: null, penalty: -5, speed: 1 };
}
function capabilitiesOf(model = {}) {
const values = ['capabilities', 'tasks', 'tags', 'repoTags', 'sourceTags']
.flatMap(key => Array.isArray(model[key]) ? model[key] : [])
.map(value => String(value).toLowerCase());
const name = String(model.model_identifier || model.name || '').toLowerCase();
const embedding = values.some(v => /^(embeddings?|feature-extraction|sentence-similarity)$/.test(v)) ||
model.specialization === 'embeddings' || /(?:embed|\bbge-|all-minilm)/.test(name);
const reranking = values.some(v => /rerank|text-ranking/.test(v)) || /rerank/.test(name);
const generation = !embedding && !reranking && values.some(v =>
/^(text-generation|text2text-generation|image-text-to-text|generation|completion|chat|instruct|coder|code|coding|reasoning|general|creative|talking|reading|summarization|vision|multimodal)$/.test(v));
const vision = generation && (values.some(v => /^(vision|multimodal|image-text-to-text)$/.test(v)) ||
model.modalities?.includes('vision'));
return { generation, embedding, reranking, vision };
}
function detectedBackend(hardware = {}) {
const acceleration = hardware.acceleration || {};
if (!hardware.cpuOnly) {
const preferred = hardware.summary?.bestBackend;
if (['metal', 'cuda', 'rocm', 'vulkan', 'sycl'].includes(preferred) &&
acceleration[`supports_${preferred}`] !== false) return preferred;
for (const backend of ['metal', 'cuda', 'rocm', 'vulkan', 'sycl']) {
if (acceleration[`supports_${backend}`]) return backend;
}
}
return /arm64|aarch64|apple silicon/i.test(hardware.cpu?.architecture || '') ? 'cpu_arm' : 'cpu_x86';
}
function memoryBudgetGB(hardware = {}) {
const positive = value => Number.isFinite(Number(value)) && Number(value) > 0 ? Number(value) : null;
const ram = positive(hardware.memory?.totalGB ?? hardware.memory?.total ?? hardware.summary?.systemRAM);
const usable = positive(hardware.usableMemGB ?? hardware.summary?.effectiveMemory) ??
(ram ? Math.max(0, Math.min(ram * 0.8, ram - 2)) : null);
const unified = hardware.gpu?.unified || hardware.gpu?.type === 'apple_silicon' ||
(hardware.summary?.hasIntegratedGPU && !hardware.summary?.hasDedicatedGPU);
if (unified || hardware.cpuOnly || detectedBackend(hardware).startsWith('cpu_')) return usable;
return positive(hardware.gpu?.vramGB ?? hardware.gpu?.totalVRAM ?? hardware.summary?.totalVRAM) ?? usable;
}
function classifyFit(requiredGB, budgetGB) {
if (!Number.isFinite(requiredGB) || requiredGB <= 0 || !Number.isFinite(budgetGB) || budgetGB <= 0) return 'unknown';
return requiredGB > budgetGB ? 'over' : requiredGB > budgetGB * 0.9 ? 'tight' : 'fits';
}
module.exports = { RANKING_CONTRACT_VERSION, normalizePrecision, precisionProfile, capabilitiesOf,
detectedBackend, memoryBudgetGB, classifyFit };

View file

@ -78,6 +78,7 @@ function slugifyModelName(text = '') {
function getRuntimeModelRef(model = {}, runtime = 'ollama') {
const normalized = resolveCommandRuntime(model, runtime);
if (!normalized) return null;
if (normalized === 'ollama' && model.artifact?.source_id && model.artifact.source_id !== 'ollama') return null;
const candidates = [
model.hfModel,
@ -158,6 +159,12 @@ function getRuntimePullCommand(model = {}, runtime = 'ollama') {
if (normalized === 'llama.cpp') {
const file = getGgufFilename(model);
if (!file || !modelRef.includes('/')) return null;
if (model.artifact?.shard_files?.length) {
return model.artifact.shard_files.map(shard => {
const url = `https://huggingface.co/${modelRef}/resolve/main/${shard.split('/').map(encodeURIComponent).join('/')}`;
return `curl --fail --location ${shellEscapeArg(url)} --output ${shellEscapeArg(`./${shard.split('/').pop()}`)}`;
}).join(' && ');
}
const url = model.artifact?.download_url || model.downloadUrl ||
`https://huggingface.co/${modelRef}/resolve/main/${file.split('/').map(encodeURIComponent).join('/')}`;
return `curl --fail --location ${shellEscapeArg(url)} --output ${shellEscapeArg(`./${file.split('/').pop()}`)}`;
@ -182,15 +189,19 @@ function getRuntimeRunCommand(model = {}, runtime = 'ollama') {
if (normalized === 'llama.cpp') {
const file = getGgufFilename(model);
if (!file) return null;
return `llama-cli --model ${shellEscapeArg(model.localPath || `./${file.split('/').pop()}`)} --prompt "Hello" --n-predict 64 --single-turn`;
const context = Number(model.context?.effective);
const contextOption = Number.isSafeInteger(context) && context > 0 ? ` --ctx-size ${context}` : '';
return `llama-cli --model ${shellEscapeArg(model.localPath || `./${file.split('/').pop()}`)}${contextOption} --prompt "Hello" --n-predict 64 --single-turn`;
}
if (normalized === 'transformers') {
const script = 'import sys; from transformers import pipeline; print(pipeline("text-generation", model=sys.argv[1])("Hello", max_new_tokens=64)[0]["generated_text"])';
const script = 'import sys, torch; from transformers import pipeline; device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"; print(pipeline("text-generation", model=sys.argv[1], dtype="auto", device=device)("Hello", max_new_tokens=64)[0]["generated_text"])';
return `python -c ${shellEscapeArg(script)} ${shellEscapeArg(modelRef)}`;
}
if (normalized === 'vllm') {
return `python -m vllm.entrypoints.openai.api_server --model ${shellEscapeArg(modelRef)} --host 0.0.0.0 --port 8000`;
const context = Number(model.context?.effective);
const contextOption = Number.isSafeInteger(context) && context > 0 ? ` --max-model-len ${context}` : '';
return `python -m vllm.entrypoints.openai.api_server --model ${shellEscapeArg(modelRef)}${contextOption} --host 0.0.0.0 --port 8000`;
}
if (normalized === 'mlx') {

View file

@ -433,7 +433,7 @@ async function testRecommendationSurfaces() {
name: 'Dolphin Uncensored 1B',
paramsB: 1,
sizeGB: 1,
tags: ['uncensored']
tags: ['uncensored', 'instruct']
}
];
const safeCpuRanking = await rankModels(safetyPool, raw, {
@ -1074,7 +1074,10 @@ function testCliHelpAndJsonSmoke() {
);
const registryMlx = JSON.parse(stripAnsi(registryMlxResult.stdout));
assert.strictEqual(registryMlx.runtime, 'ollama');
assert.ok(registryMlx.recommendations.length > 0);
assert.ok(Array.isArray(registryMlx.recommendations));
// A local registry may have no native Ollama artifacts. HF GGUF files
// must not be substituted as if their filenames were Ollama pull tags.
assert.ok(registryMlx.recommendations.every(recommendation => recommendation.source === 'ollama'));
assert.ok(
registryMlx.recommendations.every((recommendation) => recommendation.runtime === 'ollama'),
'an explicit MLX registry request must fall back to CPU-compatible Ollama artifacts'

View file

@ -0,0 +1,70 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Core, toCard } = require('../desktop/src/main/core');
const ModelDatabase = require('../src/data/model-database');
const { QualityEvals, SOURCES } = require('../src/data/quality-evals');
async function run() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ranking-desktop-'));
const dbPath = path.join(directory, 'models.db');
const db = new ModelDatabase({ dbPath, seedDbPath: path.join(directory, 'none'), disableRegistrySeedImport: true });
try {
await db.initialize();
db.upsertModel({ id: 'test', name: 'test' });
new QualityEvals(db).refreshCatalogCohort([{ name: 'test' }]);
db.close();
let scans = 0;
let invalidations = 0;
const requests = [];
const core = new Core({
dbPath,
detectRuntimes: async () => [],
fetchImpl: async url => { requests.push(url); return new Response('unavailable', { status: 503 }); },
checkerFactory: () => ({
intelligentRecommender: { invalidateQualityEvals() { invalidations++; } },
async analyze() {
scans++;
return {
hardware: { cpu: { cores: 8 }, memory: { totalGB: 26 },
gpu: { unified: true, vramGB: 0.25 }, summary: { effectiveMemory: 13 } },
recommendations: { recommendations: { general: { bestModels: [{
model_name: 'test', model_identifier: 'test:7b', runtime: 'ollama',
estimatedRAM: 6, quantization: 'Q4_K_M', size: 7
}] } } }
};
}
})
});
const initial = await core.scan();
assert.strictEqual(initial.phase, 'ready');
assert.strictEqual(initial.budgetGB, 13);
assert.strictEqual(initial.groups[0].models[0].fit, 'fits');
assert.strictEqual(toCard({ estimatedRAM: 100 }, null).fit, 'unknown');
const known = toCard({ estimatedRAM: 6, runtime: 'llama.cpp',
model_identifier: 'model-Q4_K_M.gguf',
artifact: { repo_id: 'author/model', filename: 'model-Q4_K_M.gguf' },
memory: { budgetGB: 13 }, context: { effective: 2048, limited: true }
}, 0.25, [{ id: 'ollama', installed: true }], {});
assert.strictEqual(known.fit, 'fits');
assert.strictEqual(known.runtime, 'llama.cpp', 'desktop must retain the scored runtime');
assert.ok(known.commands.pull.includes('author/model/resolve/main/model-Q4_K_M.gguf'));
assert.ok(known.commands.run.includes('--ctx-size 2048'));
const refresh = await core.runAction('refresh-benchmarks');
assert.deepStrictEqual(refresh.rows.map(row => row[0]), Object.keys(SOURCES));
assert.ok(requests.length >= Object.keys(SOURCES).length);
assert.ok(refresh.rows.every(row => row[2] !== 'ok'), 'source errors must remain visible');
assert.strictEqual(invalidations, 1);
assert.strictEqual(scans, 2, 'refresh must recalculate recommendations');
assert.strictEqual(core.snapshot().phase, 'ready');
await core.runAction('coverage');
await Promise.all([core.scan(), core.scan()]);
assert.strictEqual(scans, 4, 'scans must serialize without dropping work');
console.log('desktop-core.test.js: OK');
} finally {
db.close();
fs.rmSync(directory, { recursive: true, force: true });
}
}
run().catch(error => { console.error(error); process.exitCode = 1; });

View file

@ -543,7 +543,7 @@ async function runMultiGpuNormalizationUsesCombinedVram() {
const recommendations = await selector.getBestModelsForHardware(hardware, allModels);
const reasoningIds = (recommendations.reasoning?.bestModels || []).map((m) => m.model_identifier);
assert.ok(reasoningIds.includes('deepfit:70b'), '70B reasoning variant should be viable with 36GB aggregate VRAM');
assert.ok(!reasoningIds.includes('deepfit:70b'), '43GB artifact must not fit 36GB by inventing a lower precision');
}
async function runMultiGpu36GbIncludesThirtyBWhenFeasible() {
@ -897,9 +897,9 @@ async function runCloudVariantsDoNotContaminateLocalArtifactSizes() {
assert.ok(q8, 'q8 variant should be converted');
assert.strictEqual(latest.paramsB, 80, 'Model-size metadata should override the old 7B fallback');
assert.strictEqual(latest.sizeByQuant.Q4_K_M, 52, 'Local Q4 artifact size should stay at 52GB');
assert.strictEqual(latest.sizeByQuant.Q8_0, 85, 'Local Q8 artifact size should be preserved separately');
assert.strictEqual(cloud.sizeByQuant.Q4_K_M, 1, 'Cloud artifact size must not leak into local variants');
assert.strictEqual(latest.sizeByQuant.Q4_0, 52, 'Local Q4 artifact size should stay at 52GB');
assert.strictEqual(q8.sizeByQuant.Q8_0, 85, 'Q8 size must stay with its own artifact');
assert.strictEqual(cloud.sizeByQuant.Q4_0, 1, 'Cloud artifact size must not leak into local variants');
assert.strictEqual(q8.quant, 'Q8_0', 'Variant tag should win over incorrect generic quantization metadata');
}

View file

@ -14,6 +14,7 @@ function run() {
[
cliPath,
'recommend',
'--simulate', 'rtx4090',
'--no-verbose',
'--runtime',
'vllm',
@ -37,6 +38,13 @@ function run() {
assert.ok(output.includes('hf download '), 'recommend should emit Hugging Face download commands');
assert.ok(output.includes('Source: huggingface'), 'recommend should surface Hugging Face picks');
const tooSmall = spawnSync(process.execPath, [cliPath, 'recommend', '--runtime', 'vllm',
'--gpu', 'RTX 4090', '--ram', '4', '--vram', '1', '--no-verbose', '--category', 'coding'], {
encoding: 'utf8', timeout: 30000, env: { ...process.env, HOME: tempHome }
});
assert.strictEqual(tooSmall.status, 0, tooSmall.stderr);
assert.ok(!tooSmall.stdout.includes('ollama pull'), 'an empty vLLM ranking must not fall back to Ollama artifacts');
console.log('[OK] model-registry-main-flow.test.js passed');
} finally {
fs.rmSync(tempHome, { recursive: true, force: true });

View file

@ -179,14 +179,9 @@ test('a size the board did publish and disagrees with is refused', async () => {
assert.strictEqual(q.lookup('qwen2.5-coder', 70, 'coding'), null);
});
test('a row with no published size matches the family and says so', async () => {
// LiveBench publishes 'deepseek-r1' with no size. Refusing it loses a real
// measurement, so it is admitted and flagged instead.
test('a row without a published size cannot score a smaller checkpoint', async () => {
const { q } = await seed();
const hit = q.lookup('deepseek-r1', 14, 'reasoning');
assert.ok(hit, 'an unsized row should still match its family');
assert.strictEqual(hit.sizeUnknown, true);
assert.strictEqual(hit.evals[0].score, 88.6);
assert.strictEqual(q.lookup('deepseek-r1', 14, 'reasoning'), null);
});
test('a size-matched hit is not flagged as family-only', async () => {
@ -204,6 +199,22 @@ test('an unknown model returns null rather than a nearest guess', async () => {
assert.strictEqual(q.lookup('not-a-real-model', 7, 'coding'), null);
});
test('base, instruct, revisions and adjacent sizes never share measurements', async () => {
const { db, q } = await seed();
const insert = db.prepare(`INSERT INTO quality_evals
(source_id, bench_model_name, family_key, params_b, variant_role, metric, category, raw_score)
VALUES ('test', ?, 'qwen2.5', ?, ?, 'hf_mmlu_pro', 'general', ?)`);
insert.run('Qwen/Qwen2.5-7B', 7.61, 'base', 43.65);
insert.run('Qwen/Qwen2.5-7B-Instruct', 7.61, 'instruct', 42.86);
insert.run('Qwen/Qwen2.5-8B-Instruct', 8, 'instruct', 70);
insert.run('Qwen/Qwen2.5-7B-Instruct-1M', 7.61, 'instruct', 35.04);
assert.strictEqual(q.lookup('Qwen/Qwen2.5-7B', 7, 'general').evals[0].score, 43.65);
assert.strictEqual(q.lookup('Qwen/Qwen2.5-7B-Instruct', 7, 'general').evals[0].score, 42.86);
assert.strictEqual(q.lookup('Qwen/Qwen2.5-7B-Instruct-preview', 7, 'general'), null);
assert.strictEqual(q.lookup('other/Qwen2.5-7B-Instruct', 7, 'general'), null);
assert.strictEqual(q.lookup('Qwen/Qwen2.5-7B-Instruct', 70, 'general'), null);
});
/* ---------------- cohort percentile ---------------- */
test('the percentile cohort is the local catalog, not the whole board', async () => {
@ -339,24 +350,24 @@ test('real SNAPPY Parquet decoding ingests both sources, preserves provenance an
const { q } = await seed();
assert.strictEqual((await q.ingest('hf_open_llm', { fetchImpl: fixtureFetch })).rows, 30);
assert.strictEqual((await q.ingest('lmarena', { fetchImpl: fixtureFetch })).rows, 10);
const general = q.lookup('qwen2.5', 7, 'general');
const general = q.lookup('Qwen2.5-7B-Instruct', 7, 'general');
assert.strictEqual(general.evals.length, 2);
const elo = general.evals.find((r) => r.source === 'lmarena');
assert.strictEqual(elo.score, 1000);
assert.strictEqual(elo.scoreUnit, 'elo');
assert.strictEqual(elo.scaleMax, null);
assert.strictEqual(elo.sourceUrl, SOURCES.lmarena.homepage);
assert.strictEqual(q.lookup('qwen2.5', 70, 'general'), null);
assert.strictEqual(q.lookup('qwen2.5', 7, 'coding'), null);
assert.strictEqual(q.lookup('Qwen2.5-70B-Instruct', 70, 'general'), null);
assert.strictEqual(q.lookup('Qwen2.5-7B-Instruct', 7, 'coding'), null);
assert.ok(q.stats().sources.every((source) => source.payload_sha256?.length === 64 || source.id === 'test'));
});
test('a sized result for another task cannot hide an unsized result for the requested task', async () => {
test('an unsized row remains ineligible even when another task publishes a size', async () => {
const { db, q } = await seed();
db.prepare(`INSERT INTO quality_evals
(source_id, bench_model_name, family_key, params_b, metric, category, raw_score)
VALUES ('test', 'deepseek-r1-14b', 'deepseek-r1', 14, 'code', 'coding', 50)`).run();
assert.strictEqual(q.lookup('deepseek-r1', 14, 'reasoning').sizeUnknown, true);
assert.strictEqual(q.lookup('deepseek-r1', 14, 'reasoning'), null);
});
test('the registry consumes its configured database on both engines and reports measured task scores', async () => {
@ -367,7 +378,7 @@ test('the registry consumes its configured database on both engines and reports
const { RegistryRecommender } = require('../src/data/registry-recommender');
const recommender = new RegistryRecommender({ database: q.modelDatabase });
await recommender.initialize();
const model = { name: 'qwen2.5', paramsB: 7 };
const model = { name: 'Qwen2.5-7B-Instruct', paramsB: 7 };
for (const category of ['general', 'talking', 'reasoning']) {
const hit = recommender.selector.lookupMeasuredQuality(model, category);
assert.ok(hit && hit.score >= 45 && hit.score <= 95);

View file

@ -0,0 +1,91 @@
const assert = require('assert');
const Selector = require('../src/models/deterministic-selector');
const { artifactToSelectorModel, groupWeightShards } = require('../src/data/registry-recommender');
const { precisionProfile, memoryBudgetGB, classifyFit } = require('../src/models/ranking-contract');
const { checkpointIdentity, sameCheckpoint } = require('../src/data/checkpoint-identity');
const { getRuntimeCommandSet } = require('../src/runtime/runtime-support');
const selector = new Selector();
selector.qualityEvals = null;
const hardware = selector.normalizeHardwareProfile({
cpu: { architecture: 'x64', cores: 8 }, memory: { totalGB: 32 },
gpu: { type: 'nvidia', vramGB: 8 }, acceleration: { supports_cuda: true }
});
const base = { name: 'Qwen/Qwen2.5-7B-Instruct', paramsB: 7, tags: ['instruct'],
ctxMax: 32768, quant: 'FP16', family: 'qwen2.5' };
for (const quant of ['FP16', 'BF16', 'FP32']) {
assert.strictEqual(selector.normalizeQuantization(quant), quant);
assert.ok(selector.estimateRequiredGB({ ...base, quant }, quant, 4096) > 14);
assert.strictEqual(selector.evaluateModel({ ...base, quant }, hardware, 'general', 4096, 8), null);
}
assert.strictEqual(precisionProfile('IQ4_XS').bytes, 0.58);
assert.strictEqual(selector.estimateRequiredGB({ ...base, quant: 'unknown' }, 'unknown', 4096), Infinity);
const fixed = { ...base, quant: 'Q8_0', model_identifier: 'qwen:7b-q8_0',
availableQuantizations: ['Q8_0', 'Q4_K_M'], sizeByQuant: { Q8_0: 7.6, Q4_K_M: 4.5 } };
assert.deepStrictEqual(selector.getQuantizationCandidates(fixed), ['Q8_0']);
assert.strictEqual(selector.evaluateModel(fixed, hardware, 'general', 4096, 6), null);
const candidate = selector.evaluateModel(fixed, hardware, 'general', 4096, 9);
assert.strictEqual(candidate.quant, 'Q8_0');
assert.strictEqual(selector.mapCandidateToLegacyFormat(candidate).model_identifier, 'qwen:7b-q8_0');
const shard = { source_id: 'huggingface', repo_id: 'test/model-7B-GGUF',
canonical_model_id: 'test/model-7B-GGUF', repo_tasks: ['text-generation'], format: 'gguf',
quantization: 'Q8_0', size_gb: 4, parameter_count_b: 7, context_length: 4096,
runtime_support: ['ollama', 'llama.cpp'] };
const shards = [1, 2].map(i => ({ ...shard, filename: `model-Q8_0-0000${i}-of-00002.gguf` }));
assert.strictEqual(groupWeightShards(shards.slice(1)).length, 0, 'incomplete shard sets are not installable');
const grouped = groupWeightShards(shards);
assert.strictEqual(grouped.length, 1);
const model = artifactToSelectorModel(grouped[0]);
assert.strictEqual(model.sizeGB, 8, 'all shard sizes must count');
assert.strictEqual(model.preferredRuntime, 'llama.cpp');
const commands = getRuntimeCommandSet({ ...model, context: { effective: 4096 } }, 'auto');
for (const part of shards) assert.ok(commands.pull.includes(part.filename), 'download must include every shard');
assert.ok(commands.run.includes(shards[0].filename));
assert.ok(commands.run.includes('--ctx-size 4096'));
assert.strictEqual(getRuntimeCommandSet(model, 'ollama').pull, null, 'a HF filename is not an Ollama pull tag');
for (const category of ['general', 'talking', 'creative', 'reading', 'coding', 'reasoning']) {
const models = [base, { ...base, name: 'bge-m3', tags: ['embedding', 'instruct'] },
{ ...base, name: 'test-reranker', tags: ['text-ranking', 'instruct'] }, { name: 'unknown' }];
assert.deepStrictEqual(selector.filterByCategory(models, category), [base]);
}
const scores = ['Q8_0', 'Q6_K', 'Q5_K_M', 'Q4_K_M'].map(q => selector.calculateQualityPrior({ ...base }, q, 'general'));
assert.ok(scores.every((score, i) => i === 0 || scores[i - 1] >= score), 'precision must not lose quality through a falsy zero');
selector.lookupMeasuredQuality = () => ({ score: 80, provenance: { kind: 'measured' } });
assert.strictEqual(selector.calculateQualityPrior({ ...base }, 'Q8_0', 'general'), 80);
assert.strictEqual(selector.calculateQualityPrior({ ...base }, 'Q6_K', 'general'), 79);
for (const backend of ['rocm', 'vulkan', 'sycl']) {
const hw = { ...hardware, acceleration: { [`supports_${backend}`]: true } };
const speed = selector.estimateSpeedProfile(hw, base, 'Q4_K_M', 'general');
assert.strictEqual(speed.backend, backend);
assert.strictEqual(selector.estimateSpeedProfile({ ...hw, cpuOnly: true }, base, 'Q4_K_M', 'general').backend, 'cpu_x86');
}
const smallContext = { ...base, quant: 'Q4_K_M', ctxMax: 2048 };
assert.strictEqual(selector.evaluateModel(smallContext, hardware, 'reading', 32768, 8), null);
assert.strictEqual(selector.evaluateModel({ ...smallContext, ctxMax: null }, hardware, 'reading', 32768, 8), null);
const preferred = selector.evaluateModel(smallContext, hardware, 'reading', 32768, 8, 'balanced', 'ollama', { contextPolicy: 'preferred' });
assert.strictEqual(preferred.context.effective, 2048);
assert.strictEqual(preferred.context.limited, true);
assert.strictEqual(preferred.memory.requiredGB, selector.estimateRequiredGB(smallContext, 'Q4_K_M', 2048));
assert.strictEqual(memoryBudgetGB({ memory: { totalGB: 26 }, gpu: { unified: true, vramGB: 0.25 }, summary: { effectiveMemory: 13 } }), 13);
assert.strictEqual(classifyFit(null, 8), 'unknown');
assert.strictEqual(classifyFit(100, null), 'unknown');
assert.strictEqual(memoryBudgetGB({}), null);
const identity = (name, params = 7, metadata) => checkpointIdentity(name, params, metadata);
assert.ok(!sameCheckpoint(identity('Qwen/Qwen2.5-7B'), identity('Qwen/Qwen2.5-7B-Instruct')));
assert.ok(!sameCheckpoint(identity('Qwen/Qwen2.5-7B-Instruct'), identity('Qwen/Qwen2.5-8B-Instruct', 8)));
assert.ok(!sameCheckpoint(identity('deepseek-r1', 14), identity('deepseek-r1-0528', null)));
assert.ok(!sameCheckpoint(identity('test-7b-thinking'), identity('test-7b')));
assert.ok(!sameCheckpoint(identity('Qwen/Qwen2.5-7B-Instruct'), identity('Qwen/Qwen2.5-7B-Instruct-1M')));
assert.ok(!sameCheckpoint(identity('other/Qwen2.5-7B'), identity('Qwen/Qwen2.5-7B')));
assert.ok(!sameCheckpoint(identity('other/Qwen2.5-7B-Instruct'), identity('Qwen2.5-7B-Instruct')));
assert.ok(!sameCheckpoint(identity('qwen2.5:7b'), identity('Qwen/Qwen2.5-7B')));
assert.ok(sameCheckpoint(identity('qwen2.5:7b-base'), identity('Qwen/Qwen2.5-7B')));
assert.ok(sameCheckpoint(identity('qwen2.5:7b-instruct', 7, { artifact: { source_id: 'ollama', repo_id: 'qwen2.5' } }), identity('Qwen/Qwen2.5-7B-Instruct')));
const alias = identity('quantizer/Qwen2.5-7B-Instruct-GGUF', 7, { repoTags: ['base_model:quantized:Qwen/Qwen2.5-7B-Instruct'] });
assert.ok(sameCheckpoint(alias, identity('Qwen/Qwen2.5-7B-Instruct')));
const finetune = identity('quantizer/Qwen2.5-7B-Instruct-GGUF', 7, { repoTags: ['base_model:finetune:Qwen/Qwen2.5-7B-Instruct'] });
assert.ok(!sameCheckpoint(finetune, identity('Qwen/Qwen2.5-7B-Instruct')));
console.log('ranking-contract.test.js: OK');

View file

@ -7,6 +7,9 @@ const { spawnSync } = require('child_process');
const path = require('path');
const TESTS = [
{ name: 'Ranking contracts', file: 'ranking-contract.test.js', category: 'Recommendations' },
{ name: 'Desktop ranking bridge', file: 'desktop-core.test.js', category: 'UI' },
{ name: 'Desktop runtime registry', file: '../desktop/test/runtimes.test.js', category: 'UI' },
{ name: 'Integrated GPU memory units', file: 'hardware-integrated-memory-units.test.js', category: 'Hardware' },
{ name: 'Parallel hardware probes', file: 'hardware-parallel-probes.test.js', category: 'Hardware' },
{ name: 'AMD GPU detection', file: 'amd-gpu-detection.test.js', category: 'Hardware' },

View file

@ -55,7 +55,7 @@ function testFilterByCategoryToleratesMalformedRows() {
assert.doesNotThrow(() => s.filterByCategory([{ model_identifier: 'x' }], 'coding'));
assert.doesNotThrow(() => s.filterByCategory([{ model_identifier: 'x' }], 'multimodal'));
const general = s.filterByCategory([{ model_identifier: 'x' }], 'general');
assert.strictEqual(general.length, 1, 'a malformed row still passes the general category');
assert.strictEqual(general.length, 0, 'unknown capabilities must not imply text generation');
}
function testLatestVariantDoesNotPoisonSiblingSize() {