Actually vendor harness

This commit is contained in:
2026-09-11 13:41:22 -04:00
parent 1acd371668
commit 04e013b090
52 changed files with 8550 additions and 1 deletions
+290
View File
@@ -0,0 +1,290 @@
/**
* 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,
vision: true,
mmproj: 'MMPROJ_QWEN3_5_0_8B_MULTIMODAL_Q8_0',
minRamGb: 4,
approxDownloadGb: 0.7,
ctxSize: 8192,
},
{
id: 'qwen3.5-2b',
constant: 'QWEN3_5_2B_MULTIMODAL_Q4_K_M',
name: 'Qwen3.5 2B',
tools: true,
vision: true,
mmproj: 'MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0',
minRamGb: 6,
approxDownloadGb: 1.6,
ctxSize: 8192,
},
{
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,
vision: false,
minRamGb: 8,
approxDownloadGb: 1.2,
ctxSize: 8192,
},
{
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';
}
module.exports = {
CATALOG,
FALLBACK_LLM_IDS,
listCatalog,
findCatalogEntry,
resolveModelConstant,
suggestProfile,
isVisionModel,
mmprojConstant,
mmprojCandidates,
toolDialectFor,
catalogLabel,
};
+176
View File
@@ -0,0 +1,176 @@
/**
* Device selection: Metal (macOS), Vulkan (Linux/Windows NVIDIA+AMD), CPU fallback.
* CUDA/ROCm LLM backends are not shipped by QVAC.
* GPU is always attempted unless the caller explicitly requests CPU.
*/
function metricValue(metric, fallback) {
if (metric == null) return fallback;
if (typeof metric !== 'object') return metric;
if (metric.status === 'supported') return metric.value;
return fallback;
}
function flattenDrivers(drivers) {
const out = {};
if (!drivers || typeof drivers !== 'object') return out;
for (const key of Object.keys(drivers)) {
const v = drivers[key];
if (v === true) out[key] = true;
else if (v && v.status === 'supported' && v.value) out[key] = true;
}
return out;
}
function normalizeResources(raw) {
if (!raw || typeof raw !== 'object') {
return { totalRamBytes: 0, vramBytes: 0, gpus: [], drivers: {} };
}
if (Array.isArray(raw.gpus) || raw.vramBytes != null || raw.vram != null) {
const gpus = raw.gpus || [];
const drivers = Object.keys(raw.drivers || {}).length ? flattenDrivers(raw.drivers) : {};
if (!Object.keys(drivers).length) {
for (const g of gpus) Object.assign(drivers, flattenDrivers(g && g.drivers));
}
return {
totalRamBytes: Number(raw.totalRamBytes) || 0,
vramBytes: vramBytes(raw),
gpus,
drivers,
capabilities: raw.capabilities,
gpu: raw.gpu,
};
}
const caps = raw.capabilities || {};
const gpuList = metricValue(caps.gpus, []) || [];
const gpus = gpuList.map((g) => ({
id: g.id,
name: metricValue(g.name, null),
type: metricValue(g.type, null),
memory: metricValue(g.memoryTotalBytes, 0),
drivers: flattenDrivers(g.drivers),
}));
const drivers = {};
for (const g of gpus) Object.assign(drivers, g.drivers || {});
let maxVram = 0;
for (const g of gpus) {
const m = Number(g.memory) || 0;
if (m > maxVram) maxVram = m;
}
return {
totalRamBytes: Number(metricValue(caps.memory && caps.memory.totalBytes, 0)) || 0,
vramBytes: maxVram,
gpus,
drivers,
capabilities: { gpu: gpus.length > 0, vulkan: !!drivers.vulkan, metal: !!drivers.metal },
};
}
function pickDevice(requested, resources) {
const want = String(requested || 'auto').toLowerCase();
if (want === 'cpu') return { device: 'cpu', gpu_layers: 0 };
return { device: 'gpu', gpu_layers: 99 };
}
function hasGpu(resources) {
const res = resources && resources.capabilities && !Array.isArray(resources.gpus)
? normalizeResources(resources)
: resources;
if (!res) return false;
const drivers = res.drivers || {};
if (drivers.metal || drivers.vulkan || drivers.cuda || drivers.opencl) return true;
const gpus = res.gpus || [];
if (gpus.length > 0) return true;
const cap = res.capabilities || {};
if (cap.gpu || cap.vulkan || cap.metal) return true;
if (res.gpu) return true;
return false;
}
function backendLabel(resources) {
const res = resources && resources.capabilities && !Array.isArray(resources.gpus)
? normalizeResources(resources)
: resources;
const gpus = (res && res.gpus) || [];
const dedicated = gpus.find((g) => g && (g.type === 2 || String(g.type).toLowerCase() === 'dedicated'));
const gpu = dedicated || gpus[0] || null;
const gpuName =
(gpu && gpu.name) ||
(res && res.gpu && (res.gpu.name || res.gpu.deviceName)) ||
null;
const vram = (gpu && gpu.memory) || (res && (res.vram || res.vramBytes)) || null;
const drivers = (res && res.drivers) || (gpu && gpu.drivers) || {};
if (process.platform === 'darwin' || drivers.metal) {
return { backend: 'metal', backendId: 1, deviceName: gpuName, vram };
}
if (drivers.vulkan || hasGpu(res)) {
return { backend: 'vulkan', backendId: 3, deviceName: gpuName, vram };
}
return { backend: 'cpu', backendId: 0, deviceName: gpuName, vram };
}
function summarizeGpuProbe(info) {
const res = normalizeResources(info);
return {
gpus: res.gpus || [],
drivers: res.drivers || {},
length: (res.gpus && res.gpus.length) || 0,
};
}
/**
* Normalize GPU memory reports to bytes. bare-gpu-info may yield bytes, MiB, or GiB.
*/
function vramBytes(resources) {
if (!resources) return 0;
const raw =
resources.vramBytes != null
? resources.vramBytes
: resources.vram != null
? resources.vram
: resources.gpus && resources.gpus[0] && resources.gpus[0].memory;
const n = Number(raw);
if (!Number.isFinite(n) || n <= 0) return 0;
if (n < 1024) return Math.round(n * 1024 * 1024 * 1024);
if (n < 1024 * 1024) return Math.round(n * 1024 * 1024);
return Math.round(n);
}
/**
* Keep KV-cache context from dominating VRAM. Weights still use gpu_layers.
*/
function capCtxSize(want, resources, onGpu) {
const requested = Math.max(512, Number(want) || 8192);
if (!onGpu) return Math.min(requested, 8192);
const gb = vramBytes(resources) / 1e9;
let max = requested;
if (gb > 0 && gb < 6) max = 4096;
else if (gb > 0 && gb < 10) max = 8192;
else if (gb > 0 && gb < 16) max = 16384;
return Math.min(requested, max);
}
function mmprojOnGpu(opts, resources, onGpu) {
if (!onGpu) return false;
if (opts && opts.mmprojUseGpu === false) return false;
return true;
}
function gpuLayers(opts, dev) {
const raw = opts && opts.gpu_layers != null ? opts.gpu_layers : dev && dev.gpu_layers;
const n = Number(raw);
if (Number.isFinite(n) && n >= 0) return n;
return dev && dev.device === 'gpu' ? 99 : 0;
}
module.exports = {
pickDevice,
hasGpu,
backendLabel,
summarizeGpuProbe,
vramBytes,
capCtxSize,
mmprojOnGpu,
normalizeResources,
gpuLayers,
};
+38
View File
@@ -0,0 +1,38 @@
/**
* Normalize QVAC completion stream events for cap-chunk (keep under NMH ~1 MB).
*/
const MAX_DELTA = 64 * 1024;
function clip(s) {
const t = s == null ? '' : String(s);
if (t.length <= MAX_DELTA) return t;
return t.slice(0, MAX_DELTA);
}
function normalizeCompletionEvent(ev) {
if (!ev || !ev.type) return null;
if (ev.type === 'contentDelta') {
return { type: 'contentDelta', delta: clip(ev.delta || ev.text || ev.content || '') };
}
if (ev.type === 'thinkingDelta') {
return { type: 'thinkingDelta', delta: clip(ev.delta || ev.text || ev.content || '') };
}
if (ev.type === 'toolCall') {
const call = ev.call || ev.toolCall || ev;
return {
type: 'toolCall',
call: {
id: call.id,
name: call.name,
arguments: call.arguments != null ? call.arguments : call.args,
},
};
}
if (ev.type === 'rawDelta') {
return { type: 'rawDelta', delta: clip(ev.delta) };
}
return { type: ev.type, delta: ev.delta != null ? clip(ev.delta) : undefined, call: ev.call };
}
module.exports = { normalizeCompletionEvent, MAX_DELTA };
+51
View File
@@ -0,0 +1,51 @@
/**
* Public HTTP(S) only — private / loopback / metadata hosts are blocked.
*/
function isBlockedHostname(hostname) {
if (!hostname) return true;
const h = String(hostname).toLowerCase().replace(/^\[|\]$/g, '');
if (
h === 'localhost' ||
h.endsWith('.localhost') ||
h === '0.0.0.0' ||
h === '::' ||
h === '::1' ||
h === 'metadata.google.internal' ||
h.endsWith('.internal')
) {
return true;
}
const v4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
if (v4) {
const a = Number(v4[1]);
const b = Number(v4[2]);
if (a === 0 || a === 10 || a === 127) return true;
if (a === 169 && b === 254) return true;
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 100 && b >= 64 && b <= 127) return true;
}
if (h.includes(':')) {
if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) return true;
}
return false;
}
function assertPublicHttpUrl(raw) {
let url;
try {
url = new URL(String(raw));
} catch (_) {
throw new Error('invalid URL');
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
throw new Error('only http(s) URLs are allowed');
}
if (isBlockedHostname(url.hostname)) {
throw new Error('private, loopback, and metadata hosts are blocked');
}
return url;
}
module.exports = { isBlockedHostname, assertPublicHttpUrl };
+110
View File
@@ -0,0 +1,110 @@
/**
* Local storage for the standalone harness (sessions, models, vision stills).
* Override with AGENT_HARNESS_HOME.
*/
const path = require('path');
const fs = require('fs');
const os = require('os');
function getHome() {
return (
process.env.AGENT_HARNESS_HOME ||
path.join(os.homedir(), '.agent-harness')
);
}
function getStorageRoot() {
return getHome();
}
function getQvacRoot() {
return path.join(getStorageRoot(), 'qvac');
}
function getAgentRoot() {
return path.join(getStorageRoot(), 'agent');
}
function ensureDir(dir) {
try {
fs.mkdirSync(dir, { recursive: true });
} catch (_) {}
return dir;
}
function ensureQvacRoot() {
return ensureDir(getQvacRoot());
}
function ensureAgentRoot() {
return ensureDir(getAgentRoot());
}
function sanitizeId(id) {
return String(id)
.replace(/[^a-zA-Z0-9._-]/g, '_')
.slice(0, 128);
}
function originHash(origin) {
const s = String(origin || 'local');
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
return Math.abs(h).toString(16);
}
function getAgentOriginRoot(origin) {
return path.join(ensureAgentRoot(), 'workspaces', sanitizeId(originHash(origin) || 'default'));
}
function isPathInside(root, candidate) {
const absRoot = path.resolve(root);
const abs = path.resolve(candidate);
const rel = path.relative(absRoot, abs);
return !(rel.startsWith('..') || path.isAbsolute(rel));
}
function resolveUnderRoot(root, userPath) {
if (!userPath || typeof userPath !== 'string') throw new Error('path is required');
const absRoot = path.resolve(ensureDir(root));
const resolved = path.isAbsolute(userPath)
? path.resolve(userPath)
: path.resolve(absRoot, userPath.replace(/^\/+/, ''));
const rel = path.relative(absRoot, resolved);
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error('path escapes allowlisted root');
}
return resolved;
}
function resolveUnderAnyRoot(roots, userPath, fallbackRoot) {
if (!userPath || typeof userPath !== 'string') throw new Error('path is required');
const list = (roots || []).filter(Boolean).map((r) => path.resolve(r));
if (fallbackRoot) list.unshift(path.resolve(fallbackRoot));
if (!list.length) throw new Error('no allowlisted roots');
if (path.isAbsolute(userPath)) {
const resolved = path.resolve(userPath);
for (const root of list) {
if (isPathInside(root, resolved)) return resolved;
}
throw new Error('path escapes allowlisted roots');
}
return resolveUnderRoot(list[0], userPath);
}
module.exports = {
getHome,
getStorageRoot,
getQvacRoot,
getAgentRoot,
getAgentOriginRoot,
originHash,
ensureDir,
ensureQvacRoot,
ensureAgentRoot,
sanitizeId,
isPathInside,
resolveUnderRoot,
resolveUnderAnyRoot,
};
+461
View File
@@ -0,0 +1,461 @@
/**
* Direct QVAC LLM wrapper via @qvac/sdk (Node worker). No BridgeSwarm.
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const catalog = require('./catalog.js');
const device = require('./device.js');
const events = require('./events.js');
const paths = require('./paths.js');
let sdk = null;
let initError = null;
let initPromise = null;
let holdCount = 0;
let loaded = emptyLoaded();
const activeRequests = new Map();
function emptyLoaded() {
return {
modelId: null,
friendlyId: null,
constant: null,
tools: false,
vision: false,
device: 'cpu',
backend: null,
backendId: null,
deviceName: null,
vram: null,
ctxSize: null,
requestId: null,
};
}
function log(msg) {
try {
process.stderr.write('[agent-harness] ' + msg + '\n');
} catch (_) {}
}
function flattenError(err) {
if (!err) return 'unknown error';
if (typeof err === 'string') return err;
const parts = [];
let cur = err;
for (let i = 0; i < 6 && cur; i++) {
if (cur.message) parts.push(String(cur.message));
else parts.push(String(cur));
cur = cur.cause || cur.error;
}
return parts.join(' | ') || String(err);
}
function lookupSrc(mod, name) {
if (!name) return null;
if (mod[name] != null) return mod[name];
if (mod.models && mod.models[name] != null) return mod.models[name];
if (mod.default && mod.default[name] != null) return mod.default[name];
return null;
}
async function ensureInit() {
if (sdk) return sdk;
if (initPromise) return initPromise;
initPromise = (async () => {
if (!process.env.QVAC_CONFIG_PATH) {
const cfg = path.join(__dirname, '..', 'qvac.config.json');
try {
if (fs.existsSync(cfg)) process.env.QVAC_CONFIG_PATH = cfg;
} catch (_) {}
}
paths.ensureQvacRoot();
let mod;
try {
mod = await import('@qvac/sdk');
} catch (err) {
initError = flattenError(err);
log('sdk import failed: ' + initError);
throw new Error(initError);
}
if (typeof mod.loadModel !== 'function' && mod.default && typeof mod.default.loadModel === 'function') {
mod = mod.default;
}
if (typeof mod.loadModel !== 'function' || typeof mod.completion !== 'function') {
initError = '@qvac/sdk missing loadModel/completion';
throw new Error(initError);
}
sdk = mod;
return sdk;
})();
try {
return await initPromise;
} catch (err) {
initPromise = null;
throw err;
}
}
function probeResources() {
return device.normalizeResources({
totalRamBytes: os.totalmem(),
vramBytes: 0,
gpus: [],
drivers: {},
});
}
async function fetchSystemResources(s) {
if (!s) return null;
if (typeof s.getSystemResources === 'function') {
try {
return await s.getSystemResources();
} catch (_) {}
}
if (typeof s.getSystemInfo === 'function') {
try {
return await s.getSystemInfo();
} catch (_) {}
}
return null;
}
async function resources() {
const s = await ensureInit().catch(() => null);
const info = await fetchSystemResources(s);
if (info) return device.normalizeResources(info);
return probeResources();
}
function toTools(tools) {
if (!tools || !Array.isArray(tools)) return undefined;
return tools.map((t) => {
if (t && t.type === 'function' && t.function) {
return {
type: 'function',
name: t.function.name,
description: t.function.description,
parameters: t.function.parameters,
};
}
return t;
});
}
function decodeDataUrl(dataUrl) {
const m = String(dataUrl || '').match(/^data:([^;]+);base64,(.+)$/);
if (!m) return null;
return { mime: m[1], buf: Buffer.from(m[2], 'base64') };
}
function extForMime(mime) {
if (/png/i.test(mime)) return '.png';
if (/webp/i.test(mime)) return '.webp';
if (/gif/i.test(mime)) return '.gif';
return '.jpg';
}
function prepareVisionHistory(history) {
const dir = paths.ensureDir(path.join(paths.ensureQvacRoot(), 'vision'));
const list = Array.isArray(history) ? history : [];
return list.map((msg) => {
if (!msg || !Array.isArray(msg.images) || !msg.images.length) return msg;
const attachments = [];
for (let i = 0; i < Math.min(4, msg.images.length); i++) {
const img = msg.images[i] || {};
let buf = null;
let mime = img.mime || 'image/jpeg';
if (img.dataUrl) {
const d = decodeDataUrl(img.dataUrl);
if (d) {
buf = d.buf;
mime = d.mime;
}
} else if (img.dataBase64) {
buf = Buffer.from(img.dataBase64, 'base64');
} else if (img.path && fs.existsSync(img.path)) {
attachments.push({ path: img.path });
continue;
}
if (!buf) continue;
const file = path.join(dir, 'img_' + Date.now() + '_' + i + extForMime(mime));
fs.writeFileSync(file, buf);
attachments.push({ path: file });
}
const copy = Object.assign({}, msg);
delete copy.images;
if (attachments.length) copy.attachments = (copy.attachments || []).concat(attachments);
return copy;
});
}
async function resolveSrc(s, name) {
const constant = catalog.resolveModelConstant(name);
const fromSdk = lookupSrc(s, constant) || lookupSrc(s, name);
if (fromSdk) return fromSdk;
if (typeof s.lookupModelSrc === 'function') {
try {
const src = await s.lookupModelSrc(constant);
if (src) return src;
} catch (_) {}
}
throw new Error(
'QVAC has no constant for ' +
String(name) +
' (' +
constant +
'). Only catalog GGUFs in @qvac/sdk load.'
);
}
async function resolveMmproj(s, entry) {
const names = catalog.mmprojCandidates(entry);
for (const n of names) {
const src = lookupSrc(s, n);
if (src) return src;
if (typeof s.lookupModelSrc === 'function') {
try {
const found = await s.lookupModelSrc(n);
if (found) return found;
} catch (_) {}
}
}
return null;
}
async function load(opts, onProgress) {
const s = await ensureInit();
opts = opts || {};
onProgress = onProgress || opts.onProgress;
if (loaded.modelId) {
await unload().catch(() => {});
}
const entry = catalog.findCatalogEntry(opts.model || opts.modelSrc || opts.friendlyId);
const modelSrc = await resolveSrc(s, opts.modelSrc || (entry && entry.constant) || opts.model);
const res = await resources();
let dev = device.pickDevice(opts.device || 'auto', res);
const toolsOn = opts.tools !== false && (!entry || entry.tools !== false);
const rawCtx = opts.ctxSize || opts.ctx_size || (entry && entry.ctxSize) || 8192;
const ctxSize = device.capCtxSize(rawCtx, res, dev.device === 'gpu');
const mmprojGpu = device.mmprojOnGpu(opts, res, dev.device === 'gpu');
const wantVision =
opts.vision !== false && (entry ? entry.vision === true : catalog.isVisionModel(opts.model || opts.modelSrc));
const mmprojSrc = wantVision ? await resolveMmproj(s, entry) : null;
const modelConfig = Object.assign(
{
device: dev.device,
gpu_layers: device.gpuLayers(opts, dev),
ctx_size: Number(ctxSize) || 8192,
tools: !!toolsOn,
'mmproj-use-gpu': !!mmprojGpu,
},
mmprojSrc ? { projectionModelSrc: mmprojSrc } : {}
);
const loadOpts = {
modelSrc,
modelType: 'llm',
modelConfig,
};
if (typeof onProgress === 'function') {
loadOpts.onProgress = (p) => {
try {
onProgress({
percent: p.percentage != null ? p.percentage : p.percent,
downloaded: p.downloaded,
total: p.total,
});
} catch (_) {}
};
}
log(
'load ' +
((entry && entry.id) || opts.model || 'model') +
' device=' +
modelConfig.device +
' ngl=' +
modelConfig.gpu_layers +
' ctx=' +
modelConfig.ctx_size +
' vision=' +
!!mmprojSrc
);
let modelId;
try {
modelId = await s.loadModel(loadOpts);
} catch (err) {
const msg = flattenError(err);
log('load failed: ' + msg);
if (dev.device === 'gpu' && String(opts.device || 'auto').toLowerCase() !== 'cpu') {
log('retrying load on cpu');
modelConfig.device = 'cpu';
modelConfig.gpu_layers = 0;
modelConfig['mmproj-use-gpu'] = false;
try {
modelId = await s.loadModel(loadOpts);
dev = { device: 'cpu', gpu_layers: 0, fallback: 'cpu' };
} catch (err2) {
throw new Error(flattenError(err2));
}
} else {
throw new Error(msg);
}
}
const bl = device.backendLabel(res);
loaded = {
modelId,
friendlyId: (entry && entry.id) || opts.model || null,
constant: (entry && entry.constant) || catalog.resolveModelConstant(opts.model || opts.modelSrc),
tools: !!toolsOn,
vision: !!mmprojSrc,
device: dev.device,
backend: bl.backend,
backendId: bl.backendId,
deviceName: bl.deviceName,
vram: res && (res.vram || res.vramBytes),
ctxSize: modelConfig.ctx_size,
requestId: null,
};
return Object.assign({}, loaded);
}
async function unload() {
if (!loaded.modelId || !sdk) {
loaded = emptyLoaded();
return;
}
const id = loaded.modelId;
loaded = emptyLoaded();
try {
if (typeof sdk.unloadModel === 'function') await sdk.unloadModel({ modelId: id });
} catch (err) {
log('unload: ' + flattenError(err));
}
}
async function complete(opts, onEvent) {
const s = await ensureInit();
if (!loaded.modelId && !(opts && opts.modelId)) throw new Error('no model loaded');
const rawHistory = (opts && opts.history) || (opts && opts.messages) || [];
const history = prepareVisionHistory(rawHistory);
const tools = toTools(opts && opts.tools);
const dialect = (opts && opts.toolDialect) || catalog.toolDialectFor(loaded.friendlyId || loaded.constant);
const params = {
modelId: (opts && opts.modelId) || loaded.modelId,
history,
stream: opts && opts.stream === false ? false : true,
captureThinking: true,
};
if (tools && tools.length) {
params.tools = tools;
params.toolDialect = dialect;
}
if (opts && opts.generationParams) params.generationParams = opts.generationParams;
const run = s.completion(params);
const requestId = run.requestId || (opts && opts.requestId) || null;
loaded.requestId = requestId;
if (requestId) activeRequests.set(requestId, run);
let text = '';
let thinking = '';
const toolCalls = [];
try {
if (run.events && typeof run.events[Symbol.asyncIterator] === 'function') {
for await (const ev of run.events) {
const n = events.normalizeCompletionEvent(ev);
if (!n) continue;
if (n.type === 'contentDelta') {
text += n.delta;
if (onEvent) onEvent(n);
} else if (n.type === 'thinkingDelta') {
thinking += n.delta;
if (onEvent) onEvent(n);
} else if (n.type === 'toolCall') {
toolCalls.push(n.call);
if (onEvent) onEvent(n);
} else if (onEvent) {
onEvent(n);
}
}
} else if (run.tokenStream) {
for await (const token of run.tokenStream) {
text += token;
if (onEvent) onEvent({ type: 'contentDelta', delta: token });
}
if (run.toolCallStream) {
for await (const evt of run.toolCallStream) {
const call = evt.call || evt;
toolCalls.push(call);
if (onEvent) onEvent({ type: 'toolCall', call });
}
}
}
let stats = null;
try {
if (run.final) {
const fin = await run.final;
if (fin) {
if (fin.contentText && !text) text = fin.contentText;
if (fin.thinkingText && !thinking) thinking = fin.thinkingText;
if (Array.isArray(fin.toolCalls) && fin.toolCalls.length) {
toolCalls.length = 0;
for (const c of fin.toolCalls) toolCalls.push(c);
}
stats = fin.stats || null;
}
} else if (run.stats) {
stats = await run.stats;
}
} catch (_) {}
return { text, thinking, toolCalls, stats, requestId, stopReason: 'stop' };
} finally {
if (requestId) activeRequests.delete(requestId);
}
}
async function cancel() {
const id = loaded.requestId;
const run = id && activeRequests.get(id);
try {
if (run && typeof run.abort === 'function') run.abort();
else if (sdk && typeof sdk.abortCompletion === 'function' && id) await sdk.abortCompletion({ requestId: id });
} catch (_) {}
}
function getLoaded() {
return Object.assign({}, loaded);
}
function hold() {
holdCount += 1;
}
function release() {
holdCount = Math.max(0, holdCount - 1);
}
async function close() {
await unload().catch(() => {});
if (sdk && typeof sdk.close === 'function') {
try {
await sdk.close();
} catch (_) {}
}
sdk = null;
initPromise = null;
}
module.exports = {
ensureInit,
load,
unload,
complete,
cancel,
getLoaded,
resources,
prepareVisionHistory,
hold,
release,
close,
};