llm-checker/src/models/deterministic-selector.js

2802 lines
116 KiB
JavaScript

/**
* LLM-Checker: Deterministic Model Selection Algorithm (Spec v1.0)
*
* A two-phase selector that picks the best Ollama model + quantization
* 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');
const { applyCpuOnlyOverride, resolveCpuOnlyMode } = require('../hardware/cpu-only');
const { spawn } = require('child_process');
const OllamaClient = require('../ollama/client');
const { DETERMINISTIC_WEIGHTS } = require('./scoring-config');
const { filterModelsBySafety, isUncensoredModel } = require('./model-safety');
const {
parseBillionsValue: parseMoEBillionsValue,
parsePositiveNumber: parseMoEPositiveNumber,
normalizeMoERuntime,
extractMoEMetadata: extractCanonicalMoEMetadata,
resolveMoEParameterProfile,
estimateMoESpeedMultiplier
} = require('./moe-assumptions');
class DeterministicModelSelector {
constructor(options = {}) {
this.hasExplicitCpuOnly = Object.prototype.hasOwnProperty.call(options, 'cpuOnly');
this.cpuOnly = resolveCpuOnlyMode(options.cpuOnly);
this.activeCpuOnly = this.cpuOnly;
this.catalogPath = path.join(__dirname, 'catalog.json');
this.benchCachePath = path.join(os.homedir(), '.llm-checker', 'bench.json');
this.ollamaClient = options.ollamaClient || new OllamaClient({ cpuOnly: this.cpuOnly });
this.ollamaCachePaths = [
path.join(os.homedir(), '.llm-checker', 'cache', 'ollama', 'ollama-detailed-models.json'),
path.join(__dirname, '../ollama/.cache/ollama-detailed-models.json')
];
// Quality priors table
this.baseQualityByParams = {
0.5: 45, 1: 45, 1.5: 45,
2: 60, 3: 60, 4: 60,
7: 75, 8: 75, 9: 75,
13: 82, 14: 82, 15: 82,
30: 89, 32: 89, 34: 89,
70: 95, 72: 95
};
// Family quality bumps
this.familyBumps = {
'qwen2.5': 2,
'qwen3': 4,
'gemma3': 3,
'deepseek': 3,
'deepseek-r1': 5,
'deepseek-coder': 4,
'mistral': 1,
'llama3.1': 1,
'llama3.2': 2,
'gemma2': 1,
'yi': -3,
'yi-coder': 1,
'phi-3': 0,
'granite': 0,
'solar': 0,
'starcoder': 1,
'minicpm': 0,
'llava': 0
};
// Quantization penalties
this.quantPenalties = {
'Q8_0': 0,
'Q6_K': -1,
'Q5_K_M': -2,
'Q4_K_M': -5,
'Q3_K': -8,
'Q2_K': -12
};
// Quantization hierarchy (best to worst)
this.quantHierarchy = ['Q8_0', 'Q6_K', 'Q5_K_M', 'Q4_K_M', 'Q3_K', 'Q2_K'];
// Quantization speed multipliers
this.quantSpeedMultipliers = {
'Q8_0': 0.8,
'Q6_K': 0.95,
'Q5_K_M': 1.00,
'Q4_K_M': 1.15,
'Q3_K': 1.25,
'Q2_K': 1.35
};
// Backend speed constants (K)
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
};
// Category target speeds (tokens/sec)
this.targetSpeeds = {
'general': 40,
'coding': 40,
'reasoning': 25,
'summarization': 60,
'reading': 60,
'multimodal': 40,
'embeddings': 200
};
// Category target contexts
this.targetContexts = {
'general': 4096,
'coding': 8192,
'reasoning': 8192,
'summarization': 8192,
'reading': 8192,
'multimodal': 4096,
'embeddings': 512
};
// Category scoring weights [Q, S, F, C] from centralized config
this.categoryWeights = DETERMINISTIC_WEIGHTS;
// User optimization profile overrides [Q, S, F, C]
this.optimizationProfiles = {
balanced: null,
speed: [0.25, 0.55, 0.15, 0.05],
quality: [0.65, 0.10, 0.15, 0.10],
context: [0.30, 0.10, 0.20, 0.40],
coding: [0.55, 0.25, 0.10, 0.10]
};
this.freshnessThresholds = {
staleDays: 365,
veryStaleDays: 730,
indexCadenceDays: 14
};
this.modelIndexStatus = {
source: 'unknown',
ageDays: null,
isStale: false,
cachedAt: null
};
}
// ============================================================================
// PHASE 0: DATA SOURCES
// ============================================================================
/**
* Hardware Profiler - Detect CPU, GPU, RAM, and acceleration support
*/
async getHardware() {
const hardware = {
cpu: await this.getCPUInfo(),
gpu: await this.getGPUInfo(),
memory: await this.getMemoryInfo(),
os: await this.getOSInfo(),
acceleration: await this.getAccelerationSupport()
};
// Calculate usable memory: min(0.8 * total_ram, total_ram - 2GB)
hardware.usableMemGB = Math.min(
0.8 * hardware.memory.totalGB,
hardware.memory.totalGB - 2
);
return hardware;
}
/**
* Normalize hardware shape coming from different detectors/callers.
* Ensures deterministic selector always has:
* - memory.totalGB
* - gpu.vramGB
* - acceleration.supports_*
*/
normalizeHardwareProfile(input = {}, options = {}) {
const hasExplicitCpuOnly = Object.prototype.hasOwnProperty.call(options, 'cpuOnly');
const hasInputCpuOnly = Object.prototype.hasOwnProperty.call(input, 'cpuOnly');
const effectiveCpuOnly = hasExplicitCpuOnly
? resolveCpuOnlyMode(options.cpuOnly)
: (hasInputCpuOnly ? resolveCpuOnlyMode(input.cpuOnly) : this.cpuOnly);
const shouldExposeCpuOnlyMode = hasExplicitCpuOnly || hasInputCpuOnly ||
this.hasExplicitCpuOnly || effectiveCpuOnly;
input = applyCpuOnlyOverride(input, {
cpuOnly: effectiveCpuOnly,
source: 'deterministic-selector'
});
const toNumber = (value) => {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
return Number(value);
}
return null;
};
const cpu = input.cpu || {};
const gpu = input.gpu || {};
const memory = input.memory || {};
const acceleration = input.acceleration || {};
const gpuEntries = Array.isArray(gpu.all) ? gpu.all : [];
const totalMemGB =
toNumber(memory.totalGB) ??
toNumber(memory.total) ??
toNumber(input.total_ram_gb) ??
toNumber(input.memoryGB) ??
8;
const modelHints = `${gpu.model || ''} ${gpu.vendor || ''} ${gpu.type || ''}`.toLowerCase();
const inferredUnified =
Boolean(gpu.unified) ||
/apple|m1|m2|m3|m4|unified/.test(modelHints);
const utilizationFactor = inferredUnified ? 0.85 : 0.8;
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 =
toNumber(gpu.gpuCount) ??
toNumber(gpu.count) ??
(gpuEntries.length > 0 ? gpuEntries.length : null) ??
toNumber(input.gpuCount) ??
1;
const vramPerGPU =
toNumber(gpu.vramPerGPU) ??
toNumber(input.vramPerGPU) ??
null;
const summedEntryVRAM = gpuEntries.reduce((sum, entry) => {
return sum + (
toNumber(entry?.vramGB) ??
toNumber(entry?.vram) ??
toNumber(entry?.totalVRAM) ??
0
);
}, 0);
const explicitTotalVRAM =
toNumber(gpu.totalVRAM) ??
toNumber(input.totalVRAM) ??
toNumber(input.gpuTotalVRAM) ??
(summedEntryVRAM > 0 ? summedEntryVRAM : null);
const directVRAM =
toNumber(gpu.vramGB) ??
toNumber(gpu.vram) ??
null;
let vramGB =
explicitTotalVRAM ??
directVRAM ??
0;
// Multi-GPU: only scale up when memory is known to be PER-GPU (vramPerGPU).
// A bare `vram`/`vramGB` is treated as the box total and never multiplied,
// so we don't double an already-total figure and falsely "fit" a model
// (e.g. a 2x24=48GB box must stay 48GB, not become 96GB).
if (!explicitTotalVRAM && gpuCount > 1 && vramPerGPU) {
vramGB = vramPerGPU * gpuCount;
}
let gpuType = gpu.type;
if (!gpuType) {
if (inferredUnified) gpuType = 'apple_silicon';
else if (/nvidia|rtx|gtx|tesla|quadro/.test(modelHints)) gpuType = 'nvidia';
else if (/amd|radeon|rx |instinct/.test(modelHints)) gpuType = 'amd';
else gpuType = 'cpu_only';
}
const normalizedAcceleration = {
...acceleration,
supports_metal:
typeof acceleration.supports_metal === 'boolean'
? acceleration.supports_metal
: gpuType === 'apple_silicon',
supports_cuda:
typeof acceleration.supports_cuda === 'boolean'
? acceleration.supports_cuda
: gpuType === 'nvidia',
supports_rocm:
typeof acceleration.supports_rocm === 'boolean'
? acceleration.supports_rocm
: gpuType === 'amd'
};
return {
...input,
...(shouldExposeCpuOnlyMode ? { cpuOnly: effectiveCpuOnly } : {}),
cpu: {
...cpu,
architecture: cpu.architecture || cpu.arch || process.arch || 'x86_64',
cores: toNumber(cpu.cores) ?? toNumber(cpu.physicalCores) ?? 4
},
gpu: {
...gpu,
type: gpuType,
vramGB,
vramPerGPU: vramPerGPU ?? (gpuCount > 0 ? (vramGB > 0 ? vramGB / gpuCount : 0) : 0),
gpuCount,
isMultiGPU: Boolean(gpu.isMultiGPU || gpuCount > 1),
unified: inferredUnified
},
memory: {
...memory,
totalGB: totalMemGB
},
acceleration: normalizedAcceleration,
usableMemGB
};
}
normalizeOptimizationObjective(objective) {
if (!objective) return 'balanced';
const normalized = String(objective).toLowerCase().trim();
if (['balanced', 'default', 'auto'].includes(normalized)) return 'balanced';
if (['speed', 'fast', 'latency', 'throughput'].includes(normalized)) return 'speed';
if (['quality', 'accurate', 'accuracy'].includes(normalized)) return 'quality';
if (['context', 'long-context', 'long_context', 'memory'].includes(normalized)) return 'context';
if (['coding', 'code', 'developer'].includes(normalized)) return 'coding';
return 'balanced';
}
getScoringWeights(category, optimizeFor = 'balanced') {
const base = this.categoryWeights[category] || this.categoryWeights.general;
const objective = this.normalizeOptimizationObjective(optimizeFor);
const objectiveWeights = this.optimizationProfiles[objective];
if (!objectiveWeights) {
return base;
}
// Blend category semantics with requested profile, but keep explicit
// user intent dominant (especially for quality/context priorities).
const objectivePriorities = {
speed: 0.8,
quality: 0.95,
context: 0.85,
coding: 0.8
};
const objectivePriority = objectivePriorities[objective] || 0.75;
const categoryPriority = 1 - objectivePriority;
return base.map((weight, idx) => {
const blended = (weight * categoryPriority) + (objectiveWeights[idx] * objectivePriority);
return Math.round(blended * 1000) / 1000;
});
}
async getCPUInfo() {
const os = require('os');
return {
architecture: os.arch(),
cores: os.cpus().length,
threads: os.cpus().length, // Simplified
platform: os.platform()
};
}
async getGPUInfo() {
const cpu = await this.getCPUInfo();
// Simplified GPU detection
if (cpu.platform === 'darwin' && cpu.architecture === 'arm64') {
return {
type: 'apple_silicon',
vramGB: 0, // Unified memory
unified: true
};
}
// TODO: Add NVIDIA/AMD detection for other platforms
return {
type: 'cpu_only',
vramGB: 0,
unified: false
};
}
async getMemoryInfo() {
const os = require('os');
const totalBytes = os.totalmem();
return {
totalGB: Math.round((totalBytes / (1024**3)) * 10) / 10
};
}
async getOSInfo() {
const os = require('os');
return {
platform: os.platform(),
arch: os.arch(),
release: os.release()
};
}
async getAccelerationSupport() {
const cpu = await this.getCPUInfo();
const gpu = await this.getGPUInfo();
return {
supports_metal: gpu.type === 'apple_silicon',
supports_cuda: gpu.type === 'nvidia',
supports_rocm: gpu.type === 'amd'
};
}
/**
* Local Ollama Inventory - Get installed models from `ollama list`
*/
async getInstalledModels() {
try {
const models = await this.runOllamaCommand(['list']);
const parsed = [];
for (const line of models.split('\n').slice(1)) { // Skip header
if (!line.trim()) continue;
const parts = line.trim().split(/\s+/);
if (parts.length < 3) continue;
const modelName = parts[0];
const modelId = parts[1];
const size = parts.length >= 4 ? `${parts[2]} ${parts[3]}` : parts[2];
// Get detailed info for each model
try {
const details = await this.getModelDetails(modelName);
parsed.push({
...details,
installed: true,
installedSize: size,
source: 'ollama_local',
registry: details.registry || 'ollama.com',
version: details.version || modelName,
license: details.license || 'unknown',
digest: details.digest || 'unknown',
provenance: {
...(details.provenance || {}),
source: 'ollama_local',
registry: details.registry || 'ollama.com',
version: details.version || modelName,
license: details.license || 'unknown',
digest: details.digest || 'unknown'
}
});
} catch (error) {
console.warn(`Failed to get details for ${modelName}:`, error.message);
}
}
return parsed;
} catch (error) {
// Silently fail when Ollama is not available - this is expected
return [];
}
}
async getModelDetails(modelName) {
try {
const details = await this.runOllamaCommand(['show', modelName]);
// Parse model details from ollama show output
const meta = {
name: modelName,
family: this.extractFamily(modelName),
paramsB: this.extractParams(details),
ctxMax: this.extractContextLength(details),
quant: this.extractQuantization(details),
sizeGB: this.extractSizeGB(details),
modalities: this.extractModalities(details),
tags: this.extractTags(details),
model_identifier: modelName,
source: 'ollama_local',
registry: 'ollama.com',
version: modelName,
license: this.extractLicense(details),
digest: this.extractDigest(details)
};
meta.provenance = {
source: meta.source,
registry: meta.registry,
version: meta.version,
license: meta.license,
digest: meta.digest
};
return meta;
} catch (error) {
// If Ollama is not available or model details can't be fetched, return minimal info
return {
name: modelName,
family: 'unknown',
paramsB: 0,
ctxMax: 2048,
quant: 'unknown',
sizeGB: 0,
modalities: ['text'],
tags: [],
model_identifier: modelName,
source: 'ollama_local',
registry: 'ollama.com',
version: modelName,
license: 'unknown',
digest: 'unknown',
provenance: {
source: 'ollama_local',
registry: 'ollama.com',
version: modelName,
license: 'unknown',
digest: 'unknown'
},
error: error.message
};
}
}
/**
* Curated Catalog - Load known models from catalog.json
*/
async loadCatalog() {
try {
if (!fs.existsSync(this.catalogPath)) {
console.warn('Catalog not found, creating default...');
await this.createDefaultCatalog();
}
const catalogData = fs.readFileSync(this.catalogPath, 'utf8');
const catalog = JSON.parse(catalogData);
return catalog.models.map(model => ({
...model,
installed: false,
source: model.source || 'static_catalog',
registry: model.registry || 'ollama.com',
version: model.version || model.model_identifier || model.name || 'unknown',
license: model.license || 'unknown',
digest: model.digest || 'unknown',
provenance: {
source: model.source || 'static_catalog',
registry: model.registry || 'ollama.com',
version: model.version || model.model_identifier || model.name || 'unknown',
license: model.license || 'unknown',
digest: model.digest || 'unknown'
}
}));
} catch (error) {
console.warn('Failed to load catalog:', error.message);
return [];
}
}
async createDefaultCatalog() {
const defaultCatalog = {
version: "1.0",
updated: new Date().toISOString(),
models: [
{
name: "qwen2.5-coder:0.5b",
family: "qwen2.5",
paramsB: 0.5,
ctxMax: 32768,
quant: "Q4_K_M",
sizeGB: 0.4,
modalities: ["text"],
tags: ["coder", "instruct"],
model_identifier: "qwen2.5-coder:0.5b"
},
{
name: "qwen2.5-coder:1.5b",
family: "qwen2.5",
paramsB: 1.5,
ctxMax: 32768,
quant: "Q4_K_M",
sizeGB: 1.1,
modalities: ["text"],
tags: ["coder", "instruct"],
model_identifier: "qwen2.5-coder:1.5b"
},
{
name: "qwen2.5-coder:7b",
family: "qwen2.5",
paramsB: 7,
ctxMax: 32768,
quant: "Q4_K_M",
sizeGB: 4.4,
modalities: ["text"],
tags: ["coder", "instruct"],
model_identifier: "qwen2.5-coder:7b"
},
{
name: "llama3.2:3b",
family: "llama3.2",
paramsB: 3,
ctxMax: 131072,
quant: "Q4_K_M",
sizeGB: 2.0,
modalities: ["text"],
tags: ["instruct", "chat"],
model_identifier: "llama3.2:3b"
},
{
name: "llava:7b",
family: "llava",
paramsB: 7,
ctxMax: 4096,
quant: "Q4_K_M",
sizeGB: 4.7,
modalities: ["text", "vision"],
tags: ["multimodal", "vision"],
model_identifier: "llava:7b"
}
]
};
// Ensure directory exists
const dir = path.dirname(this.catalogPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.catalogPath, JSON.stringify(defaultCatalog, null, 2));
}
/**
* Full model pool loader:
* 1) Prefer complete Ollama scraped cache (all families/sizes)
* 2) Fallback to static curated catalog
*/
async loadModelPool() {
const cacheModels = await this.loadOllamaCacheModels();
if (cacheModels.length > 0) {
return cacheModels;
}
return this.loadCatalog();
}
async loadOllamaCacheModels() {
for (const cachePath of this.ollamaCachePaths) {
try {
if (!fs.existsSync(cachePath)) continue;
const raw = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
const sourceModels = Array.isArray(raw) ? raw : (raw.models || []);
const indexMeta = this.extractModelIndexMetadata(raw, cachePath);
const normalized = this.normalizeExternalModels(sourceModels, { indexMeta });
if (normalized.length > 0) return normalized;
} catch (error) {
// Ignore broken cache files and keep trying fallbacks
}
}
return [];
}
extractModelIndexMetadata(raw, sourcePath = '') {
const cachedAtRaw = raw?.cached_at || raw?.generated_at || raw?.last_updated || null;
const cachedAt = this.parseDateSafe(cachedAtRaw);
const ageDays = cachedAt
? Math.max(0, (Date.now() - cachedAt.getTime()) / (1000 * 60 * 60 * 24))
: null;
const isStale = Number.isFinite(ageDays) && ageDays > this.freshnessThresholds.indexCadenceDays;
const status = {
source: sourcePath || 'cache',
ageDays: Number.isFinite(ageDays) ? Math.round(ageDays * 10) / 10 : null,
isStale: Boolean(isStale),
cachedAt: cachedAt ? cachedAt.toISOString() : null
};
this.modelIndexStatus = status;
return status;
}
normalizeExternalModels(models = [], context = {}) {
const normalized = [];
const indexMeta = context.indexMeta || this.modelIndexStatus || {};
for (const model of models) {
if (!model || typeof model !== 'object') continue;
const alreadyNormalized =
typeof model.paramsB === 'number' &&
typeof model.ctxMax === 'number' &&
model.model_identifier;
const freshness = this.computeFreshnessMetadata(model, indexMeta);
const quantizations = this.extractAvailableQuantizations(model, model.variants || []);
if (alreadyNormalized) {
normalized.push({
...model,
tags: Array.isArray(model.tags) ? model.tags : [],
modalities: Array.isArray(model.modalities) ? model.modalities : ['text'],
installed: Boolean(model.installed),
availableQuantizations: model.availableQuantizations || quantizations,
sizeByQuant: model.sizeByQuant || {},
source: model.source || 'ollama_database',
registry: model.registry || 'ollama.com',
version: model.version || model.model_identifier,
license: model.license || 'unknown',
digest: model.digest || 'unknown',
...freshness,
provenance: model.provenance || {
source: model.source || 'ollama_database',
registry: model.registry || 'ollama.com',
version: model.version || model.model_identifier,
license: model.license || 'unknown',
digest: model.digest || 'unknown'
}
});
continue;
}
const converted = this.convertOllamaModelToDeterministicModels(model, { indexMeta });
normalized.push(...converted);
}
const deduped = new Map();
for (const model of normalized) {
const key = model.model_identifier || model.name;
if (!key || deduped.has(key)) continue;
deduped.set(key, model);
}
return [...deduped.values()];
}
convertOllamaModelToDeterministicModels(ollamaModel, context = {}) {
const baseIdentifier = ollamaModel.model_identifier || ollamaModel.model_name || 'unknown';
const fallbackTag = `${baseIdentifier}:latest`;
const variants = Array.isArray(ollamaModel.variants) && ollamaModel.variants.length > 0
? ollamaModel.variants
: [{ tag: ollamaModel.model_identifier || fallbackTag }];
const indexMeta = context.indexMeta || this.modelIndexStatus || {};
const freshness = this.computeFreshnessMetadata(ollamaModel, indexMeta);
const contextLength = this.parseContextLength(
ollamaModel.context_length ||
ollamaModel.contextLength ||
ollamaModel.ctxMax
);
const baseText = [
ollamaModel.model_identifier,
ollamaModel.model_name,
ollamaModel.description,
ollamaModel.detailed_description,
ollamaModel.primary_category,
...(Array.isArray(ollamaModel.use_cases) ? ollamaModel.use_cases : []),
...(Array.isArray(ollamaModel.categories) ? ollamaModel.categories : [])
].filter(Boolean).join(' ').toLowerCase();
const derivedTags = new Set();
if (baseText.includes('code') || baseText.includes('coder')) derivedTags.add('coder');
if (baseText.includes('instruct')) derivedTags.add('instruct');
if (baseText.includes('chat') || baseText.includes('assistant') || baseText.includes('conversation')) derivedTags.add('chat');
if (baseText.includes('embed')) derivedTags.add('embedding');
if (baseText.includes('vision') || baseText.includes('vl') || baseText.includes('multimodal') || baseText.includes('image')) derivedTags.add('vision');
if (baseText.includes('reason') || baseText.includes('math') || baseText.includes('logic')) derivedTags.add('reasoning');
if (baseText.includes('creative') || baseText.includes('story') || baseText.includes('roleplay')) derivedTags.add('creative');
if (ollamaModel.primary_category === 'coding') derivedTags.add('coder');
if (ollamaModel.primary_category === 'chat') derivedTags.add('chat');
if (ollamaModel.primary_category === 'embeddings') derivedTags.add('embedding');
if (ollamaModel.primary_category === 'multimodal') derivedTags.add('vision');
if (ollamaModel.primary_category === 'reasoning') derivedTags.add('reasoning');
if (ollamaModel.primary_category === 'creative') derivedTags.add('creative');
const hasConcreteVariants = variants.some((variant) => this.variantHasConcreteSizeOrParams(variant));
const selectableVariants = hasConcreteVariants
? variants.filter((variant) => this.variantHasConcreteSizeOrParams(variant))
: variants;
return selectableVariants
.map((variant) => {
const variantTag = variant.tag || fallbackTag;
const quant = this.resolveVariantQuantization(variant, variantTag);
const paramsB = this.resolveVariantParamsB(ollamaModel, variant, quant);
const moeMetadata = this.extractMoEMetadata(ollamaModel, variant, paramsB, baseText);
const variantSizeGB = this.extractVariantSizeGB(variant, paramsB);
const modalities = this.inferModalities(ollamaModel, variantTag);
const modelTags = this.inferTagsForVariant(derivedTags, variant, variantTag);
const 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';
const version = ollamaModel.version || variantTag;
const license = ollamaModel.license || 'unknown';
const digest = ollamaModel.digest || 'unknown';
const normalizedExpertCount = Number.isFinite(moeMetadata.expertCount) && moeMetadata.expertCount > 0
? Math.round(moeMetadata.expertCount)
: null;
const normalizedExpertsActive = Number.isFinite(moeMetadata.expertsActivePerToken) && moeMetadata.expertsActivePerToken > 0
? moeMetadata.expertsActivePerToken
: null;
const normalizedTotalParamsB = Number.isFinite(moeMetadata.totalParamsB) && moeMetadata.totalParamsB > 0
? moeMetadata.totalParamsB
: null;
const normalizedActiveParamsB = Number.isFinite(moeMetadata.activeParamsB) && moeMetadata.activeParamsB > 0
? moeMetadata.activeParamsB
: null;
return {
name: variantTag,
model_name: ollamaModel.model_name || baseIdentifier,
family: this.extractFamily(baseIdentifier),
paramsB,
isMoE: Boolean(moeMetadata.isMoE),
is_moe: Boolean(moeMetadata.isMoE),
totalParamsB: normalizedTotalParamsB,
activeParamsB: normalizedActiveParamsB,
expertCount: normalizedExpertCount,
expertsActivePerToken: normalizedExpertsActive,
total_params_b: normalizedTotalParamsB,
active_params_b: normalizedActiveParamsB,
expert_count: normalizedExpertCount,
experts_active_per_token: normalizedExpertsActive,
ctxMax: contextLength,
quant,
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 || '',
model_identifier: variantTag,
last_updated: ollamaModel.last_updated || ollamaModel.lastUpdated || '',
updated_at: ollamaModel.updated_at || ollamaModel.updatedAt || '',
installed: false,
pulls: ollamaModel.actual_pulls || ollamaModel.pulls || 0,
availableQuantizations,
sizeByQuant,
...freshness,
source,
registry,
version,
license,
digest,
provenance: {
source,
registry,
version,
license,
digest
}
};
});
}
variantHasConcreteSizeOrParams(variant = {}) {
const params = this.extractParamsFromString(
variant.params_b,
variant.paramsB,
variant.parameter_size,
variant.size,
variant.tag,
variant.label,
variant.name
);
if (Number.isFinite(params) && params > 0) return true;
const artifactSize = Number(
variant.real_size_gb ??
variant.estimated_size_gb ??
variant.size_gb ??
NaN
);
return Number.isFinite(artifactSize) && artifactSize > 0;
}
parseBillionsValue(rawValue) {
return parseMoEBillionsValue(rawValue);
}
parsePositiveNumber(rawValue) {
return parseMoEPositiveNumber(rawValue);
}
extractMoEMetadata(model = {}, variant = {}, paramsB = null, baseText = '') {
return extractCanonicalMoEMetadata({
model,
variant,
paramsB,
baseText
});
}
parseDateSafe(value) {
if (!value || typeof value !== 'string') return null;
const normalized = value.trim();
const relativeMatch = normalized.match(/^(\d+)\s*(minutes?|hours?|days?|weeks?|months?|years?)\s+ago$/i);
if (relativeMatch) {
const amount = parseInt(relativeMatch[1], 10);
const unit = relativeMatch[2].toLowerCase();
const days =
unit.startsWith('minute') ? amount / (24 * 60) :
unit.startsWith('hour') ? amount / 24 :
unit.startsWith('day') ? amount :
unit.startsWith('week') ? amount * 7 :
unit.startsWith('month') ? amount * 30 :
unit.startsWith('year') ? amount * 365 :
null;
if (Number.isFinite(days)) {
return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
}
}
const parsed = new Date(normalized);
if (Number.isNaN(parsed.getTime())) return null;
return parsed;
}
extractAvailableQuantizations(model, variants = []) {
const quantSet = new Set();
const candidateStrings = [];
if (Array.isArray(model?.quantizations)) {
candidateStrings.push(...model.quantizations);
}
if (typeof model?.quantization === 'string') {
candidateStrings.push(model.quantization);
}
for (const variant of variants) {
if (variant?.quantization) candidateStrings.push(variant.quantization);
if (variant?.tag) candidateStrings.push(variant.tag);
}
for (const value of candidateStrings) {
const inferred = this.normalizeQuantization(
this.extractQuantizationFromTag(String(value)) || String(value)
);
if (inferred) quantSet.add(inferred);
}
if (quantSet.size === 0 && model?.quant) {
quantSet.add(this.normalizeQuantization(model.quant));
}
if (quantSet.size === 0) {
quantSet.add('Q4_K_M');
}
return [...quantSet].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;
});
}
computeFreshnessMetadata(model = {}, indexMeta = {}) {
const dateCandidates = [
model.last_updated,
model.lastUpdated,
model.updated_at,
model.updatedAt,
model.release_date,
model.released_at,
model.created_at
];
const updatedAt = dateCandidates
.map((value) => this.parseDateSafe(value))
.find(Boolean);
const ageDays = updatedAt
? Math.max(0, (Date.now() - updatedAt.getTime()) / (1000 * 60 * 60 * 24))
: null;
let freshnessScore = 55; // neutral fallback when timestamp is unknown
if (Number.isFinite(ageDays)) {
if (ageDays <= 30) freshnessScore = 100;
else if (ageDays <= 90) freshnessScore = 90;
else if (ageDays <= 180) freshnessScore = 75;
else if (ageDays <= 365) freshnessScore = 60;
else if (ageDays <= 540) freshnessScore = 40;
else if (ageDays <= 720) freshnessScore = 25;
else freshnessScore = 10;
}
const textBlob = [
model.model_identifier,
model.model_name,
model.name,
model.description,
model.detailed_description,
model.status,
...(Array.isArray(model.tags) ? model.tags : [])
]
.filter(Boolean)
.join(' ')
.toLowerCase();
const isDeprecatedByText =
/\bdeprecated\b|\bobsolete\b|\blegacy\b|\barchived\b|\breplaced by\b|\buse .+ instead\b/.test(textBlob);
const isDeprecated = Boolean(model.deprecated || model.is_deprecated || model.archived || isDeprecatedByText);
const isStale = Number.isFinite(ageDays) && ageDays > this.freshnessThresholds.staleDays;
const veryStale = Number.isFinite(ageDays) && ageDays > this.freshnessThresholds.veryStaleDays;
const indexStale = Boolean(indexMeta?.isStale);
if (isDeprecated) freshnessScore = Math.min(freshnessScore, 15);
if (veryStale) freshnessScore = Math.min(freshnessScore, 20);
if (indexStale && !updatedAt) freshnessScore = Math.max(0, freshnessScore - 10);
return {
lastUpdatedAt: updatedAt ? updatedAt.toISOString() : null,
modelAgeDays: Number.isFinite(ageDays) ? Math.round(ageDays * 10) / 10 : null,
freshnessScore,
isStale,
isDeprecated,
indexAgeDays: Number.isFinite(indexMeta?.ageDays) ? indexMeta.ageDays : null,
indexStale
};
}
parseContextLength(contextValue) {
if (typeof contextValue === 'number' && Number.isFinite(contextValue) && contextValue > 0) {
return Math.round(contextValue);
}
if (typeof contextValue === 'string') {
const match = contextValue.match(/(\d+\.?\d*)\s*([KkMm]?)/);
if (match) {
const value = parseFloat(match[1]);
const unit = (match[2] || '').toUpperCase();
if (unit === 'M') return Math.round(value * 1024 * 1024);
if (unit === 'K') return Math.round(value * 1024);
return Math.round(value);
}
}
return 4096;
}
extractParamsFromString(...values) {
const candidates = this.extractParameterCandidates(...values);
return candidates.length > 0 ? candidates[0] : null;
}
extractParameterCandidates(...values) {
const candidates = [];
const seen = new Set();
const pushCandidate = (value) => {
if (!Number.isFinite(value) || value <= 0) return;
const rounded = Math.round(value * 1000) / 1000;
const key = String(rounded);
if (seen.has(key)) return;
seen.add(key);
candidates.push(rounded);
};
const visit = (value) => {
if (typeof value === 'number') {
pushCandidate(value);
return;
}
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (value && typeof value === 'object') {
Object.values(value).forEach(visit);
return;
}
if (typeof value !== 'string') return;
const regex = /(\d+\.?\d*)\s*([BbMm])/g;
for (const match of value.matchAll(regex)) {
const suffix = value.slice(match.index + match[0].length, match.index + match[0].length + 2);
if (/^\s*b\b/i.test(suffix) || /^\s*[gk]b\b/i.test(suffix)) continue;
const amount = parseFloat(match[1]);
const unit = match[2].toUpperCase();
pushCandidate(unit === 'M' ? amount / 1000 : amount);
}
};
values.forEach(visit);
return candidates;
}
extractArtifactSizeGBFromValue(value) {
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
return value;
}
if (typeof value !== 'string') return null;
const match = value.match(/(\d+\.?\d*)\s*g(?:i)?b\b/i);
if (!match) return null;
return parseFloat(match[1]);
}
inferParamsFromArtifactSizeGB(sizeGB, quant = 'Q4_K_M') {
const normalizedQuant = this.normalizeQuantization(quant);
const bpp = precisionProfile(normalizedQuant).bytes || 2;
const inferred = sizeGB / bpp;
return Math.max(0.5, Math.round(inferred * 2) / 2);
}
isCloudVariantTag(tag = '') {
return /:cloud$/i.test(String(tag).trim());
}
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);
}
return this.normalizeQuantization(
variant.quantization ||
variant.quant ||
'UNKNOWN'
);
}
resolveVariantParamsB(ollamaModel = {}, variant = {}, quant = 'Q4_K_M') {
const explicitParams = this.extractParamsFromString(
variant.size,
variant.tag,
variant.label,
variant.name,
ollamaModel.model_identifier,
ollamaModel.model_name,
ollamaModel.parameter_size,
ollamaModel.parameter_count,
ollamaModel.parameters
);
if (Number.isFinite(explicitParams) && explicitParams > 0) {
return explicitParams;
}
// Use the variant's OWN artifact size to DISAMBIGUATE the model-level size
// list. A size-unknown variant (e.g. `:latest`) must not blindly inherit
// model_sizes[0]: for qwen3 (model_sizes ["30b","235b"]) that mislabeled a
// small qwen3:latest as 30B and poisoned the real qwen3:30b size map, making
// a 19GB model falsely "fit" a 16GB machine.
const artifactSizeGB = this.extractVariantSizeGB(variant, null);
const artifactParamsB =
(!this.isCloudVariantTag(variant.tag) && Number.isFinite(artifactSizeGB) && artifactSizeGB > 0)
? this.inferParamsFromArtifactSizeGB(artifactSizeGB, quant)
: null;
const metadataCandidates = this.extractParameterCandidates(
ollamaModel.model_sizes,
ollamaModel.parameters,
ollamaModel.parameter_size,
ollamaModel.parameter_count
);
if (metadataCandidates.length > 0) {
if (Number.isFinite(artifactParamsB) && artifactParamsB > 0) {
// Pick the listed size CLOSEST to what this variant's own artifact
// implies; if even the closest is far off, trust the artifact size.
let closest = metadataCandidates[0];
let bestDiff = Math.abs(closest - artifactParamsB);
for (const cand of metadataCandidates) {
const diff = Math.abs(cand - artifactParamsB);
if (diff < bestDiff) { bestDiff = diff; closest = cand; }
}
const tolerance = Math.max(2, closest * 0.5);
return bestDiff <= tolerance ? closest : artifactParamsB;
}
return metadataCandidates[0];
}
if (Number.isFinite(artifactParamsB) && artifactParamsB > 0) {
return artifactParamsB;
}
const modelArtifactSizeGB = this.extractArtifactSizeGBFromValue(ollamaModel.main_size);
if (Number.isFinite(modelArtifactSizeGB) && modelArtifactSizeGB > 0) {
return this.inferParamsFromArtifactSizeGB(modelArtifactSizeGB, quant);
}
return 7;
}
extractQuantizationFromTag(tag = '') {
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) {
return normalizePrecision(quant);
}
extractVariantSizeGB(variant, paramsB) {
const candidate = Number(variant.real_size_gb ?? variant.size_gb ?? NaN);
if (Number.isFinite(candidate) && candidate > 0) return candidate;
return null; // No observed artifact size; estimate from the actual precision later.
}
inferModalities(model, variantTag = '') {
const inputTypes = Array.isArray(model.input_types) ? model.input_types.map((x) => String(x).toLowerCase()) : [];
const primaryCategory = String(model.primary_category || '').toLowerCase();
const categories = Array.isArray(model.categories) ? model.categories.map((x) => String(x).toLowerCase()) : [];
const useCases = Array.isArray(model.use_cases) ? model.use_cases.map((x) => String(x).toLowerCase()) : [];
const text = [
model.model_identifier,
model.model_name,
model.description,
model.detailed_description,
variantTag
].filter(Boolean).join(' ').toLowerCase();
const hasVisionInputFlag = inputTypes.includes('image') || inputTypes.includes('vision');
const hasVisionMetadataHint =
primaryCategory === 'multimodal' ||
categories.some((cat) => cat.includes('multimodal') || cat.includes('vision')) ||
useCases.some((useCase) => useCase.includes('multimodal') || useCase.includes('vision'));
const hasVisionTextHint =
/(?:\bmultimodal\b|\bvision\b|\bllava\b|\bbakllava\b|\bmoondream\b|\bpixtral\b|\bidefics\b|\bpaligemma\b|\bminicpm-?v\b|\bqwen[\w.-]*vl\b|\bllama3\.2[-_ ]?vision\b|\bdeepseek-ocr\b)/.test(text);
const hasVisionContextHint =
/\b(image[- ]?(understanding|caption|analysis)|vision[- ]?language|vlm)\b/.test(text);
// Some upstream scrapers may over-report `image` support by scanning generic page text.
// Trust image input flags only when accompanied by multimodal metadata or explicit vision naming.
const hasVision = hasVisionTextHint || hasVisionMetadataHint || (hasVisionInputFlag && hasVisionContextHint);
return hasVision ? ['text', 'vision'] : ['text'];
}
inferTagsForVariant(baseTags, variant, variantTag = '') {
const tags = new Set(baseTags);
if (Array.isArray(variant.categories)) {
for (const cat of variant.categories) {
const c = String(cat).toLowerCase();
if (c.includes('code')) tags.add('coder');
if (c.includes('chat')) tags.add('chat');
if (c.includes('embed')) tags.add('embedding');
if (c.includes('vision') || c.includes('multimodal')) tags.add('vision');
if (c.includes('reason')) tags.add('reasoning');
if (c.includes('creative')) tags.add('creative');
}
}
const lowerTag = String(variantTag).toLowerCase();
if (lowerTag.includes('code') || lowerTag.includes('coder')) tags.add('coder');
if (lowerTag.includes('instruct')) tags.add('instruct');
if (lowerTag.includes('chat')) tags.add('chat');
if (lowerTag.includes('embed')) tags.add('embedding');
if (lowerTag.includes('vision') || lowerTag.includes('vl')) tags.add('vision');
if (lowerTag.includes('reason') || lowerTag.includes('math')) tags.add('reasoning');
return [...tags];
}
// ============================================================================
// HELPER METHODS FOR PARSING OLLAMA OUTPUT
// ============================================================================
extractFamily(modelName) {
const name = modelName.toLowerCase();
if (name.includes('qwen2.5')) return 'qwen2.5';
if (name.includes('qwen3')) return 'qwen3';
if (name.includes('qwen')) return 'qwen2.5';
if (name.includes('deepseek-r1')) return 'deepseek-r1';
if (name.includes('deepseek-coder')) return 'deepseek-coder';
if (name.includes('deepseek')) return 'deepseek';
if (name.includes('llama3.2') || name.includes('llama3.3')) return 'llama3.2';
if (name.includes('llama3.1')) return 'llama3.1';
if (name.includes('llama')) return 'llama';
if (name.includes('mistral')) return 'mistral';
if (name.includes('gemma3')) return 'gemma3';
if (name.includes('gemma')) return 'gemma2';
if (name.includes('phi')) return 'phi-3';
if (name.includes('llava')) return 'llava';
if (name.includes('granite')) return 'granite';
if (name.includes('solar')) return 'solar';
if (name.includes('starcoder')) return 'starcoder';
if (name.includes('minicpm')) return 'minicpm';
if (name.includes('yi-coder')) return 'yi-coder';
if (name.includes('yi')) return 'yi';
return 'unknown';
}
extractParams(details) {
// Look for parameter info in ollama show output
const match = details.match(/parameters\s+(\d+\.?\d*)\s*[BM]/i);
if (match) {
const num = parseFloat(match[1]);
return match[0].toUpperCase().includes('B') ? num : num / 1000;
}
return null;
}
extractContextLength(details) {
const match = details.match(/context[ _]length\s+(\d+)/i);
return match ? parseInt(match[1]) : null;
}
extractQuantization(details) {
const match = details.match(/quantization\s+(\S+)/i);
return normalizePrecision(match?.[1]);
}
extractSizeGB(details) {
const match = details.match(/size\s+(\d+\.?\d*)\s*(GB|MB)/i);
return match ? parseFloat(match[1]) / (match[2].toUpperCase() === 'MB' ? 1024 : 1) : null;
}
extractModalities(details) {
const modalities = ['text'];
if (details.toLowerCase().includes('vision') || details.toLowerCase().includes('image')) {
modalities.push('vision');
}
return modalities;
}
extractTags(details) {
const tags = [];
const lowerDetails = details.toLowerCase();
if (lowerDetails.includes('instruct')) tags.push('instruct');
if (lowerDetails.includes('chat')) tags.push('chat');
if (lowerDetails.includes('code')) tags.push('coder');
if (lowerDetails.includes('vision')) tags.push('vision');
// Only mark as embedding if it's explicitly an embedding model
if (lowerDetails.includes('embed-text') ||
lowerDetails.includes('nomic-embed') ||
lowerDetails.includes('bge-') ||
lowerDetails.includes('all-minilm')) tags.push('embedding');
return tags;
}
extractLicense(details) {
const match = details.match(/license\s+([^\n\r]+)/i);
return match ? match[1].trim().toLowerCase() : 'unknown';
}
extractDigest(details) {
const match = details.match(/digest\s+([a-f0-9:]+)/i);
return match ? match[1].trim().toLowerCase() : 'unknown';
}
async runOllamaCommand(args) {
return new Promise((resolve, reject) => {
try {
const proc = spawn('ollama', args, { stdio: 'pipe' });
let output = '';
let error = '';
proc.stdout.on('data', (data) => output += data);
proc.stderr.on('data', (data) => error += data);
proc.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
reject(new Error(`Ollama command failed: ${error}`));
}
});
proc.on('error', (err) => {
// Handle ENOENT and other spawn errors gracefully
if (err.code === 'ENOENT') {
reject(new Error('Ollama not found. Please install Ollama from https://ollama.ai'));
} else {
reject(new Error(`Ollama spawn error: ${err.message}`));
}
});
} catch (spawnError) {
// Handle synchronous spawn errors
reject(new Error(`Failed to start Ollama: ${spawnError.message}`));
}
});
}
// ============================================================================
// PHASE 1: ESTIMATION FILTER
// ============================================================================
/**
* Main model selection function
*/
async selectModels(category = 'general', options = {}) {
const {
targetCtx = this.targetContexts[category] || this.targetContexts.general,
topN = 5,
enableProbe = false,
silent = false,
optimizeFor = 'balanced',
runtime = 'ollama',
hardware: providedHardware = null,
installedModels = null,
modelPool = null,
includeUncensored = false
} = options;
const normalizedRuntime = normalizeMoERuntime(runtime);
const optimizationObjective = this.normalizeOptimizationObjective(
options.optimize || options.objective || optimizeFor
);
if (!silent) {
console.log(`🔍 Selecting models for category: ${category}`);
if (optimizationObjective !== 'balanced') {
console.log(`⚙️ Optimization profile: ${optimizationObjective}`);
}
}
// Phase 0: Gather data
const rawHardware = providedHardware || await this.getHardware();
const hardware = this.normalizeHardwareProfile(rawHardware, {
...(Object.prototype.hasOwnProperty.call(options, 'cpuOnly')
? { cpuOnly: options.cpuOnly }
: {})
});
this.activeCpuOnly = Boolean(hardware.cpuOnly);
if (typeof this.ollamaClient.setCpuOnly === 'function') {
this.ollamaClient.setCpuOnly(this.activeCpuOnly);
}
const installed = Array.isArray(installedModels) ? installedModels : await this.getInstalledModels();
const externalPool = Array.isArray(modelPool) && modelPool.length > 0
? (modelPool.some(model => typeof model?.paramsB === 'number' && model?.model_identifier)
? modelPool
: this.normalizeExternalModels(modelPool))
: await this.loadModelPool();
if (!silent) {
const memoryGB = hardware?.memory?.totalGB ?? hardware?.memory?.total ?? 0;
console.log(`Found ${installed.length} installed, ${externalPool.length} available models`);
console.log(`Hardware: ${hardware.cpu.cores} cores, ${memoryGB}GB RAM, ${hardware.gpu.type}`);
}
// Combine and dedupe models (prefer installed versions)
const pool = this.combineModels(installed, externalPool);
const filtered = this.filterByCategory(pool, category, {
includeUncensored,
referenceModels: externalPool
});
if (!silent) {
console.log(`Evaluating ${filtered.length} models for ${category} category`);
}
// Phase 1: Estimation filter
const candidates = [];
const budget = memoryBudgetGB(hardware);
for (const model of filtered) {
const result = this.evaluateModel(
model,
hardware,
category,
targetCtx,
budget,
optimizationObjective,
normalizedRuntime,
{ contextPolicy: Number(options.targetCtx) > 0 ? 'required' : 'preferred' }
);
if (result) {
candidates.push(result);
}
}
// Sort by score
candidates.sort((a, b) => b.score - a.score);
let topCandidates = candidates.slice(0, topN);
topCandidates = this.ensureFeasibleMidTierCoverage(
topCandidates,
candidates,
category,
hardware,
optimizationObjective
);
if (!silent) {
console.log(`✨ Selected ${topCandidates.length} top candidates`);
}
// Phase 2: Quick probe (optional)
if (enableProbe && topCandidates.length > 0) {
if (!silent) {
console.log(`🔬 Running quick probes...`);
}
await this.runQuickProbes(topCandidates, hardware, category);
// Re-sort after probing
topCandidates.sort((a, b) => b.score - a.score);
}
return {
category,
optimizeFor: optimizationObjective,
runtime: normalizedRuntime,
hardware,
candidates: topCandidates,
total_evaluated: filtered.length,
timestamp: new Date().toISOString()
};
}
combineModels(installed, catalog) {
const combined = [...installed];
const installedNames = new Set(installed.map(m => m.model_identifier));
// Add catalog models that aren't installed
for (const model of catalog) {
if (!installedNames.has(model.model_identifier)) {
combined.push(model);
}
}
return combined;
}
isUncensoredModel(model = {}) {
return isUncensoredModel(model);
}
filterByCategory(models, category, options = {}) {
const includeUncensored = options.includeUncensored === true;
const eligibleModels = filterModelsBySafety(models, {
includeUncensored,
referenceModels: options.referenceModels
});
return eligibleModels.filter(model => {
if (this.isCloudVariantTag(model.model_identifier || model.name)) {
return false;
}
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();
const paramsB = Number(model.paramsB) || 0;
switch (category) {
case 'coding':
return tags.some(tag => ['coder', 'code', 'instruct'].includes(tag)) ||
name.includes('code');
case 'multimodal':
return modalities.includes('vision') ||
tags.includes('vision');
case 'embeddings':
return tags.includes('embedding') ||
tags.includes('embeddings') ||
name.includes('embed') ||
name.includes('bge-') ||
name.includes('nomic-embed') ||
name.includes('all-minilm') ||
model.specialization === 'embeddings';
case 'reasoning':
return tags.includes('instruct') ||
paramsB >= 7; // Prefer larger models for reasoning
default: // general, reading, summarization
return true; // Most models can handle these
}
});
}
evaluateModel(model, hardware, category, targetCtx, budget, optimizeFor = 'balanced', runtime = 'ollama', options = {}) {
if (!(this.parseBillionsValue(model.totalParamsB || model.paramsB) > 0)) return null;
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, effectiveContext);
if (!bestQuant) return null;
// 2. Calculate required memory
const memoryEstimate = this.estimateMemoryBreakdown(model, bestQuant.quant, effectiveContext);
const requiredGB = memoryEstimate.requiredGB;
if (requiredGB > budget) return null;
// 3. Calculate component scores
const Q = this.calculateQualityPrior(model, bestQuant.quant, category);
const speedEstimate = this.estimateSpeedProfile(hardware, model, bestQuant.quant, category, runtime);
const S = speedEstimate.score;
const F = this.calculateFitScore(requiredGB, budget);
const C = this.calculateContextScore(model, targetCtx);
const capacityAdjustment = this.calculateHighCapacitySizeAdjustment(
hardware,
model,
budget,
category,
optimizeFor
);
// 4. Calculate final weighted score
const weights = this.getScoringWeights(category, optimizeFor);
const weightedScore = Q * weights[0] + S * weights[1] + F * weights[2] + C * weights[3];
const score = Math.max(
0,
Math.min(100, Math.round((weightedScore + capacityAdjustment.score) * 10) / 10)
);
// 5. Build rationale
const rationale = this.buildRationale(
hardware,
model,
bestQuant.quant,
requiredGB,
budget,
category,
Q,
S,
memoryEstimate,
speedEstimate,
capacityAdjustment
);
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,
score,
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,
memorySource: memoryEstimate.memorySource,
assumptionSource: memoryEstimate.parameterProfile.assumptionSource,
isMoE: memoryEstimate.parameterProfile.isMoE,
effectiveParamsB: Math.round(memoryEstimate.parameterProfile.effectiveParamsB * 1000) / 1000
},
speed: {
backend: speedEstimate.backend,
basis: speedEstimate.basis,
targetTPS: speedEstimate.targetTPS,
estimatedTPS: speedEstimate.estimatedTPS,
runtime: speedEstimate.runtime,
moe: speedEstimate.moe
},
components: { Q, S, F, C, H: capacityAdjustment.score },
optimizeFor
};
}
getQuantizationCandidates(model) {
// 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 !== 1 && (model?.model_identifier || !available.length)) {
return Number(model?.sizeGB) > 0 ? ['UNKNOWN'] : [];
}
return [...new Set(available.map(normalizePrecision))]
.sort((a, b) => (precisionProfile(b).bytes || 0) - (precisionProfile(a).bytes || 0));
}
selectBestQuantization(model, budget, targetCtx) {
for (const quant of this.getQuantizationCandidates(model)) {
const requiredGB = this.estimateRequiredGB(model, quant, targetCtx);
if (requiredGB <= budget) return { quant, sizeGB: requiredGB };
}
return null;
}
resolveMemoryParameterProfile(model = {}) {
return resolveMoEParameterProfile(model);
}
estimateMemoryBreakdown(model, quant, ctx) {
const normalizedQuant = this.normalizeQuantization(quant);
const bpp = precisionProfile(normalizedQuant).bytes;
const sizeByQuant = model?.sizeByQuant && typeof model.sizeByQuant === 'object' ? model.sizeByQuant : {};
const observedFromSizeMap = Number(sizeByQuant[normalizedQuant]);
const directVariantMatch =
this.normalizeQuantization(model?.quant || '') === normalizedQuant
? Number(model?.sizeGB ?? model?.size)
: NaN;
const observedWeightGB = Number.isFinite(observedFromSizeMap) && observedFromSizeMap > 0
? observedFromSizeMap
: (Number.isFinite(directVariantMatch) && directVariantMatch > 0 ? directVariantMatch : null);
const parameterProfile = this.resolveMemoryParameterProfile(model);
// Weight memory must account for ALL resident parameters. For MoE under
// Ollama / Metal / vLLM every expert is resident, so size the weights by
// the TOTAL parameter count (not the active count). Active params drive
// speed and KV-cache only. Sizing weights by active params used to make a
// 236B MoE look like ~14GB and falsely "fit" small hardware.
const weightParamsB =
parameterProfile.isMoE && Number.isFinite(parameterProfile.totalParamsB) && parameterProfile.totalParamsB > 0
? parameterProfile.totalParamsB
: parameterProfile.effectiveParamsB;
const modeledWeightGB = bpp == null ? Infinity : weightParamsB * bpp;
// Full-precision resident weights cannot shrink to a compressed download
// or an old Q4 size estimate. Quantized artifacts retain their observed size.
const fullPrecision = ['FP16', 'BF16', 'FP32'].includes(normalizedQuant);
const useObservedArtifactSize = Number.isFinite(observedWeightGB) && observedWeightGB > 0 &&
(!fullPrecision || observedWeightGB >= modeledWeightGB);
const modelMemGB = useObservedArtifactSize ? observedWeightGB : modeledWeightGB;
const effectiveCtx = Number.isFinite(Number(ctx)) && Number(ctx) > 0 ? Number(ctx) : 4096;
// KV cache: ~2 * numLayers * hiddenDim * 2bytes * ctx / 1e9
// Simplified: ~0.000008 GB per billion params per context token
const kvCacheGB = 0.000008 * parameterProfile.effectiveParamsB * effectiveCtx;
// Runtime overhead (Metal/CUDA context, buffers)
const runtimeOverhead = useObservedArtifactSize ? 0.35 : 0.5;
const usedMoeTotal = parameterProfile.isMoE && weightParamsB === parameterProfile.totalParamsB;
const memorySource = useObservedArtifactSize
? 'observed_artifact_size'
: (usedMoeTotal ? 'moe_total_params' : 'estimated_from_params');
return {
parameterProfile,
memorySource,
modelMemGB,
kvCacheGB,
runtimeOverheadGB: runtimeOverhead,
requiredGB: modelMemGB + kvCacheGB + runtimeOverhead
};
}
estimateRequiredGB(model, quant, ctx) {
return this.estimateMemoryBreakdown(model, quant, ctx).requiredGB;
}
calculateQualityPrior(model, quant, category) {
// Measured benchmark scores win over the size heuristic whenever we
// actually have them. `qualityEvals` is injected by the caller; when
// it is absent or the model was never benchmarked we fall back to the
// estimate below and mark it as such on the result, so nothing ever
// presents a guess as a measurement.
const measured = this.lookupMeasuredQuality(model, category);
if (measured) {
let Qm = measured.score;
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));
}
model.qualitySource = { kind: 'estimated', basis: 'parameter count' };
// Base quality by parameter count
let Q = this.getBaseQuality(model.paramsB);
// Family bump
const familyBump = this.familyBumps[model.family] || 0;
Q += familyBump;
// Quantization penalty
const quantPenalty = this.quantPenalties[quant] ?? precisionProfile(quant).penalty;
Q += quantPenalty;
// Freshness/deprecation adjustment
const freshnessAdjustment = this.calculateFreshnessAdjustment(model);
Q += freshnessAdjustment;
const pulls = Number(model.pulls || model.actual_pulls || 0);
if (pulls >= 100000000) Q += 4;
else if (pulls >= 20000000) Q += 3;
else if (pulls >= 5000000) Q += 2;
else if (pulls >= 1000000) Q += 1;
// Task alignment bump
const taskBump = this.getTaskAlignmentBump(model, category);
Q += taskBump;
// Reasoning bonus for larger models
if (category === 'reasoning' && model.paramsB >= 13) {
Q += 5;
}
// Coding penalty for non-instruct models
if (category === 'coding' && !model.tags.some(tag => ['coder', 'instruct'].includes(tag))) {
Q -= 15;
}
return Math.max(0, Math.min(100, Q));
}
/**
* Real benchmark score for this model at this size, on the 0-100 axis the
* selector uses, or null when nothing was measured.
*
* Only metrics that actually measure the category are consulted: HumanEval
* 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
// first use and degrades to the estimate if that is not possible.
if (this.qualityEvals === undefined) {
this.qualityEvals = null;
let connection;
try {
const { DatabaseSync } = require('node:sqlite');
const { QualityEvals } = require('../data/quality-evals');
const dbPath = require('path').join(
require('os').homedir(), '.llm-checker', 'models.db'
);
if (require('fs').existsSync(dbPath)) {
connection = new DatabaseSync(dbPath, { readOnly: true });
const quality = new QualityEvals(connection, { readOnly: true });
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 */ }
}
if (!this.qualityEvals) return null;
// Preference order per category: the most discriminating benchmark
// first. LiveBench closes the gap for reasoning, creative and chat —
// the three categories that previously had no task signal at all and
// therefore ranked purely by model size.
const BENCH_FOR_CATEGORY = {
coding: [
'bcb_hard_instruct', 'bcb_instruct', 'livebench_coding',
'livebench_agentic_coding', 'humaneval_plus', 'mbpp_plus', 'bcb_complete',
],
reasoning: ['livebench_reasoning', 'hf_bbh', 'hf_gpqa', 'livebench_mathematics', 'hf_math_lvl5', 'hf_musr'],
creative: ['livebench_language'],
talking: ['lmarena_chat', 'hf_ifeval', 'livebench_if'],
general: ['lmarena_general', 'hf_mmlu_pro', 'livebench_data_analysis'],
multimodal: ['mmmu_val'],
};
const wanted = BENCH_FOR_CATEGORY[category];
if (!wanted) return null; // no benchmark speaks to this category yet
let hit;
try {
hit = this.qualityEvals.lookup(model.model_identifier || model.name, model.paramsB, category, model);
} catch { return null; }
if (!hit?.evals?.length) return null;
// Take the most preferred metric that exists for this model.
const byMetric = new Map(hit.evals.map((e) => [e.metric, e]));
const chosen = wanted.map((m) => byMetric.get(m)).filter(Boolean)
.find((entry) => this.qualityEvals.percentile(entry.metric, entry.score) != null);
if (!chosen) return null;
// BigCodeBench runs far below HumanEval on the same model (its top
// score is ~50 where HumanEval saturates near 90), so the two are
// rescaled onto a common axis instead of being compared directly.
// Rank within the metric's own distribution rather than against a
// fixed ceiling. Boards differ wildly in difficulty — BigCodeBench
// tops out near 51 while HumanEval+ saturates at 89, and LiveBench's
// reasoning median sits at 70 because frontier hosted models dominate
// it. A percentile puts every metric on one comparable axis.
const pct = this.qualityEvals.percentile(chosen.metric, chosen.score);
if (pct == null) return null; // too thin a cohort to rank against
// Map the percentile onto the SAME band the size estimator produces
// (getBaseQuality spans 45-95). Handing back a raw 0-100 percentile
// instead makes measured and estimated models incomparable: measured
// ones spread across the whole axis while unmeasured ones cluster high,
// so a benchmarked model scores below an unbenchmarked one and gets
// punished for having been evaluated. Verified before this change:
// qwen3@30B measured = 20.8 vs deepseek-r1@14B estimated = 88.0.
const EST_FLOOR = 45;
const EST_CEIL = 95;
const score = EST_FLOOR + (pct / 100) * (EST_CEIL - EST_FLOOR);
return {
score,
provenance: {
kind: 'measured',
metric: chosen.metric,
rawScore: chosen.score,
source: chosen.sourceName,
sourceUrl: chosen.sourceUrl,
scoreUnit: chosen.scoreUnit,
independent: chosen.independent,
benchModel: chosen.benchModel,
precision: chosen.precision,
// The board did not publish a size, so this measures the
// family rather than this exact build. Shown as such.
sizeUnknown: Boolean(hit.sizeUnknown),
},
};
}
getBaseQuality(paramsB) {
// Find closest parameter count in our table
const keys = Object.keys(this.baseQualityByParams).map(Number).sort((a, b) => a - b);
for (let i = 0; i < keys.length; i++) {
if (paramsB <= keys[i]) {
return this.baseQualityByParams[keys[i]];
}
}
// If larger than our table, return the largest
return this.baseQualityByParams[keys[keys.length - 1]];
}
getTaskAlignmentBump(model, category) {
const name = model.name.toLowerCase();
const tags = model.tags;
switch (category) {
case 'coding':
if (tags.includes('coder') || name.includes('code')) return 6;
if (tags.includes('instruct')) return 2;
return 0;
case 'multimodal':
if (model.modalities?.includes('vision')) return 6;
return 0;
case 'general':
if (tags.includes('chat') || tags.includes('instruct')) return 4;
if (name.includes('code')) return 2;
return 0;
default:
return 0;
}
}
calculateFreshnessAdjustment(model = {}) {
const freshnessScore = Number.isFinite(model.freshnessScore) ? model.freshnessScore : 55;
const ageDays = Number.isFinite(model.modelAgeDays) ? model.modelAgeDays : null;
const isDeprecated = Boolean(model.isDeprecated);
const isStale = Boolean(model.isStale);
if (isDeprecated) return -12;
if (ageDays !== null && ageDays > this.freshnessThresholds.veryStaleDays) return -8;
if (ageDays !== null && ageDays > this.freshnessThresholds.staleDays) return -4;
if (isStale) return -3;
if (freshnessScore >= 90) return 3;
if (freshnessScore >= 75) return 2;
if (freshnessScore >= 60) return 1;
if (freshnessScore <= 25) return -4;
return 0;
}
estimateSpeed(hardware, model, quant, category, runtime = 'ollama') {
return this.estimateSpeedProfile(hardware, model, quant, category, runtime).score;
}
estimateSpeedProfile(hardware, model, quant, category, runtime = 'ollama') {
const backend = detectedBackend(hardware);
// Base speed calculation
const K = this.backendK[backend];
const denseParamsB = Number.isFinite(this.parseBillionsValue(model.paramsB))
? this.parseBillionsValue(model.paramsB)
: 1;
const parameterProfile = this.resolveMemoryParameterProfile(model);
const effectiveParamsB = Number.isFinite(parameterProfile.effectiveParamsB) && parameterProfile.effectiveParamsB > 0
? parameterProfile.effectiveParamsB
: denseParamsB;
let base = K / effectiveParamsB;
// Quantization multiplier
const quantMultiplier = this.quantSpeedMultipliers[quant] ?? precisionProfile(quant).speed;
base *= quantMultiplier;
// Threading multiplier
if (hardware.cpu.cores >= 8) base *= 1.1;
if (!backend.startsWith('cpu_')) base *= 1.2;
const acceleratorScale = this.calculateAcceleratorSpeedScale(hardware, backend);
base *= acceleratorScale.multiplier;
const normalizedRuntime = normalizeMoERuntime(runtime);
const moe = estimateMoESpeedMultiplier({
model,
runtime: normalizedRuntime,
denseParamsB,
parameterProfile
});
if (moe.applied) {
base *= moe.multiplier;
}
// Normalize to 0-100 score
const target = this.targetSpeeds[category] || this.targetSpeeds.general;
const estimatedTPS = Math.max(1, Math.round(base * 10) / 10);
const score = Math.min(100, Math.round((100 * estimatedTPS / target) * 10) / 10);
return {
backend,
basis: 'backend_profile_estimate',
targetTPS: target,
estimatedTPS,
score,
runtime: normalizedRuntime,
moe,
acceleratorScale
};
}
calculateAcceleratorSpeedScale(hardware = {}, backend = 'cpu_x86') {
if (!['cuda', 'metal', 'rocm', 'vulkan', 'sycl'].includes(backend)) {
return { multiplier: 1, reason: null };
}
const gpu = hardware.gpu || {};
const memory = hardware.memory || {};
const toFiniteNumber = (value, fallback = 0) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
const vramGB = toFiniteNumber(gpu.vramGB ?? gpu.vram ?? gpu.totalVRAM, 0);
const ramGB = toFiniteNumber(memory.totalGB ?? memory.total, 0);
const acceleratorMemoryGB = backend === 'metal' && Boolean(gpu.unified)
? Math.max(vramGB, ramGB)
: vramGB;
const gpuCount = Math.max(1, toFiniteNumber(gpu.gpuCount ?? gpu.count, 1));
let multiplier = 1;
if (acceleratorMemoryGB >= 160) multiplier *= 3.2;
else if (acceleratorMemoryGB >= 96) multiplier *= 2.6;
else if (acceleratorMemoryGB >= 80) multiplier *= 2.2;
else if (acceleratorMemoryGB >= 48) multiplier *= 1.7;
else if (acceleratorMemoryGB >= 24) multiplier *= 1.15;
if (['cuda', 'rocm'].includes(backend) && gpuCount > 1) {
multiplier *= Math.min(1.8, 1 + ((gpuCount - 1) * 0.25));
}
const rounded = Math.round(multiplier * 100) / 100;
return {
multiplier: rounded,
reason: rounded > 1
? `${backend.toUpperCase()} capacity x${rounded}`
: null
};
}
calculateFitScore(requiredGB, budgetGB) {
const ratio = requiredGB / budgetGB;
if (ratio <= 0.9) return 100;
if (ratio <= 1.0) return 70;
return 0; // Unreachable in practice: evaluateModel drops requiredGB > budget.
}
calculateContextScore(model, targetCtx) {
const ctxMax = Number(model?.ctxMax) || 0;
if (ctxMax >= targetCtx) return 100;
if (ctxMax >= targetCtx * 0.5) return 70;
// Required contexts are filtered before scoring. Preferred contexts
// contribute a lower score when the model window is shorter.
return 0;
}
getHighCapacitySizeTarget(budgetGB, hardware = {}) {
if (!Number.isFinite(budgetGB) || budgetGB < 32) return null;
const isMultiGPU = Boolean(hardware?.gpu?.isMultiGPU);
if (budgetGB >= 128) return { minParamsB: 30, sweetSpotParamsB: 70 };
if (budgetGB >= 80) return { minParamsB: 30, sweetSpotParamsB: 70 };
if (budgetGB >= 48) return { minParamsB: 20, sweetSpotParamsB: 34 };
if (budgetGB >= 32 && isMultiGPU) return { minParamsB: 30, sweetSpotParamsB: 30 };
if (budgetGB >= 32) return { minParamsB: 13, sweetSpotParamsB: 30 };
return null;
}
calculateHighCapacitySizeAdjustment(hardware, model, budgetGB, category, optimizeFor = 'balanced') {
const objective = this.normalizeOptimizationObjective(optimizeFor);
if (objective === 'speed' || category === 'embeddings') {
return { score: 0, reason: null };
}
const normalizedHardware = this.normalizeHardwareProfile(hardware || {});
const tier = this.mapHardwareTier(normalizedHardware);
const highCapacityTiers = new Set(['very_high', 'ultra_high', 'extreme', 'flagship']);
const target = this.getHighCapacitySizeTarget(budgetGB, normalizedHardware);
const hasHighCapacitySignal =
Boolean(target) ||
highCapacityTiers.has(tier) ||
Number(normalizedHardware?.gpu?.vramGB || 0) >= 48;
if (!hasHighCapacitySignal || !target) {
return { score: 0, reason: null };
}
const params = this.parseBillionsValue(model?.paramsB);
if (!Number.isFinite(params) || params <= 0) {
return { score: 0, reason: null };
}
const categoryMultiplier = category === 'multimodal' ? 0.6 : 1;
if (params < target.minParamsB) {
const deficitRatio = (target.minParamsB - params) / target.minParamsB;
const penalty = -Math.min(24, deficitRatio * 24) * categoryMultiplier;
const roundedPenalty = Math.round(penalty * 10) / 10;
return {
score: roundedPenalty,
reason: `below ${target.minParamsB}B high-capacity floor`
};
}
const distanceRatio = Math.min(
1,
Math.abs(params - target.sweetSpotParamsB) / target.sweetSpotParamsB
);
const bonus = Math.max(0, 12 * (1 - distanceRatio)) * categoryMultiplier;
const roundedBonus = Math.round(bonus * 10) / 10;
return {
score: roundedBonus,
reason: roundedBonus > 0
? `${target.sweetSpotParamsB}B high-capacity target`
: null
};
}
estimatePracticalMaxParamsForBudget(budgetGB) {
if (!Number.isFinite(budgetGB) || budgetGB <= 0) return 4;
if (budgetGB >= 80) return 70;
if (budgetGB >= 48) return 46;
if (budgetGB >= 32) return 30;
if (budgetGB >= 24) return 14;
if (budgetGB >= 16) return 8;
return 4;
}
ensureFeasibleMidTierCoverage(selectedCandidates, allCandidates, category, hardware, optimizeFor = 'balanced') {
if (!Array.isArray(selectedCandidates) || selectedCandidates.length === 0) {
return selectedCandidates;
}
const objective = this.normalizeOptimizationObjective(optimizeFor);
if (objective === 'speed') {
return selectedCandidates;
}
const enforceCategories = new Set(['general', 'talking', 'reading', 'coding', 'reasoning', 'multimodal']);
if (!enforceCategories.has(category)) {
return selectedCandidates;
}
const normalizedHardware = this.normalizeHardwareProfile(hardware || {});
const budget = normalizedHardware.gpu.unified
? normalizedHardware.usableMemGB
: (normalizedHardware.gpu.vramGB || normalizedHardware.usableMemGB);
if (!Number.isFinite(budget) || budget < 16) {
return selectedCandidates;
}
const candidatePool = Array.isArray(allCandidates) && allCandidates.length > 0
? allCandidates
: selectedCandidates;
let promoted = [...selectedCandidates];
const minMidTierParams = budget >= 24 ? 7 : 6;
const alreadyHasMidTier = promoted.some((candidate) => (candidate?.meta?.paramsB || 0) >= minMidTierParams);
if (!alreadyHasMidTier) {
const practicalSpeedFloor = normalizedHardware.gpu.unified ? 25 : 20;
const feasibleMidTier = candidatePool.find((candidate) => {
const params = candidate?.meta?.paramsB || 0;
const speedScore = candidate?.components?.S ?? candidate?.estTPS ?? 0;
return params >= minMidTierParams && speedScore >= practicalSpeedFloor;
});
if (
feasibleMidTier &&
!promoted.some((candidate) => candidate?.meta?.model_identifier === feasibleMidTier?.meta?.model_identifier)
) {
promoted[promoted.length - 1] = feasibleMidTier;
promoted.sort((a, b) => b.score - a.score);
}
}
const practicalMaxParams = this.estimatePracticalMaxParamsForBudget(budget);
const shouldEnforceThirtyBCoverage =
Boolean(normalizedHardware?.gpu?.isMultiGPU) &&
!Boolean(normalizedHardware?.gpu?.unified) &&
practicalMaxParams >= 30;
if (!shouldEnforceThirtyBCoverage || objective === 'speed') {
return promoted;
}
const alreadyHasThirtyB = promoted.some((candidate) => (candidate?.meta?.paramsB || 0) >= 30);
if (alreadyHasThirtyB) {
return promoted;
}
const largeModelSpeedFloor = Math.max(
8,
Math.round((this.targetSpeeds[category] || this.targetSpeeds.general) * 0.2)
);
const feasibleThirtyB = candidatePool.find((candidate) => {
const params = candidate?.meta?.paramsB || 0;
const estTPS = candidate?.estTPS ?? candidate?.speed?.estimatedTPS ?? 0;
return params >= 30 && estTPS >= largeModelSpeedFloor;
});
if (!feasibleThirtyB) {
return promoted;
}
if (promoted.some((candidate) => candidate?.meta?.model_identifier === feasibleThirtyB?.meta?.model_identifier)) {
return promoted;
}
const highCapacityPromoted = [...promoted];
highCapacityPromoted[highCapacityPromoted.length - 1] = feasibleThirtyB;
highCapacityPromoted.sort((a, b) => b.score - a.score);
return highCapacityPromoted;
}
buildRationale(
hardware,
model,
quant,
requiredGB,
budget,
category,
Q,
S,
memoryEstimate = null,
speedEstimate = null,
capacityAdjustment = null
) {
const parts = [];
// Memory fit
parts.push(`fits in ${requiredGB}/${budget}GB`);
// Quantization
parts.push(quant);
// Special attributes
if (model.tags.includes('coder')) parts.push('coder-tuned');
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');
const memoryProfile = memoryEstimate?.parameterProfile;
if (memoryProfile?.isMoE) {
const assumptionLabels = {
moe_active_metadata: 'MoE active params',
moe_derived_expert_ratio: 'MoE derived active ratio',
moe_fallback_total_params: 'MoE fallback total params',
moe_fallback_model_params: 'MoE fallback model params',
moe_fallback_default: 'MoE fallback default'
};
parts.push(assumptionLabels[memoryProfile.assumptionSource] || memoryProfile.assumptionSource);
}
if (speedEstimate?.moe?.applied) {
const runtimeLabel = speedEstimate.runtime || 'ollama';
const multiplier = Number(speedEstimate.moe.multiplier || 1).toFixed(2);
parts.push(`MoE speed x${multiplier} (${runtimeLabel})`);
}
if (speedEstimate?.acceleratorScale?.multiplier > 1) {
parts.push(speedEstimate.acceleratorScale.reason);
}
if (capacityAdjustment?.reason) {
parts.push(capacityAdjustment.reason);
}
// Size sweet spot
if (model.paramsB >= 7 && model.paramsB <= 13) {
parts.push(`${model.paramsB}B is sweet spot`);
}
// Backend
const backend = speedEstimate?.backend || detectedBackend(hardware);
parts.push(`${backend.replace('_', ' ').toUpperCase()} speed estimate`);
return parts.join(', ');
}
// ============================================================================
// PHASE 2: QUICK PROBE (Optional)
// ============================================================================
async runQuickProbes(candidates, hardware, category) {
this.activeCpuOnly = Boolean(hardware?.cpuOnly);
if (typeof this.ollamaClient.setCpuOnly === 'function') {
this.ollamaClient.setCpuOnly(this.activeCpuOnly);
}
// Load cached results
const cache = this.loadBenchCache();
const hardwareFingerprint = this.getHardwareFingerprint(hardware);
for (const candidate of candidates) {
const cacheKey = `${hardwareFingerprint}_${candidate.meta.model_identifier}@${candidate.quant}`;
// Check cache first
if (cache[cacheKey] && this.isCacheValid(cache[cacheKey])) {
const cachedTPS = cache[cacheKey].tps;
this.updateCandidateWithMeasuredSpeed(candidate, cachedTPS, category);
candidate.rationale += ` | measured ${cachedTPS.toFixed(1)} t/s (cached)`;
continue;
}
// Run probe
try {
const measuredTPS = await this.runSingleProbe(candidate.meta.model_identifier, category);
this.updateCandidateWithMeasuredSpeed(candidate, measuredTPS, category);
candidate.rationale += ` | measured ${measuredTPS.toFixed(1)} t/s`;
// Cache result
cache[cacheKey] = {
tps: measuredTPS,
timestamp: Date.now(),
category
};
this.saveBenchCache(cache);
} catch (error) {
console.warn(`Probe failed for ${candidate.meta.name}: ${error.message}`);
}
}
}
async runSingleProbe(modelId, category) {
const prompts = {
'coding': 'Write 3 bullet points about the benefits of unit tests.',
'general': 'Explain the benefits of regular exercise in 3 sentences.',
'reasoning': 'What are the steps to solve a quadratic equation?',
'multimodal': 'Describe what you see in this image.', // Text-only fallback
'summarization': 'Summarize the key points of effective communication.',
'reading': 'What are the main themes in classic literature?'
};
const prompt = prompts[category] || prompts['general'];
const targetTokens = 128;
const result = await this.ollamaClient.generate(modelId, prompt, {
generationOptions: {
num_predict: targetTokens,
...(this.activeCpuOnly ? { num_gpu: 0 } : {})
}
});
if (Number.isFinite(result.tokensPerSecond) && result.tokensPerSecond > 0) {
return result.tokensPerSecond;
}
const elapsedSeconds = Math.max(0.001, Number(result.responseTime || 0) / 1000);
const estimatedResponseTokens = result.response
? result.response.split(/\s+/).filter(Boolean).length * 1.3
: targetTokens;
const tokensGenerated = Number(result.eval_count) || estimatedResponseTokens;
return tokensGenerated / elapsedSeconds;
}
updateCandidateWithMeasuredSpeed(candidate, measuredTPS, category) {
const normalizedS = this.normalizeTPSToScore(measuredTPS, category);
// Re-score with the measured speed using the SAME weighting source as
// evaluateModel: getScoringWeights honours the user's optimizeFor profile and
// falls back to the general weights for categories (e.g. 'talking') that have
// no entry in DETERMINISTIC_WEIGHTS — indexing this.categoryWeights[category]
// directly threw a TypeError for those. We also re-add the stored capacity
// adjustment (H) and clamp, so a probed score stays comparable to a
// non-probed one instead of being silently lower.
const weights = this.getScoringWeights(category, candidate.optimizeFor || 'balanced');
const { Q, F, C, H = 0 } = candidate.components;
candidate.estTPS = measuredTPS;
candidate.components.S = normalizedS;
const weighted = Q * weights[0] + normalizedS * weights[1] + F * weights[2] + C * weights[3];
candidate.score = Math.max(0, Math.min(100, Math.round((weighted + H) * 10) / 10));
}
normalizeTPSToScore(tps, category) {
const target = this.targetSpeeds[category] || this.targetSpeeds.general;
return Math.min(100, Math.round((100 * tps / target) * 10) / 10);
}
loadBenchCache() {
try {
if (fs.existsSync(this.benchCachePath)) {
return JSON.parse(fs.readFileSync(this.benchCachePath, 'utf8'));
}
} catch (error) {
console.warn('Failed to load benchmark cache:', error.message);
}
return {};
}
saveBenchCache(cache) {
try {
const dir = path.dirname(this.benchCachePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.benchCachePath, JSON.stringify(cache, null, 2));
} catch (error) {
console.warn('Failed to save benchmark cache:', error.message);
}
}
isCacheValid(cacheEntry) {
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
return (Date.now() - cacheEntry.timestamp) < maxAge;
}
getHardwareFingerprint(hardware) {
const executionMode = hardware.cpuOnly
? 'cpu-only'
: (hardware.summary?.bestBackend || hardware.gpu.type || 'cpu');
return `${hardware.cpu.architecture}_${hardware.cpu.cores}c_${hardware.memory.totalGB}gb_${executionMode}`;
}
// ============================================================================
// FORMAT HELPERS (migrated from enhanced-selector.js)
// ============================================================================
/**
* Map a candidate to the legacy format expected by callers
*/
mapCandidateToLegacyFormat(candidate) {
const provenance = candidate.meta.provenance || {
source: candidate.meta.source || 'unknown',
registry: candidate.meta.registry || 'unknown',
version: candidate.meta.version || 'unknown',
license: candidate.meta.license || 'unknown',
digest: candidate.meta.digest || 'unknown'
};
return {
model_name: candidate.meta.name,
model_identifier: candidate.meta.model_identifier,
categoryScore: candidate.score,
hardwareScore: candidate.components ? candidate.components.F : 90,
specializationScore: candidate.components ? candidate.components.Q : 85,
popularityScore: candidate.components ? Math.min(100, (candidate.meta.pulls || 0) / 100000 * 100) : 10,
efficiencyScore: candidate.components ? candidate.components.S : 80,
pulls: candidate.meta.pulls || 0,
size: candidate.meta.paramsB,
family: candidate.meta.family,
category: this.inferCategoryFromModel(candidate.meta),
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 || '',
downloadUrl: candidate.meta.downloadUrl || provenance.download_url || '',
artifactFormat: candidate.meta.artifact?.format || '',
memoryAssumptionSource: candidate.memory?.assumptionSource || 'dense_params',
// Where the quality half of the score came from: a real benchmark
// or the parameter-count estimate. Carried all the way out so the
// UI can label it — a 23%-measured catalog must not present an
// estimate and a measurement as the same kind of number.
qualitySource: candidate.meta.qualitySource || { kind: 'estimated', basis: 'parameter count' },
speedAssumptions: candidate.speed?.moe ? {
applied: Boolean(candidate.speed.moe.applied),
runtime: candidate.speed.runtime || candidate.runtime || 'ollama',
multiplier: Number.isFinite(candidate.speed.moe.multiplier) ? candidate.speed.moe.multiplier : 1,
theoreticalSpeedup: Number.isFinite(candidate.speed.moe.theoreticalSpeedup) ? candidate.speed.moe.theoreticalSpeedup : 1,
overheadMultiplier: Number.isFinite(candidate.speed.moe.overheadMultiplier) ? candidate.speed.moe.overheadMultiplier : 1,
assumptionSource: candidate.speed.moe.assumptionSource || candidate.memory?.assumptionSource || 'dense_params'
} : null,
source: provenance.source,
registry: provenance.registry,
version: provenance.version,
license: provenance.license,
digest: provenance.digest,
provenance
};
}
mapHardwareTier(hardware = {}) {
const summary = hardware?.summary || {};
const canonicalTier = summary.hardwareTier || summary.hardware_tier;
if (typeof canonicalTier === 'string' && canonicalTier.trim()) {
return canonicalTier.trim().toLowerCase().replace(/\s+/g, '_');
}
const effectiveMemory = Number(summary.effectiveMemory);
const speedCoefficient = Number(summary.speedCoefficient);
if (Number.isFinite(effectiveMemory) && effectiveMemory > 0 && Number.isFinite(speedCoefficient)) {
if (effectiveMemory >= 80 && speedCoefficient >= 300) return 'ultra_high';
if (effectiveMemory >= 48 && speedCoefficient >= 200) return 'very_high';
if (effectiveMemory >= 24 && speedCoefficient >= 150) return 'high';
if (effectiveMemory >= 16 && speedCoefficient >= 100) return 'medium_high';
if (effectiveMemory >= 12 && speedCoefficient >= 80) return 'medium';
if (effectiveMemory >= 8 && speedCoefficient >= 50) return 'medium_low';
if (effectiveMemory >= 6 && speedCoefficient >= 30) return 'low';
return 'ultra_low';
}
let ram, cores;
if (hardware?.memory?.totalGB) {
ram = hardware.memory.totalGB;
} else if (hardware?.memory?.total) {
ram = hardware.memory.total;
} else if (hardware?.total_ram_gb) {
ram = hardware.total_ram_gb;
} else {
ram = 8;
}
if (hardware?.cpu?.cores) {
cores = hardware.cpu.cores;
} else if (hardware?.cpu_cores) {
cores = hardware.cpu_cores;
} else {
cores = 4;
}
const gpu = hardware?.gpu || {};
const gpuCount =
(Number.isFinite(Number(gpu.gpuCount)) ? Number(gpu.gpuCount) : null) ??
(Number.isFinite(Number(hardware?.gpuCount)) ? Number(hardware.gpuCount) : null) ??
1;
const totalVRAM =
(Number.isFinite(Number(gpu.vramGB)) ? Number(gpu.vramGB) : null) ??
(Number.isFinite(Number(gpu.vram)) ? Number(gpu.vram) : null) ??
(Number.isFinite(Number(gpu.totalVRAM)) ? Number(gpu.totalVRAM) : null) ??
0;
const unifiedGPU = Boolean(gpu.unified) || gpu.type === 'apple_silicon';
const effectiveAcceleratorMem = unifiedGPU ? Math.max(totalVRAM, ram) : totalVRAM;
if (effectiveAcceleratorMem >= 80 || (ram >= 64 && cores >= 16)) return 'extreme';
if (effectiveAcceleratorMem >= 48 || (ram >= 32 && cores >= 12)) return 'very_high';
if (effectiveAcceleratorMem >= 24 || (ram >= 16 && cores >= 8)) return 'high';
if (gpuCount >= 2 && effectiveAcceleratorMem >= 20) return 'high';
if (ram >= 8 && cores >= 4) return 'medium';
return 'low';
}
getCategoryInfo(category) {
const categoryData = {
coding: { weight: 1.0, keywords: ['code', 'programming', 'coder'] },
reasoning: { weight: 1.2, keywords: ['reasoning', 'logic', 'math'] },
multimodal: { weight: 1.1, keywords: ['vision', 'image', 'multimodal'] },
creative: { weight: 0.9, keywords: ['creative', 'writing', 'story'] },
talking: { weight: 1.0, keywords: ['chat', 'conversation', 'assistant'] },
reading: { weight: 1.0, keywords: ['reading', 'comprehension', 'text'] },
general: { weight: 1.0, keywords: ['general', 'assistant', 'helper'] }
};
return categoryData[category] || categoryData.general;
}
inferCategoryFromModel(model) {
const name = model.name.toLowerCase();
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('embed')) return 'embeddings';
if (name.includes('creative') || name.includes('wizard')) return 'creative';
return 'general';
}
formatModelSize(model) {
if (model.paramsB) return `${model.paramsB}B`;
if (model.size) return `${model.size}B`;
return 'Unknown';
}
/**
* Generate recommendations by category (main API, replaces EnhancedModelSelector)
*/
async getBestModelsForHardware(hardware, allModels, options = {}) {
const categories = ['coding', 'reasoning', 'multimodal', 'creative', 'talking', 'reading', 'general'];
const recommendations = {};
const normalizedPool = this.normalizeExternalModels(Array.isArray(allModels) ? allModels : []);
const installedModels = await this.getInstalledModels();
const normalizedHardware = this.normalizeHardwareProfile(hardware || await this.getHardware(), {
...(Object.prototype.hasOwnProperty.call(options, 'cpuOnly')
? { cpuOnly: options.cpuOnly }
: {})
});
const runtime = normalizeMoERuntime(options.runtime || 'ollama');
const optimizationObjective = this.normalizeOptimizationObjective(
options.optimizeFor || options.optimize || options.objective
);
for (const category of categories) {
try {
const result = await this.selectModels(category, {
topN: 3,
enableProbe: false,
silent: true,
optimizeFor: optimizationObjective,
runtime,
...(Object.prototype.hasOwnProperty.call(options, 'cpuOnly')
? { cpuOnly: options.cpuOnly }
: {}),
hardware: normalizedHardware,
installedModels,
modelPool: normalizedPool,
includeUncensored: options.includeUncensored === true
});
recommendations[category] = {
tier: this.mapHardwareTier(normalizedHardware),
optimizeFor: optimizationObjective,
runtime,
bestModels: result.candidates.map(candidate => this.mapCandidateToLegacyFormat(candidate)),
totalEvaluated: result.total_evaluated,
category: this.getCategoryInfo(category)
};
} catch (error) {
recommendations[category] = {
tier: this.mapHardwareTier(normalizedHardware),
optimizeFor: optimizationObjective,
runtime,
bestModels: [],
totalEvaluated: 0,
category: this.getCategoryInfo(category)
};
}
}
return recommendations;
}
/**
* Generate recommendation summary
*/
generateRecommendationSummary(recommendations, hardware, options = {}) {
const summary = {
hardware_tier: this.mapHardwareTier(hardware),
optimize_for: this.normalizeOptimizationObjective(
options.optimizeFor || options.optimize || options.objective
),
total_categories: Object.keys(recommendations).length,
best_overall: null,
by_category: {},
quick_commands: []
};
let bestOverallScore = 0;
let bestOverallModel = null;
let bestOverallCategory = null;
Object.entries(recommendations).forEach(([category, data]) => {
const bestModel = data.bestModels[0];
if (bestModel) {
const command = bestModel.installCommand ||
bestModel.provenance?.install_command ||
`ollama pull ${bestModel.model_identifier}`;
summary.by_category[category] = {
name: bestModel.model_name || bestModel.name,
identifier: bestModel.model_identifier,
score: Math.round(bestModel.categoryScore || bestModel.score),
command,
size: this.formatModelSize(bestModel),
quantization: bestModel.quantization || bestModel.quant || 'Q4_K_M',
runtime: bestModel.runtime || bestModel.provenance?.runtime || 'ollama',
pulls: bestModel.pulls || 0,
source: bestModel.source || bestModel.provenance?.source || 'unknown',
registry: bestModel.registry || bestModel.provenance?.registry || 'unknown',
version: bestModel.version || bestModel.provenance?.version || 'unknown',
license: bestModel.license || bestModel.provenance?.license || 'unknown',
digest: bestModel.digest || bestModel.provenance?.digest || 'unknown',
download_url: bestModel.downloadUrl || bestModel.provenance?.download_url || '',
provenance: bestModel.provenance || {
source: bestModel.source || 'unknown',
registry: bestModel.registry || 'unknown',
version: bestModel.version || 'unknown',
license: bestModel.license || 'unknown',
digest: bestModel.digest || 'unknown'
}
};
summary.quick_commands.push(command);
const isGeneralCategory = ['general', 'coding', 'talking', 'reading'].includes(category);
const score = bestModel.categoryScore || bestModel.score || 0;
if (isGeneralCategory && (score > bestOverallScore || !bestOverallModel)) {
bestOverallScore = score;
bestOverallModel = bestModel;
bestOverallCategory = category;
}
}
});
if (bestOverallModel) {
const command = bestOverallModel.installCommand ||
bestOverallModel.provenance?.install_command ||
`ollama pull ${bestOverallModel.model_identifier}`;
summary.best_overall = {
name: bestOverallModel.model_name || bestOverallModel.name,
identifier: bestOverallModel.model_identifier,
category: bestOverallCategory,
score: Math.round(bestOverallScore),
command,
quantization: bestOverallModel.quantization || bestOverallModel.quant || 'Q4_K_M',
runtime: bestOverallModel.runtime || bestOverallModel.provenance?.runtime || 'ollama',
source: bestOverallModel.source || bestOverallModel.provenance?.source || 'unknown',
registry: bestOverallModel.registry || bestOverallModel.provenance?.registry || 'unknown',
version: bestOverallModel.version || bestOverallModel.provenance?.version || 'unknown',
license: bestOverallModel.license || bestOverallModel.provenance?.license || 'unknown',
digest: bestOverallModel.digest || bestOverallModel.provenance?.digest || 'unknown',
download_url: bestOverallModel.downloadUrl || bestOverallModel.provenance?.download_url || '',
provenance: bestOverallModel.provenance || {
source: bestOverallModel.source || 'unknown',
registry: bestOverallModel.registry || 'unknown',
version: bestOverallModel.version || 'unknown',
license: bestOverallModel.license || 'unknown',
digest: bestOverallModel.digest || 'unknown'
}
};
}
return summary;
}
// ============================================================================
// PUBLIC API
// ============================================================================
async recommend(category = 'general', options = {}) {
const result = await this.selectModels(category, options);
return this.formatRecommendations(result);
}
formatRecommendations(result) {
const { category, hardware, candidates, total_evaluated } = result;
console.log(`\n${category.toUpperCase()} RECOMMENDATIONS`);
console.log(`Hardware: ${hardware.cpu.cores} cores, ${hardware.memory.totalGB}GB RAM, ${hardware.gpu.type}`);
console.log(`Evaluated ${total_evaluated} models\n`);
if (candidates.length === 0) {
console.log('❌ No suitable models found for your hardware');
return result;
}
// Table header
console.log('┌─────────────────────────────┬────────┬───────┬─────────┬──────────┬───────┬─────────────────────────────┐');
console.log('│ Model │ Params │ Quant │ Est t/s │ Mem GB │ Score │ Why │');
console.log('├─────────────────────────────┼────────┼───────┼─────────┼──────────┼───────┼─────────────────────────────┤');
candidates.forEach((candidate, index) => {
const isInstalled = candidate.meta.installed ? 'INSTALLED' : 'CLOUD';
const name = candidate.meta.name.padEnd(26);
const params = `${candidate.meta.paramsB}B`.padEnd(5);
const quant = candidate.quant.padEnd(6);
const tps = candidate.estTPS.toFixed(1).padStart(7);
const mem = `${candidate.requiredGB}/${hardware.usableMemGB}`.padEnd(9);
const score = candidate.score.toFixed(1).padStart(5);
const why = candidate.rationale.substring(0, 29);
console.log(`│ ${isInstalled}${name} │ ${params} │ ${quant} │ ${tps} │ ${mem} │ ${score} │ ${why} │`);
});
console.log('└─────────────────────────────┴────────┴───────┴─────────┴──────────┴───────┴─────────────────────────────┘');
// Best pick
const best = candidates[0];
console.log(`\nBEST PICK: ${best.meta.name}`);
console.log(`Command: ollama pull ${best.meta.model_identifier}`);
console.log(`Why: ${best.rationale}`);
console.log(`Score: ${best.score} (Q:${best.components.Q} S:${best.components.S} F:${best.components.F} C:${best.components.C})`);
return result;
}
}
module.exports = DeterministicModelSelector;