/** * 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', 'browser', 'webcam', 'jarvis_status', 'cu_status', 'cu_observe', 'cu_find', 'cu_tree', '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 // 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, };