406 lines
10 KiB
JavaScript
406 lines
10 KiB
JavaScript
/**
|
|
* LLM catalog — Hugging Face GGUFs that already exist as @qvac/sdk constants.
|
|
* Arbitrary HF repos will not load.
|
|
*/
|
|
|
|
const CATALOG = [
|
|
{
|
|
id: 'qwen3.5-0.8b',
|
|
constant: 'QWEN3_5_0_8B_MULTIMODAL_Q4_K_M',
|
|
name: 'Qwen3.5 0.8B',
|
|
tools: true,
|
|
compactTools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_5_0_8B_MULTIMODAL_Q8_0',
|
|
minRamGb: 4,
|
|
approxDownloadGb: 0.7,
|
|
ctxSize: 16384,
|
|
},
|
|
{
|
|
id: 'qwen3.5-2b',
|
|
constant: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M',
|
|
name: 'Qwen3.5 2B',
|
|
tools: true,
|
|
compactTools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0',
|
|
minRamGb: 6,
|
|
approxDownloadGb: 1.6,
|
|
ctxSize: 16384,
|
|
},
|
|
{
|
|
id: 'qwen3.5-4b',
|
|
constant: 'QWEN3_5_4B_MULTIMODAL_Q4_K_M',
|
|
name: 'Qwen3.5 4B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0',
|
|
minRamGb: 10,
|
|
approxDownloadGb: 2.8,
|
|
ctxSize: 16384,
|
|
},
|
|
{
|
|
id: 'qwen3.5-9b',
|
|
constant: 'QWEN3_5_9B_MULTIMODAL_Q4_K_M',
|
|
name: 'Qwen3.5 9B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_5_9B_MULTIMODAL_F16',
|
|
minRamGb: 16,
|
|
approxDownloadGb: 5.5,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'gemma4-2b',
|
|
constant: 'GEMMA4_2B_MULTIMODAL_Q4_K_M',
|
|
name: 'Gemma4 E2B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_GEMMA4_2B_MULTIMODAL_Q8_0',
|
|
minRamGb: 8,
|
|
approxDownloadGb: 3.5,
|
|
ctxSize: 16384,
|
|
},
|
|
{
|
|
id: 'gemma4-4b',
|
|
constant: 'GEMMA4_4B_MULTIMODAL_Q4_K_M',
|
|
name: 'Gemma4 E4B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_GEMMA4_4B_MULTIMODAL_Q8_0',
|
|
minRamGb: 16,
|
|
approxDownloadGb: 5,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'qwen3-8b',
|
|
constant: 'QWEN3_8B_INST_Q4_K_M',
|
|
name: 'Qwen3 8B',
|
|
tools: true,
|
|
vision: false,
|
|
minRamGb: 16,
|
|
approxDownloadGb: 5,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'qwen3vl-2b',
|
|
constant: 'QWEN3_VL_2B_INSTRUCT_Q4_K_M',
|
|
name: 'Qwen3 VL 2B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_VL_2B_INSTRUCT_Q8_0',
|
|
minRamGb: 6,
|
|
approxDownloadGb: 1.5,
|
|
ctxSize: 8192,
|
|
},
|
|
{
|
|
id: 'qwen3.6-27b',
|
|
constant: 'QWEN3_6_27B_MULTIMODAL_Q4_K_XL',
|
|
name: 'Qwen3.6 27B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_6_27B_MULTIMODAL_F16',
|
|
minRamGb: 40,
|
|
approxDownloadGb: 16,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'qwen3.6-35b-a3b',
|
|
constant: 'QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M',
|
|
name: 'Qwen3.6 35B A3B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16',
|
|
minRamGb: 48,
|
|
approxDownloadGb: 20,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'gpt-oss-20b',
|
|
constant: 'GPT_OSS_20B_INST_Q4_K_M',
|
|
name: 'GPT-OSS 20B',
|
|
tools: true,
|
|
vision: false,
|
|
minRamGb: 32,
|
|
approxDownloadGb: 12,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'gemma4-31b',
|
|
constant: 'GEMMA4_31B_MULTIMODAL_Q4_K_M',
|
|
name: 'Gemma4 31B',
|
|
tools: true,
|
|
vision: true,
|
|
mmproj: 'MMPROJ_GEMMA4_31B_MULTIMODAL_F16',
|
|
minRamGb: 48,
|
|
approxDownloadGb: 18,
|
|
ctxSize: 32768,
|
|
},
|
|
{
|
|
id: 'qwen3-0.6b',
|
|
constant: 'QWEN3_600M_INST_Q4',
|
|
name: 'Qwen3 0.6B (lite)',
|
|
tools: false,
|
|
vision: false,
|
|
minRamGb: 4,
|
|
approxDownloadGb: 0.5,
|
|
ctxSize: 4096,
|
|
},
|
|
{
|
|
id: 'qwen3-1.7b',
|
|
constant: 'QWEN3_1_7B_INST_Q4',
|
|
name: 'Qwen3 1.7B',
|
|
tools: true,
|
|
compactTools: true,
|
|
vision: false,
|
|
minRamGb: 8,
|
|
approxDownloadGb: 1.2,
|
|
ctxSize: 16384,
|
|
},
|
|
{
|
|
id: 'qwen3-4b',
|
|
constant: 'QWEN3_4B_INST_Q4_K_M',
|
|
name: 'Qwen3 4B',
|
|
tools: true,
|
|
vision: false,
|
|
minRamGb: 16,
|
|
approxDownloadGb: 2.6,
|
|
ctxSize: 8192,
|
|
},
|
|
{
|
|
id: 'llama-tool-1b',
|
|
constant: 'LLAMA_TOOL_CALLING_1B_INST_Q4_K',
|
|
name: 'Llama tool-calling 1B',
|
|
tools: true,
|
|
vision: false,
|
|
minRamGb: 6,
|
|
approxDownloadGb: 0.8,
|
|
ctxSize: 4096,
|
|
},
|
|
];
|
|
|
|
const byId = new Map(CATALOG.map((e) => [e.id, e]));
|
|
const byConstant = new Map(CATALOG.filter((e) => e.constant).map((e) => [e.constant, e]));
|
|
|
|
const FALLBACK_LLM_IDS = [
|
|
'qwen3.5-4b',
|
|
'qwen3.5-9b',
|
|
'gemma4-4b',
|
|
'qwen3-8b',
|
|
'gemma4-2b',
|
|
'qwen3vl-2b',
|
|
'qwen3.5-2b',
|
|
'qwen3.5-0.8b',
|
|
];
|
|
|
|
function sizeTag(e) {
|
|
const n = Number(e && e.approxDownloadGb);
|
|
if (!Number.isFinite(n) || n <= 0) return '';
|
|
const g = n >= 10 ? String(Math.round(n)) : String(n).replace(/\.0$/, '');
|
|
return ' · ~' + g + ' GB';
|
|
}
|
|
|
|
function catalogLabel(e) {
|
|
if (!e) return '';
|
|
return (e.name || e.id || '') + sizeTag(e);
|
|
}
|
|
|
|
function listCatalog() {
|
|
return CATALOG.map((e) => Object.assign({}, e, { label: catalogLabel(e) }));
|
|
}
|
|
|
|
function findCatalogEntry(idOrConstant) {
|
|
if (!idOrConstant) return null;
|
|
return byId.get(idOrConstant) || byConstant.get(idOrConstant) || null;
|
|
}
|
|
|
|
function resolveModelConstant(idOrConstant) {
|
|
const e = findCatalogEntry(idOrConstant);
|
|
return e ? e.constant : idOrConstant;
|
|
}
|
|
|
|
function suggestProfile(hw) {
|
|
const ramGb = Number(hw && hw.totalRamBytes) / 1e9 || 0;
|
|
const vramGb = Number(hw && hw.vramBytes) / 1e9 || 0;
|
|
const budget = Math.max(ramGb, vramGb * 1.5);
|
|
const toolModels = CATALOG.filter((e) => e.tools).sort((a, b) => a.minRamGb - b.minRamGb);
|
|
let pick = toolModels[0] || CATALOG[0];
|
|
for (const e of toolModels) {
|
|
if (e.minRamGb + 2 <= budget) pick = e;
|
|
}
|
|
return pick;
|
|
}
|
|
|
|
function isVisionModel(idOrConstant) {
|
|
const e = findCatalogEntry(idOrConstant);
|
|
if (e) return e.vision === true;
|
|
return /_MULTIMODAL_/.test(String(idOrConstant || ''));
|
|
}
|
|
|
|
const MMPROJ_QUANTS = ['Q8_0', 'F16', 'BF16'];
|
|
|
|
function mmprojConstant(idOrConstant) {
|
|
if (idOrConstant && typeof idOrConstant === 'object') {
|
|
if (idOrConstant.mmproj) return idOrConstant.mmproj;
|
|
idOrConstant = idOrConstant.id || idOrConstant.constant;
|
|
}
|
|
const e = findCatalogEntry(idOrConstant);
|
|
if (e && e.mmproj) return e.mmproj;
|
|
const constant = (e && e.constant) || String(idOrConstant || '');
|
|
if (constant.indexOf('_MULTIMODAL_') < 0) return null;
|
|
return 'MMPROJ_' + constant.replace(/_Q[0-9A-Z_]+$/, '') + '_Q8_0';
|
|
}
|
|
|
|
function mmprojFamily(name) {
|
|
return String(name || '').replace(/_(Q8_0|F16|BF16|Q4_K)$/, '');
|
|
}
|
|
|
|
function mmprojCandidates(idOrConstant) {
|
|
const preferred = mmprojConstant(idOrConstant);
|
|
if (!preferred) return [];
|
|
const family = mmprojFamily(preferred);
|
|
const out = [];
|
|
function add(n) {
|
|
if (n && out.indexOf(n) < 0) out.push(n);
|
|
}
|
|
add(preferred);
|
|
for (let i = 0; i < MMPROJ_QUANTS.length; i++) add(family + '_' + MMPROJ_QUANTS[i]);
|
|
return out;
|
|
}
|
|
|
|
function toolDialectFor(idOrConstant) {
|
|
const e = findCatalogEntry(idOrConstant);
|
|
const id = (e && e.id) || String(idOrConstant || '').toLowerCase();
|
|
if (id.includes('qwen3.5') || id.includes('qwen3.6') || id.includes('qwen35')) return 'qwen35';
|
|
if (id.includes('gemma4')) return 'gemma4';
|
|
if (id.includes('gpt-oss') || id.includes('harmony')) return 'harmony';
|
|
if (id.includes('llama')) return 'pythonic';
|
|
return 'hermes';
|
|
}
|
|
|
|
function isCompactToolModel(idOrConstant) {
|
|
const e = findCatalogEntry(idOrConstant);
|
|
if (e && e.compactTools) return true;
|
|
const id = String((e && e.id) || idOrConstant || '').toLowerCase();
|
|
return /qwen3\.5-0\.8b|qwen3\.5-2b|qwen3-1\.7b/.test(id);
|
|
}
|
|
|
|
const COMPACT_TOOL_ALLOW = [
|
|
'read_file',
|
|
'write_file',
|
|
'search_replace',
|
|
'list_dir',
|
|
'grep',
|
|
'run_terminal_cmd',
|
|
'web_search',
|
|
'google_search',
|
|
'fetch_page',
|
|
'web_fetch',
|
|
'wiki_search',
|
|
'hn_search',
|
|
'code_search',
|
|
'jarvis_status',
|
|
'cu_status',
|
|
'cu_observe',
|
|
'cu_find',
|
|
'cu_click',
|
|
'cu_type',
|
|
'cu_key',
|
|
'fs_search',
|
|
'fs_read',
|
|
'fs_write',
|
|
'app_list',
|
|
'memory_recall',
|
|
'memory_remember',
|
|
'memory_search',
|
|
'memory_get',
|
|
'memory_write',
|
|
'todo_write',
|
|
'task',
|
|
'update_goal',
|
|
'capability_status',
|
|
'qvac_runtime_state',
|
|
'qvac_system_resources',
|
|
];
|
|
|
|
function compactToolAllowlist() {
|
|
return new Set(COMPACT_TOOL_ALLOW);
|
|
}
|
|
|
|
function filterToolsForModel(defs, idOrConstant) {
|
|
const list = Array.isArray(defs) ? defs : [];
|
|
if (!isCompactToolModel(idOrConstant)) return list;
|
|
const allow = compactToolAllowlist();
|
|
const filtered = list.filter((t) => t && allow.has(t.name));
|
|
return filtered.length ? filtered : list;
|
|
}
|
|
|
|
function catalogId(idOrConstant) {
|
|
const e = findCatalogEntry(idOrConstant);
|
|
return String((e && e.id) || idOrConstant || '').toLowerCase();
|
|
}
|
|
|
|
function isQwenReasoningModel(idOrConstant) {
|
|
const id = catalogId(idOrConstant);
|
|
return /qwen3(?:\.\d+)?/.test(id) || /qwen3[._-]/.test(String(idOrConstant || '').toLowerCase());
|
|
}
|
|
|
|
// Qwen3.5's chat template leaves thinking off unless the reasoning channel is
|
|
// enabled. Cap it so HUD thinking still streams, then the addon force-closes
|
|
// <think> and the model can emit the tool XML / spoken reply.
|
|
function reasoningBudgetForModel(idOrConstant) {
|
|
if (!isQwenReasoningModel(idOrConstant)) return null;
|
|
const id = catalogId(idOrConstant);
|
|
if (/0\.8b|0\.6b|600m/.test(id)) return 128;
|
|
if (/1\.7b|(?:^|-)2b/.test(id)) return 192;
|
|
if (/(?:^|-)4b/.test(id)) return 320;
|
|
if (/(?:^|-)[89]b/.test(id)) return 448;
|
|
return 320;
|
|
}
|
|
|
|
// Slightly below Qwen thinking's 0.6 so compact models still follow the
|
|
// tool XML, but high enough to avoid the greedy repeat loops of temp 0.3.
|
|
function compactGenerationParams(idOrConstant) {
|
|
if (!isCompactToolModel(idOrConstant)) return {};
|
|
return { temp: 0.55 };
|
|
}
|
|
|
|
function generationParamsForModel(idOrConstant, extra) {
|
|
const base = Object.assign(
|
|
{
|
|
repeat_penalty: 1.15,
|
|
frequency_penalty: 0.15,
|
|
},
|
|
compactGenerationParams(idOrConstant)
|
|
);
|
|
const budget = reasoningBudgetForModel(idOrConstant);
|
|
if (budget != null) {
|
|
base.reasoning_budget = budget;
|
|
base.predict = Math.min(768, Math.max(384, budget + 288));
|
|
} else {
|
|
base.predict = isCompactToolModel(idOrConstant) ? 384 : 640;
|
|
}
|
|
return Object.assign({}, base, extra || {});
|
|
}
|
|
|
|
module.exports = {
|
|
CATALOG,
|
|
FALLBACK_LLM_IDS,
|
|
listCatalog,
|
|
findCatalogEntry,
|
|
resolveModelConstant,
|
|
suggestProfile,
|
|
isVisionModel,
|
|
mmprojConstant,
|
|
mmprojCandidates,
|
|
toolDialectFor,
|
|
isCompactToolModel,
|
|
compactToolAllowlist,
|
|
filterToolsForModel,
|
|
isQwenReasoningModel,
|
|
reasoningBudgetForModel,
|
|
compactGenerationParams,
|
|
generationParamsForModel,
|
|
catalogLabel,
|
|
};
|