@@ -105,6 +105,8 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Testing QVAC enable over native messaging"
|
||||||
|
python3 scripts/qvac-packed-selftest.py "$BIN"
|
||||||
echo "PASS: smoke test complete"
|
echo "PASS: smoke test complete"
|
||||||
|
|
||||||
- name: Generate checksums
|
- name: Generate checksums
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Testing QVAC enable over native messaging"
|
||||||
|
python3 scripts/qvac-packed-selftest.py "$BIN"
|
||||||
|
|
||||||
- name: List release artifacts
|
- name: List release artifacts
|
||||||
run: |
|
run: |
|
||||||
echo "=== releases/ ==="
|
echo "=== releases/ ==="
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ function renderCapabilitiesPage(state) {
|
|||||||
<div class="health-row"><span class="k">Available</span><span class="v">${q.available ? 'yes' : 'no'}</span></div>
|
<div class="health-row"><span class="k">Available</span><span class="v">${q.available ? 'yes' : 'no'}</span></div>
|
||||||
<div class="health-row"><span class="k">Device</span><span class="v">${escapeHtml(q.device || '—')}</span></div>
|
<div class="health-row"><span class="k">Device</span><span class="v">${escapeHtml(q.device || '—')}</span></div>
|
||||||
<div class="health-row"><span class="k">Backend</span><span class="v">${escapeHtml(q.backend || q.backendId || '—')}</span></div>
|
<div class="health-row"><span class="k">Backend</span><span class="v">${escapeHtml(q.backend || q.backendId || '—')}</span></div>
|
||||||
<div class="health-row"><span class="k">GPU</span><span class="v">${escapeHtml(q.deviceName || '—')}</span></div>
|
<div class="health-row"><span class="k">GPU</span><span class="v">${escapeHtml(q.deviceName || (q.vram ? String(q.vram) : '—'))}</span></div>
|
||||||
<div class="health-row"><span class="k">Model</span><span class="v">${escapeHtml(q.friendlyId || q.modelId || 'none')}</span></div>
|
<div class="health-row"><span class="k">Model</span><span class="v">${escapeHtml(q.friendlyId || q.modelId || 'none')}</span></div>
|
||||||
<div class="health-row"><span class="k">Plugins</span><span class="v">${escapeHtml((q.plugins || []).join(', ') || '—')}</span></div>
|
<div class="health-row"><span class="k">Plugins</span><span class="v">${escapeHtml((q.plugins || []).join(', ') || '—')}</span></div>
|
||||||
<div class="health-row"><span class="k">OpenAI</span><span class="v">${escapeHtml(q.openaiUrl || 'off')}</span></div>
|
<div class="health-row"><span class="k">OpenAI</span><span class="v">${escapeHtml(q.openaiUrl || 'off')}</span></div>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
} from './register-default-packs.mjs';
|
} from './register-default-packs.mjs';
|
||||||
import _messenger from './messenger.js';
|
import _messenger from './messenger.js';
|
||||||
import _host from './host.js';
|
import _host from './host.js';
|
||||||
|
import selftest from './qvac/selftest.js';
|
||||||
|
|
||||||
installConsoleToStderr();
|
installConsoleToStderr();
|
||||||
normalizeBareVersions();
|
normalizeBareVersions();
|
||||||
@@ -49,6 +50,8 @@ if (process.argv.includes('--extract-addons')) {
|
|||||||
await warmDefaultModules();
|
await warmDefaultModules();
|
||||||
await extractAddons(DEFAULT_ADDON_PACKAGES);
|
await extractAddons(DEFAULT_ADDON_PACKAGES);
|
||||||
})();
|
})();
|
||||||
|
} else if (process.argv.includes('--qvac-selftest') || process.env.BRIDGE_SWARM_QVAC_SELFTEST === '1') {
|
||||||
|
selftest.runQvacSelftest(logErr, normalizeBareVersions);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
startHost(_messenger, _host);
|
startHost(_messenger, _host);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function backendLabel(resources) {
|
|||||||
if (process.platform === 'darwin' || drivers.metal) {
|
if (process.platform === 'darwin' || drivers.metal) {
|
||||||
return { backend: 'metal', backendId: 1, deviceName: gpuName, vram };
|
return { backend: 'metal', backendId: 1, deviceName: gpuName, vram };
|
||||||
}
|
}
|
||||||
if (drivers.vulkan || process.platform === 'linux' || process.platform === 'win32') {
|
if (drivers.vulkan || hasGpu(resources)) {
|
||||||
return { backend: 'vulkan', backendId: 3, deviceName: gpuName, vram };
|
return { backend: 'vulkan', backendId: 3, deviceName: gpuName, vram };
|
||||||
}
|
}
|
||||||
return { backend: 'cpu', backendId: 0, deviceName: gpuName, vram };
|
return { backend: 'cpu', backendId: 0, deviceName: gpuName, vram };
|
||||||
|
|||||||
+106
-20
@@ -12,6 +12,7 @@ const { ensureQvacRoot } = require('../capabilities/paths.js');
|
|||||||
const catalog = require('./catalog.js');
|
const catalog = require('./catalog.js');
|
||||||
const device = require('./device.js');
|
const device = require('./device.js');
|
||||||
const events = require('./events.js');
|
const events = require('./events.js');
|
||||||
|
const llamacppHost = require('./llamacpp-host.js');
|
||||||
|
|
||||||
const SKIP = process.env.BRIDGE_SWARM_SKIP_QVAC === '1';
|
const SKIP = process.env.BRIDGE_SWARM_SKIP_QVAC === '1';
|
||||||
|
|
||||||
@@ -68,7 +69,10 @@ function setUserEnabled(on) {
|
|||||||
if (userEnabled === next) return;
|
if (userEnabled === next) return;
|
||||||
userEnabled = next;
|
userEnabled = next;
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
if (!next) initError = 'disabled';
|
if (!next) {
|
||||||
|
initError = 'disabled';
|
||||||
|
lastResources = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function disabledError() {
|
function disabledError() {
|
||||||
@@ -108,6 +112,11 @@ async function assembleSdk() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
normalizeBareVersions();
|
normalizeBareVersions();
|
||||||
|
try {
|
||||||
|
llamacppHost.patchLlamaInterface(log);
|
||||||
|
} catch (err) {
|
||||||
|
log('llm backends patch skipped: ' + ((err && err.message) || err));
|
||||||
|
}
|
||||||
ensureQvacRoot();
|
ensureQvacRoot();
|
||||||
if (!process.env.QVAC_CONFIG_PATH) {
|
if (!process.env.QVAC_CONFIG_PATH) {
|
||||||
const cfg = path.join(__dirname, '..', 'qvac.config.json');
|
const cfg = path.join(__dirname, '..', 'qvac.config.json');
|
||||||
@@ -120,14 +129,24 @@ async function assembleSdk() {
|
|||||||
await import('@qvac/fabric');
|
await import('@qvac/fabric');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
const infMain = await tryImport('@qvac/inference/plugins');
|
let infMain;
|
||||||
|
try {
|
||||||
|
infMain = await import('@qvac/inference/plugins');
|
||||||
|
} catch (err) {
|
||||||
|
infMain = { __error: err };
|
||||||
|
}
|
||||||
if (infMain.__error) {
|
if (infMain.__error) {
|
||||||
initError = infMain.__error.message || String(infMain.__error);
|
initError = infMain.__error.message || String(infMain.__error);
|
||||||
log('inference plugins unavailable: ' + initError);
|
log('inference plugins unavailable: ' + initError);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const llmMod = await tryImport('@qvac/inference/llamacpp-completion/plugin');
|
let llmMod;
|
||||||
|
try {
|
||||||
|
llmMod = await import('@qvac/inference/llamacpp-completion/plugin');
|
||||||
|
} catch (err) {
|
||||||
|
llmMod = { __error: err };
|
||||||
|
}
|
||||||
if (llmMod.__error) {
|
if (llmMod.__error) {
|
||||||
initError = llmMod.__error.message || String(llmMod.__error);
|
initError = llmMod.__error.message || String(llmMod.__error);
|
||||||
log('llm plugin unavailable: ' + initError);
|
log('llm plugin unavailable: ' + initError);
|
||||||
@@ -182,9 +201,14 @@ function ensureInit() {
|
|||||||
if (initPromise) return initPromise;
|
if (initPromise) return initPromise;
|
||||||
normalizeBareVersions();
|
normalizeBareVersions();
|
||||||
initPromise = assembleSdk()
|
initPromise = assembleSdk()
|
||||||
.then((s) => {
|
.then(async (s) => {
|
||||||
sdk = s;
|
sdk = s;
|
||||||
if (!s) initPromise = null;
|
if (!s) initPromise = null;
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
await probeHardware(s);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -195,6 +219,8 @@ function ensureInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function publicStatus() {
|
function publicStatus() {
|
||||||
|
const hw = lastResources || {};
|
||||||
|
const modelLoaded = !!loaded.modelId;
|
||||||
return {
|
return {
|
||||||
enabled: isUserEnabled(),
|
enabled: isUserEnabled(),
|
||||||
available: isUserEnabled() && !!sdk && !skipped(),
|
available: isUserEnabled() && !!sdk && !skipped(),
|
||||||
@@ -206,11 +232,11 @@ function publicStatus() {
|
|||||||
constant: loaded.constant,
|
constant: loaded.constant,
|
||||||
modelType: loaded.modelType,
|
modelType: loaded.modelType,
|
||||||
tools: loaded.tools,
|
tools: loaded.tools,
|
||||||
device: loaded.device,
|
device: modelLoaded ? loaded.device : hw.deviceDefault || loaded.device,
|
||||||
backend: loaded.backend,
|
backend: loaded.backend || hw.backend || null,
|
||||||
backendId: loaded.backendId,
|
backendId: loaded.backendId != null ? loaded.backendId : hw.backendId,
|
||||||
deviceName: loaded.deviceName,
|
deviceName: loaded.deviceName || hw.deviceName || null,
|
||||||
vram: loaded.vram,
|
vram: loaded.vram || hw.vram || null,
|
||||||
ctxSize: loaded.ctxSize,
|
ctxSize: loaded.ctxSize,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
arch: process.arch,
|
arch: process.arch,
|
||||||
@@ -230,8 +256,13 @@ const pickDevice = device.pickDevice;
|
|||||||
const backendLabel = device.backendLabel;
|
const backendLabel = device.backendLabel;
|
||||||
|
|
||||||
async function probeGpuInfo() {
|
async function probeGpuInfo() {
|
||||||
const gpuMod = await tryImport('bare-gpu-info');
|
let gpuMod;
|
||||||
if (gpuMod.__error) return null;
|
try {
|
||||||
|
gpuMod = await import('bare-gpu-info');
|
||||||
|
} catch (err) {
|
||||||
|
log('bare-gpu-info unavailable: ' + ((err && err.message) || err));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const GPUInfo = gpuMod.default || gpuMod.GPUInfo || gpuMod;
|
const GPUInfo = gpuMod.default || gpuMod.GPUInfo || gpuMod;
|
||||||
if (typeof GPUInfo !== 'function') return null;
|
if (typeof GPUInfo !== 'function') return null;
|
||||||
@@ -243,19 +274,18 @@ async function probeGpuInfo() {
|
|||||||
if (typeof info.destroy === 'function') info.destroy();
|
if (typeof info.destroy === 'function') info.destroy();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
return { gpus, drivers, length };
|
return { gpus, drivers, length };
|
||||||
} catch (_) {
|
} catch (err) {
|
||||||
|
log('gpu probe failed: ' + ((err && err.message) || err));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resources(opts) {
|
async function probeHardware(s) {
|
||||||
if (!isUserEnabled()) throw disabledError();
|
|
||||||
const s = await ensureInit();
|
|
||||||
const ram = totalmem();
|
const ram = totalmem();
|
||||||
let qvacRes = null;
|
let qvacRes = null;
|
||||||
if (s && typeof s.getSystemResources === 'function') {
|
if (s && typeof s.getSystemResources === 'function') {
|
||||||
try {
|
try {
|
||||||
qvacRes = await s.getSystemResources({ sample: !!(opts && opts.sample) });
|
qvacRes = await s.getSystemResources({ sample: false });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
qvacRes = { error: err.message };
|
qvacRes = { error: err.message };
|
||||||
}
|
}
|
||||||
@@ -265,6 +295,7 @@ async function resources(opts) {
|
|||||||
if (gpuProbe && gpuProbe.gpus && gpuProbe.gpus[0] && gpuProbe.gpus[0].memory) {
|
if (gpuProbe && gpuProbe.gpus && gpuProbe.gpus[0] && gpuProbe.gpus[0].memory) {
|
||||||
merged.vram = gpuProbe.gpus[0].memory;
|
merged.vram = gpuProbe.gpus[0].memory;
|
||||||
}
|
}
|
||||||
|
const bl = backendLabel(merged);
|
||||||
lastResources = {
|
lastResources = {
|
||||||
totalRamBytes: ram,
|
totalRamBytes: ram,
|
||||||
capabilities: qvacRes && qvacRes.capabilities,
|
capabilities: qvacRes && qvacRes.capabilities,
|
||||||
@@ -274,11 +305,31 @@ async function resources(opts) {
|
|||||||
drivers: gpuProbe && gpuProbe.drivers,
|
drivers: gpuProbe && gpuProbe.drivers,
|
||||||
suggest: catalog.suggestProfile({ totalRamBytes: ram, vramBytes: merged.vram }),
|
suggest: catalog.suggestProfile({ totalRamBytes: ram, vramBytes: merged.vram }),
|
||||||
deviceDefault: pickDevice('auto', merged).device,
|
deviceDefault: pickDevice('auto', merged).device,
|
||||||
...backendLabel(merged),
|
...bl,
|
||||||
};
|
};
|
||||||
|
log(
|
||||||
|
'hardware ' +
|
||||||
|
lastResources.deviceDefault +
|
||||||
|
'/' +
|
||||||
|
(lastResources.backend || 'none') +
|
||||||
|
(lastResources.deviceName ? ' ' + lastResources.deviceName : '')
|
||||||
|
);
|
||||||
return lastResources;
|
return lastResources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resources(opts) {
|
||||||
|
if (!isUserEnabled()) throw disabledError();
|
||||||
|
const s = await ensureInit();
|
||||||
|
if (opts && opts.sample && s && typeof s.getSystemResources === 'function') {
|
||||||
|
try {
|
||||||
|
const qvacRes = await s.getSystemResources({ sample: true });
|
||||||
|
if (lastResources) lastResources.sample = qvacRes && qvacRes.sample;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
if (lastResources && !opts) return lastResources;
|
||||||
|
return probeHardware(s);
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveSrc(s, modelSrc) {
|
async function resolveSrc(s, modelSrc) {
|
||||||
if (modelSrc && typeof modelSrc === 'object') return modelSrc;
|
if (modelSrc && typeof modelSrc === 'object') return modelSrc;
|
||||||
const constant = catalog.resolveModelConstant(modelSrc);
|
const constant = catalog.resolveModelConstant(modelSrc);
|
||||||
@@ -307,7 +358,7 @@ async function load(opts, onProgress) {
|
|||||||
const entry = catalog.findCatalogEntry(opts.model || opts.modelSrc || opts.friendlyId);
|
const entry = catalog.findCatalogEntry(opts.model || opts.modelSrc || opts.friendlyId);
|
||||||
const modelSrc = await resolveSrc(s, opts.modelSrc || (entry && entry.constant) || opts.model);
|
const modelSrc = await resolveSrc(s, opts.modelSrc || (entry && entry.constant) || opts.model);
|
||||||
const res = lastResources || (await resources({ sample: false }));
|
const res = lastResources || (await resources({ sample: false }));
|
||||||
const dev = pickDevice(opts.device || 'auto', res);
|
let dev = pickDevice(opts.device || 'auto', res);
|
||||||
const tools = opts.tools !== false && (!entry || entry.tools !== false);
|
const tools = opts.tools !== false && (!entry || entry.tools !== false);
|
||||||
const ctxSize = opts.ctxSize || opts.ctx_size || (entry && entry.ctxSize) || 8192;
|
const ctxSize = opts.ctxSize || opts.ctx_size || (entry && entry.ctxSize) || 8192;
|
||||||
const modelConfig = Object.assign(
|
const modelConfig = Object.assign(
|
||||||
@@ -339,8 +390,42 @@ async function load(opts, onProgress) {
|
|||||||
}
|
}
|
||||||
if (opts.delegate) loadOpts.delegate = opts.delegate;
|
if (opts.delegate) loadOpts.delegate = opts.delegate;
|
||||||
|
|
||||||
const modelId = await s.loadModel(loadOpts);
|
log(
|
||||||
const bl = backendLabel(res);
|
'load ' +
|
||||||
|
((entry && entry.id) || opts.model || 'model') +
|
||||||
|
' device=' +
|
||||||
|
modelConfig.device +
|
||||||
|
' ngl=' +
|
||||||
|
modelConfig.gpu_layers +
|
||||||
|
' ctx=' +
|
||||||
|
modelConfig.ctx_size
|
||||||
|
);
|
||||||
|
|
||||||
|
let modelId;
|
||||||
|
try {
|
||||||
|
modelId = await s.loadModel(loadOpts);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = llamacppHost.flattenError(err);
|
||||||
|
log('load failed: ' + msg);
|
||||||
|
const requested = String(opts.device || 'auto').toLowerCase();
|
||||||
|
if (dev.device === 'gpu' && requested !== 'cpu') {
|
||||||
|
log('retrying load on cpu');
|
||||||
|
modelConfig.device = 'cpu';
|
||||||
|
modelConfig.gpu_layers = 0;
|
||||||
|
try {
|
||||||
|
modelId = await s.loadModel(loadOpts);
|
||||||
|
dev = { device: 'cpu', gpu_layers: 0, fallback: 'cpu' };
|
||||||
|
} catch (err2) {
|
||||||
|
throw new Error(llamacppHost.flattenError(err2));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bl =
|
||||||
|
dev.device === 'cpu'
|
||||||
|
? Object.assign({}, backendLabel(res), { backend: 'cpu', backendId: 0 })
|
||||||
|
: backendLabel(res);
|
||||||
loaded = {
|
loaded = {
|
||||||
modelId,
|
modelId,
|
||||||
friendlyId: (entry && entry.id) || opts.model || null,
|
friendlyId: (entry && entry.id) || opts.model || null,
|
||||||
@@ -549,6 +634,7 @@ async function close() {
|
|||||||
}
|
}
|
||||||
sdk = null;
|
sdk = null;
|
||||||
initPromise = null;
|
initPromise = null;
|
||||||
|
lastResources = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIdleUnloadMin(min) {
|
function setIdleUnloadMin(min) {
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Packed Bare extracts the llama addon .bare + backend .so files under /tmp,
|
||||||
|
* but addon.js still sets backendsDir from bundled __dirname (`bare:/...`).
|
||||||
|
* C++ cannot dlopen that URL; llama.cpp then runs --fit against a broken
|
||||||
|
* backend path and init fails with "Failed to initialize model".
|
||||||
|
*
|
||||||
|
* Point backendsDir at the extracted prebuilds directory before createInstance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function posixDirname(p) {
|
||||||
|
const n = String(p || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
||||||
|
const i = n.lastIndexOf('/');
|
||||||
|
if (i < 0) return n;
|
||||||
|
if (i === 0) return '/';
|
||||||
|
return n.slice(0, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenError(err) {
|
||||||
|
const parts = [];
|
||||||
|
let cur = err;
|
||||||
|
for (let n = 0; cur && n < 6; n++) {
|
||||||
|
const msg = cur && cur.message ? String(cur.message) : cur ? String(cur) : '';
|
||||||
|
if (msg && parts.indexOf(msg) === -1) parts.push(msg);
|
||||||
|
cur = cur && cur.cause;
|
||||||
|
}
|
||||||
|
return parts.join(' | ') || 'unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hrefOf(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
if (typeof url === 'string') return url;
|
||||||
|
if (typeof url.href === 'string') return url.href;
|
||||||
|
try {
|
||||||
|
return String(url);
|
||||||
|
} catch (_) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileUrlToPath(href) {
|
||||||
|
if (!href || href.indexOf('file:') !== 0) return null;
|
||||||
|
try {
|
||||||
|
const url = require('bare-url');
|
||||||
|
if (url && typeof url.fileURLToPath === 'function') return url.fileURLToPath(href);
|
||||||
|
} catch (_) {}
|
||||||
|
let rest = href.replace(/^file:\/\//, '');
|
||||||
|
if (/^\/[A-Za-z]:\//.test(rest)) rest = rest.slice(1);
|
||||||
|
try {
|
||||||
|
rest = decodeURIComponent(rest);
|
||||||
|
} catch (_) {}
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* addon.js uses path.join(packageRoot, 'prebuilds').
|
||||||
|
* Extracted .bare lives at prebuilds/<host>/qvac__llm-llamacpp.bare
|
||||||
|
*/
|
||||||
|
function backendsDirFromAddonHref(href) {
|
||||||
|
const raw = hrefOf(href);
|
||||||
|
if (!raw || !/qvac__llm-llamacpp|llm-llamacpp/i.test(raw)) return null;
|
||||||
|
const file = raw.indexOf('file:') === 0 ? fileUrlToPath(raw) : raw.indexOf('bare:') === 0 ? null : raw;
|
||||||
|
if (!file) return null;
|
||||||
|
const base = file.replace(/\\/g, '/');
|
||||||
|
if (!/\.(bare|node)$/i.test(base)) return null;
|
||||||
|
return posixDirname(posixDirname(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBackendsDir() {
|
||||||
|
try {
|
||||||
|
const cache = typeof Bare !== 'undefined' && Bare.Addon && Bare.Addon.cache;
|
||||||
|
if (cache) {
|
||||||
|
const hrefs = Object.keys(cache);
|
||||||
|
for (let i = 0; i < hrefs.length; i++) {
|
||||||
|
const dir = backendsDirFromAddonHref(hrefs[i]);
|
||||||
|
if (dir) return dir;
|
||||||
|
const addon = cache[hrefs[i]];
|
||||||
|
const fromUrl = backendsDirFromAddonHref(addon && addon.url);
|
||||||
|
if (fromUrl) return fromUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchLlamaInterface(logFn) {
|
||||||
|
const log = typeof logFn === 'function' ? logFn : function () {};
|
||||||
|
let addon;
|
||||||
|
try {
|
||||||
|
addon = require('@qvac/llm-llamacpp/addon.js');
|
||||||
|
} catch (err) {
|
||||||
|
log('llm addon.js unavailable: ' + ((err && err.message) || err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Orig = addon && addon.LlamaInterface;
|
||||||
|
if (!Orig) return false;
|
||||||
|
if (Orig.__bsBackendsPatched) return true;
|
||||||
|
function Wrapped(binding, configurationParams, outputCb) {
|
||||||
|
if (!configurationParams) configurationParams = {};
|
||||||
|
if (!configurationParams.config) configurationParams.config = {};
|
||||||
|
const dir = resolveBackendsDir();
|
||||||
|
if (dir) {
|
||||||
|
configurationParams.config.backendsDir = dir;
|
||||||
|
log('llm backendsDir ' + dir);
|
||||||
|
}
|
||||||
|
return new Orig(binding, configurationParams, outputCb);
|
||||||
|
}
|
||||||
|
Wrapped.prototype = Orig.prototype;
|
||||||
|
try {
|
||||||
|
Object.setPrototypeOf(Wrapped, Orig);
|
||||||
|
} catch (_) {}
|
||||||
|
Wrapped.__bsBackendsPatched = true;
|
||||||
|
Orig.__bsBackendsPatched = true;
|
||||||
|
addon.LlamaInterface = Wrapped;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
flattenError,
|
||||||
|
backendsDirFromAddonHref,
|
||||||
|
resolveBackendsDir,
|
||||||
|
patchLlamaInterface,
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
function runQvacSelftest(logErr, normalizeBareVersions) {
|
||||||
|
logErr('qvac-selftest starting');
|
||||||
|
try {
|
||||||
|
normalizeBareVersions();
|
||||||
|
} catch (err) {
|
||||||
|
logErr('qvac-selftest normalize fail: ' + ((err && err.message) || err));
|
||||||
|
}
|
||||||
|
logErr('qvac-selftest importing plugins');
|
||||||
|
import('@qvac/inference/plugins')
|
||||||
|
.then((mod) => {
|
||||||
|
logErr('qvac-selftest ok plugins=' + typeof (mod.plugins || mod.default));
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
const msg = String((err && err.message) || err);
|
||||||
|
logErr('qvac-selftest fail: ' + msg);
|
||||||
|
process.exit(/INVALID_VERSION/i.test(msg) ? 1 : 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runQvacSelftest };
|
||||||
@@ -15,13 +15,6 @@ function concreteVersion(input) {
|
|||||||
return m ? m[1] : input;
|
return m ? m[1] : input;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceEnginesMap(engines) {
|
|
||||||
if (!engines || typeof engines !== 'object') return engines;
|
|
||||||
const out = {};
|
|
||||||
for (const key of Object.keys(engines)) out[key] = concreteVersion(engines[key]);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function log(msg) {
|
function log(msg) {
|
||||||
try {
|
try {
|
||||||
process.stderr.write('[bridge-swarm-qvac] ' + msg + '\n');
|
process.stderr.write('[bridge-swarm-qvac] ' + msg + '\n');
|
||||||
@@ -70,24 +63,6 @@ function normalizeBareVersions() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchBareModule() {
|
|
||||||
try {
|
|
||||||
const Module = typeof module !== 'undefined' && module.constructor;
|
|
||||||
if (!Module) return;
|
|
||||||
const wrap = (name) => {
|
|
||||||
const orig = Module[name];
|
|
||||||
if (typeof orig !== 'function' || orig.__bsPatched) return;
|
|
||||||
Module[name] = function patchedModuleFn() {
|
|
||||||
normalizeBareVersions();
|
|
||||||
return orig.apply(this, arguments);
|
|
||||||
};
|
|
||||||
Module[name].__bsPatched = true;
|
|
||||||
};
|
|
||||||
wrap('resolve');
|
|
||||||
wrap('asset');
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
try {
|
try {
|
||||||
const semver = require('bare-semver');
|
const semver = require('bare-semver');
|
||||||
@@ -103,17 +78,16 @@ function install() {
|
|||||||
try {
|
try {
|
||||||
const resolve = require('bare-module-resolve');
|
const resolve = require('bare-module-resolve');
|
||||||
if (typeof resolve.validateEngines === 'function' && !resolve.validateEngines.__bsPatched) {
|
if (typeof resolve.validateEngines === 'function' && !resolve.validateEngines.__bsPatched) {
|
||||||
const orig = resolve.validateEngines;
|
resolve.validateEngines = function patchedValidateEngines() {
|
||||||
resolve.validateEngines = function patchedValidateEngines(packageURL, packageEngines, opts) {
|
// Packed Bare.versions.bare can be a range; skip engines checks.
|
||||||
const next = Object.assign({}, opts || {});
|
return;
|
||||||
if (next.engines) next.engines = coerceEnginesMap(next.engines);
|
|
||||||
return orig(packageURL, packageEngines, next);
|
|
||||||
};
|
};
|
||||||
resolve.validateEngines.__bsPatched = true;
|
resolve.validateEngines.__bsPatched = true;
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
patchBareModule();
|
// Do not wrap Module.resolve: the packed runtime's resolver is inside
|
||||||
|
// bare:/bare.bundle and wrapping module.constructor.resolve can stall imports.
|
||||||
normalizeBareVersions();
|
normalizeBareVersions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ export async function warmDefaultModules() {
|
|||||||
try {
|
try {
|
||||||
await import('@qvac/inference');
|
await import('@qvac/inference');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
await import('@qvac/inference/plugins');
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
await import('@qvac/inference/llamacpp-completion/plugin');
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
await import('@qvac/inference/models');
|
||||||
|
} catch (_) {}
|
||||||
try {
|
try {
|
||||||
await import('bare-gpu-info');
|
await import('bare-gpu-info');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ async function testBareVersionsCoerce() {
|
|||||||
|
|
||||||
const { satisfies } = require('bare-semver');
|
const { satisfies } = require('bare-semver');
|
||||||
ok(satisfies('^1.30.3', '^1.30.3') === true, 'satisfies coerces caret runtime version');
|
ok(satisfies('^1.30.3', '^1.30.3') === true, 'satisfies coerces caret runtime version');
|
||||||
ok(!!(module.constructor.resolve && module.constructor.resolve.__bsPatched), 'runtime Module.resolve wrapped');
|
|
||||||
|
|
||||||
Object.defineProperty(Bare, 'versions', {
|
Object.defineProperty(Bare, 'versions', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ function patchBundle(bundle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let jsonFixed = 0;
|
let jsonFixed = 0;
|
||||||
|
let enginesStripped = 0;
|
||||||
let keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
|
let keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
|
||||||
for (const key of keysToProcess) {
|
for (const key of keysToProcess) {
|
||||||
if (!key.endsWith('.json')) continue;
|
if (!key.endsWith('.json')) continue;
|
||||||
@@ -195,6 +196,18 @@ function patchBundle(bundle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (content && content.length > 0) {
|
if (content && content.length > 0) {
|
||||||
|
const keyNormSlash = key.replace(/\\/g, '/');
|
||||||
|
if (/package\.json$/i.test(keyNormSlash)) {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(content.toString());
|
||||||
|
if (json && json.engines) {
|
||||||
|
delete json.engines;
|
||||||
|
content = Buffer.from(JSON.stringify(json));
|
||||||
|
bundle.write(key, content);
|
||||||
|
enginesStripped++;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
const keyNoLead = key.replace(/^\/+/, '');
|
const keyNoLead = key.replace(/^\/+/, '');
|
||||||
const prefixSlash = 'runtime.bundle/' + keyNoLead;
|
const prefixSlash = 'runtime.bundle/' + keyNoLead;
|
||||||
const prefixLead = '/runtime.bundle/' + keyNoLead;
|
const prefixLead = '/runtime.bundle/' + keyNoLead;
|
||||||
@@ -207,6 +220,11 @@ function patchBundle(bundle) {
|
|||||||
void resolveKey;
|
void resolveKey;
|
||||||
|
|
||||||
if (jsonFixed > 0) console.log(` Patched ${jsonFixed} empty/invalid .json entries`);
|
if (jsonFixed > 0) console.log(` Patched ${jsonFixed} empty/invalid .json entries`);
|
||||||
|
if (enginesStripped > 0) {
|
||||||
|
console.log(
|
||||||
|
` Stripped engines from ${enginesStripped} package.json files (packed Bare.versions.bare can be a range)`
|
||||||
|
);
|
||||||
|
}
|
||||||
console.log(' Added runtime.bundle pathname variants for .json entries');
|
console.log(' Added runtime.bundle pathname variants for .json entries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Send qvac.setEnabled over native messaging and fail on INVALID_VERSION."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
binpath = sys.argv[1]
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.setdefault('BRIDGE_SWARM_STORAGE', '/tmp/bs-qvac-packed-selftest')
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[binpath],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
err_chunks = []
|
||||||
|
|
||||||
|
def pump_err():
|
||||||
|
while True:
|
||||||
|
line = proc.stderr.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
err_chunks.append(line)
|
||||||
|
sys.stderr.buffer.write(line)
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
threading.Thread(target=pump_err, daemon=True).start()
|
||||||
|
time.sleep(1.5)
|
||||||
|
msg = json.dumps(
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'type': 'capability',
|
||||||
|
'payload': {
|
||||||
|
'pack': 'qvac',
|
||||||
|
'cmd': 'setEnabled',
|
||||||
|
'payload': {'enabled': True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).encode()
|
||||||
|
proc.stdin.write(struct.pack('<I', len(msg)) + msg)
|
||||||
|
proc.stdin.flush()
|
||||||
|
hdr = proc.stdout.read(4)
|
||||||
|
if len(hdr) < 4:
|
||||||
|
print('FAIL: no native-messaging reply', file=sys.stderr)
|
||||||
|
proc.kill()
|
||||||
|
return 1
|
||||||
|
n = struct.unpack('<I', hdr)[0]
|
||||||
|
body = proc.stdout.read(n).decode('utf-8', 'replace')
|
||||||
|
print('REPLY', body[:4000])
|
||||||
|
time.sleep(0.5)
|
||||||
|
proc.kill()
|
||||||
|
blob = body + b''.join(err_chunks).decode('utf-8', 'replace')
|
||||||
|
if 'INVALID_VERSION' in blob:
|
||||||
|
print('FAIL: packed host still throws INVALID_VERSION', file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print('PASS: no INVALID_VERSION')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -182,10 +182,32 @@ function testDeviceFallback() {
|
|||||||
assert.strictEqual(gpuMissing.fallback, 'cpu');
|
assert.strictEqual(gpuMissing.fallback, 'cpu');
|
||||||
const metal = device.backendLabel({ drivers: { metal: true }, gpus: [{ name: 'Apple M4', memory: 1 }] });
|
const metal = device.backendLabel({ drivers: { metal: true }, gpus: [{ name: 'Apple M4', memory: 1 }] });
|
||||||
assert.strictEqual(metal.backend, 'metal');
|
assert.strictEqual(metal.backend, 'metal');
|
||||||
|
const vulkan = device.backendLabel({ drivers: { vulkan: true }, gpus: [{ name: 'NVIDIA' }] });
|
||||||
|
assert.strictEqual(vulkan.backend, 'vulkan');
|
||||||
|
assert.strictEqual(vulkan.deviceName, 'NVIDIA');
|
||||||
|
const cpuOnly = device.backendLabel({ drivers: {}, gpus: [] });
|
||||||
|
assert.strictEqual(cpuOnly.backend, 'cpu');
|
||||||
}
|
}
|
||||||
|
|
||||||
testDeviceFallback();
|
testDeviceFallback();
|
||||||
|
|
||||||
|
function testLlamacppHostHelpers() {
|
||||||
|
const host = require('../native-host/qvac/llamacpp-host.js');
|
||||||
|
assert.strictEqual(
|
||||||
|
host.backendsDirFromAddonHref(
|
||||||
|
'file:///tmp/bridge-swarm-host-abc/node_modules/@qvac/llm-llamacpp/prebuilds/linux-x64/qvac__llm-llamacpp.bare'
|
||||||
|
),
|
||||||
|
'/tmp/bridge-swarm-host-abc/node_modules/@qvac/llm-llamacpp/prebuilds'
|
||||||
|
);
|
||||||
|
assert.strictEqual(host.backendsDirFromAddonHref('bare:/app.bundle/node_modules/@qvac/llm-llamacpp/addon.js'), null);
|
||||||
|
const wrapped = new Error('Failed to load model: Failed to initialize model');
|
||||||
|
wrapped.cause = new Error('failed to load model');
|
||||||
|
assert.ok(host.flattenError(wrapped).indexOf('Failed to initialize model') !== -1);
|
||||||
|
assert.ok(host.flattenError(wrapped).indexOf('failed to load model') !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
testLlamacppHostHelpers();
|
||||||
|
|
||||||
function testCompletionEventMap() {
|
function testCompletionEventMap() {
|
||||||
const events = require('../native-host/qvac/events.js');
|
const events = require('../native-host/qvac/events.js');
|
||||||
assert.deepStrictEqual(events.normalizeCompletionEvent({ type: 'contentDelta', text: 'hi' }), {
|
assert.deepStrictEqual(events.normalizeCompletionEvent({ type: 'contentDelta', text: 'hi' }), {
|
||||||
|
|||||||
Reference in New Issue
Block a user