Actually vendor harness
This commit is contained in:
Vendored
+290
@@ -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,
|
||||
};
|
||||
Vendored
+176
@@ -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,
|
||||
};
|
||||
Vendored
+38
@@ -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 };
|
||||
Vendored
+51
@@ -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 };
|
||||
Vendored
+110
@@ -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,
|
||||
};
|
||||
Vendored
+461
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user