@@ -105,6 +105,8 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing QVAC enable over native messaging"
|
||||
python3 scripts/qvac-packed-selftest.py "$BIN"
|
||||
echo "PASS: smoke test complete"
|
||||
|
||||
- name: Generate checksums
|
||||
|
||||
@@ -72,6 +72,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing QVAC enable over native messaging"
|
||||
python3 scripts/qvac-packed-selftest.py "$BIN"
|
||||
|
||||
- name: List release artifacts
|
||||
run: |
|
||||
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">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">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">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>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from './register-default-packs.mjs';
|
||||
import _messenger from './messenger.js';
|
||||
import _host from './host.js';
|
||||
import selftest from './qvac/selftest.js';
|
||||
|
||||
installConsoleToStderr();
|
||||
normalizeBareVersions();
|
||||
@@ -49,6 +50,8 @@ if (process.argv.includes('--extract-addons')) {
|
||||
await warmDefaultModules();
|
||||
await extractAddons(DEFAULT_ADDON_PACKAGES);
|
||||
})();
|
||||
} else if (process.argv.includes('--qvac-selftest') || process.env.BRIDGE_SWARM_QVAC_SELFTEST === '1') {
|
||||
selftest.runQvacSelftest(logErr, normalizeBareVersions);
|
||||
} else {
|
||||
try {
|
||||
startHost(_messenger, _host);
|
||||
|
||||
@@ -44,7 +44,7 @@ function backendLabel(resources) {
|
||||
if (process.platform === 'darwin' || drivers.metal) {
|
||||
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: 'cpu', backendId: 0, deviceName: gpuName, vram };
|
||||
|
||||
+106
-20
@@ -12,6 +12,7 @@ const { ensureQvacRoot } = require('../capabilities/paths.js');
|
||||
const catalog = require('./catalog.js');
|
||||
const device = require('./device.js');
|
||||
const events = require('./events.js');
|
||||
const llamacppHost = require('./llamacpp-host.js');
|
||||
|
||||
const SKIP = process.env.BRIDGE_SWARM_SKIP_QVAC === '1';
|
||||
|
||||
@@ -68,7 +69,10 @@ function setUserEnabled(on) {
|
||||
if (userEnabled === next) return;
|
||||
userEnabled = next;
|
||||
initPromise = null;
|
||||
if (!next) initError = 'disabled';
|
||||
if (!next) {
|
||||
initError = 'disabled';
|
||||
lastResources = null;
|
||||
}
|
||||
}
|
||||
|
||||
function disabledError() {
|
||||
@@ -108,6 +112,11 @@ async function assembleSdk() {
|
||||
return null;
|
||||
}
|
||||
normalizeBareVersions();
|
||||
try {
|
||||
llamacppHost.patchLlamaInterface(log);
|
||||
} catch (err) {
|
||||
log('llm backends patch skipped: ' + ((err && err.message) || err));
|
||||
}
|
||||
ensureQvacRoot();
|
||||
if (!process.env.QVAC_CONFIG_PATH) {
|
||||
const cfg = path.join(__dirname, '..', 'qvac.config.json');
|
||||
@@ -120,14 +129,24 @@ async function assembleSdk() {
|
||||
await import('@qvac/fabric');
|
||||
} 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) {
|
||||
initError = infMain.__error.message || String(infMain.__error);
|
||||
log('inference plugins unavailable: ' + initError);
|
||||
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) {
|
||||
initError = llmMod.__error.message || String(llmMod.__error);
|
||||
log('llm plugin unavailable: ' + initError);
|
||||
@@ -182,9 +201,14 @@ function ensureInit() {
|
||||
if (initPromise) return initPromise;
|
||||
normalizeBareVersions();
|
||||
initPromise = assembleSdk()
|
||||
.then((s) => {
|
||||
.then(async (s) => {
|
||||
sdk = s;
|
||||
if (!s) initPromise = null;
|
||||
else {
|
||||
try {
|
||||
await probeHardware(s);
|
||||
} catch (_) {}
|
||||
}
|
||||
return s;
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -195,6 +219,8 @@ function ensureInit() {
|
||||
}
|
||||
|
||||
function publicStatus() {
|
||||
const hw = lastResources || {};
|
||||
const modelLoaded = !!loaded.modelId;
|
||||
return {
|
||||
enabled: isUserEnabled(),
|
||||
available: isUserEnabled() && !!sdk && !skipped(),
|
||||
@@ -206,11 +232,11 @@ function publicStatus() {
|
||||
constant: loaded.constant,
|
||||
modelType: loaded.modelType,
|
||||
tools: loaded.tools,
|
||||
device: loaded.device,
|
||||
backend: loaded.backend,
|
||||
backendId: loaded.backendId,
|
||||
deviceName: loaded.deviceName,
|
||||
vram: loaded.vram,
|
||||
device: modelLoaded ? loaded.device : hw.deviceDefault || loaded.device,
|
||||
backend: loaded.backend || hw.backend || null,
|
||||
backendId: loaded.backendId != null ? loaded.backendId : hw.backendId,
|
||||
deviceName: loaded.deviceName || hw.deviceName || null,
|
||||
vram: loaded.vram || hw.vram || null,
|
||||
ctxSize: loaded.ctxSize,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
@@ -230,8 +256,13 @@ const pickDevice = device.pickDevice;
|
||||
const backendLabel = device.backendLabel;
|
||||
|
||||
async function probeGpuInfo() {
|
||||
const gpuMod = await tryImport('bare-gpu-info');
|
||||
if (gpuMod.__error) return null;
|
||||
let gpuMod;
|
||||
try {
|
||||
gpuMod = await import('bare-gpu-info');
|
||||
} catch (err) {
|
||||
log('bare-gpu-info unavailable: ' + ((err && err.message) || err));
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const GPUInfo = gpuMod.default || gpuMod.GPUInfo || gpuMod;
|
||||
if (typeof GPUInfo !== 'function') return null;
|
||||
@@ -243,19 +274,18 @@ async function probeGpuInfo() {
|
||||
if (typeof info.destroy === 'function') info.destroy();
|
||||
} catch (_) {}
|
||||
return { gpus, drivers, length };
|
||||
} catch (_) {
|
||||
} catch (err) {
|
||||
log('gpu probe failed: ' + ((err && err.message) || err));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resources(opts) {
|
||||
if (!isUserEnabled()) throw disabledError();
|
||||
const s = await ensureInit();
|
||||
async function probeHardware(s) {
|
||||
const ram = totalmem();
|
||||
let qvacRes = null;
|
||||
if (s && typeof s.getSystemResources === 'function') {
|
||||
try {
|
||||
qvacRes = await s.getSystemResources({ sample: !!(opts && opts.sample) });
|
||||
qvacRes = await s.getSystemResources({ sample: false });
|
||||
} catch (err) {
|
||||
qvacRes = { error: err.message };
|
||||
}
|
||||
@@ -265,6 +295,7 @@ async function resources(opts) {
|
||||
if (gpuProbe && gpuProbe.gpus && gpuProbe.gpus[0] && gpuProbe.gpus[0].memory) {
|
||||
merged.vram = gpuProbe.gpus[0].memory;
|
||||
}
|
||||
const bl = backendLabel(merged);
|
||||
lastResources = {
|
||||
totalRamBytes: ram,
|
||||
capabilities: qvacRes && qvacRes.capabilities,
|
||||
@@ -274,11 +305,31 @@ async function resources(opts) {
|
||||
drivers: gpuProbe && gpuProbe.drivers,
|
||||
suggest: catalog.suggestProfile({ totalRamBytes: ram, vramBytes: merged.vram }),
|
||||
deviceDefault: pickDevice('auto', merged).device,
|
||||
...backendLabel(merged),
|
||||
...bl,
|
||||
};
|
||||
log(
|
||||
'hardware ' +
|
||||
lastResources.deviceDefault +
|
||||
'/' +
|
||||
(lastResources.backend || 'none') +
|
||||
(lastResources.deviceName ? ' ' + lastResources.deviceName : '')
|
||||
);
|
||||
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) {
|
||||
if (modelSrc && typeof modelSrc === 'object') return 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 modelSrc = await resolveSrc(s, opts.modelSrc || (entry && entry.constant) || opts.model);
|
||||
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 ctxSize = opts.ctxSize || opts.ctx_size || (entry && entry.ctxSize) || 8192;
|
||||
const modelConfig = Object.assign(
|
||||
@@ -339,8 +390,42 @@ async function load(opts, onProgress) {
|
||||
}
|
||||
if (opts.delegate) loadOpts.delegate = opts.delegate;
|
||||
|
||||
const modelId = await s.loadModel(loadOpts);
|
||||
const bl = backendLabel(res);
|
||||
log(
|
||||
'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 = {
|
||||
modelId,
|
||||
friendlyId: (entry && entry.id) || opts.model || null,
|
||||
@@ -549,6 +634,7 @@ async function close() {
|
||||
}
|
||||
sdk = null;
|
||||
initPromise = null;
|
||||
lastResources = null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
process.stderr.write('[bridge-swarm-qvac] ' + msg + '\n');
|
||||
@@ -70,24 +63,6 @@ function normalizeBareVersions() {
|
||||
} 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() {
|
||||
try {
|
||||
const semver = require('bare-semver');
|
||||
@@ -103,17 +78,16 @@ function install() {
|
||||
try {
|
||||
const resolve = require('bare-module-resolve');
|
||||
if (typeof resolve.validateEngines === 'function' && !resolve.validateEngines.__bsPatched) {
|
||||
const orig = resolve.validateEngines;
|
||||
resolve.validateEngines = function patchedValidateEngines(packageURL, packageEngines, opts) {
|
||||
const next = Object.assign({}, opts || {});
|
||||
if (next.engines) next.engines = coerceEnginesMap(next.engines);
|
||||
return orig(packageURL, packageEngines, next);
|
||||
resolve.validateEngines = function patchedValidateEngines() {
|
||||
// Packed Bare.versions.bare can be a range; skip engines checks.
|
||||
return;
|
||||
};
|
||||
resolve.validateEngines.__bsPatched = true;
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ export async function warmDefaultModules() {
|
||||
try {
|
||||
await import('@qvac/inference');
|
||||
} 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 {
|
||||
await import('bare-gpu-info');
|
||||
} catch (_) {}
|
||||
|
||||
@@ -154,7 +154,6 @@ async function testBareVersionsCoerce() {
|
||||
|
||||
const { satisfies } = require('bare-semver');
|
||||
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', {
|
||||
configurable: true,
|
||||
|
||||
@@ -164,6 +164,7 @@ function patchBundle(bundle) {
|
||||
}
|
||||
|
||||
let jsonFixed = 0;
|
||||
let enginesStripped = 0;
|
||||
let keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
|
||||
for (const key of keysToProcess) {
|
||||
if (!key.endsWith('.json')) continue;
|
||||
@@ -195,6 +196,18 @@ function patchBundle(bundle) {
|
||||
}
|
||||
}
|
||||
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 prefixSlash = 'runtime.bundle/' + keyNoLead;
|
||||
const prefixLead = '/runtime.bundle/' + keyNoLead;
|
||||
@@ -207,6 +220,11 @@ function patchBundle(bundle) {
|
||||
void resolveKey;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
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');
|
||||
const metal = device.backendLabel({ drivers: { metal: true }, gpus: [{ name: 'Apple M4', memory: 1 }] });
|
||||
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();
|
||||
|
||||
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() {
|
||||
const events = require('../native-host/qvac/events.js');
|
||||
assert.deepStrictEqual(events.normalizeCompletionEvent({ type: 'contentDelta', text: 'hi' }), {
|
||||
|
||||
Reference in New Issue
Block a user