Files
bare-operating-system/packages/bare-os-booter/lib/services/bare-os-qvac-sdk-bind.mjs
T
snxraven d3aa66b0cc
Release rolling / release (push) Successful in 10m20s
Updates
2026-08-18 20:01:31 -04:00

480 lines
14 KiB
JavaScript

/**
* Static @qvac/sdk binding for Bare standalone pack + in-process Bare.
*
* Imported only via relative dynamic `import('./bare-os-qvac-sdk-bind.mjs')` so
* booter startup does not evaluate the SDK (avoids CodeRange OOM). bare-pack
* still embeds this module because pack-anchor / host reference the literal path.
*
* When packing with --skip-qvac, `@qvac/sdk` resolves to build/stubs (see bare-standalone).
*/
import * as sdkNs from '@qvac/sdk'
import { llmPlugin } from '@qvac/sdk/llamacpp-completion/plugin'
import LlmLlamacpp from '@qvac/llm-llamacpp'
import barePath from 'bare-path'
import hostFs from '#host-fs'
import os from 'bare-os'
import bareModule from 'bare-module'
import { bareOsQvacPrepareHostCacheConfig } from './bare-os-qvac-models-store.mjs'
import {
bareOsQvacHostBackendsRoot,
bundledExecCandidates
} from '../host/paths.js'
const createRequire =
bareModule.createRequire ||
(bareModule.default && bareModule.default.createRequire)
if (typeof createRequire !== 'function') {
throw new Error('bare-module createRequire unavailable')
}
const requireFromBind = createRequire(import.meta.url)
/** Well-known ggml backend filenames when bundle readdir fails. */
const KNOWN_GGML_BACKEND_NAMES = [
'libqvac-ggml-cpu-x64.so',
'libqvac-ggml-cpu-sse42.so',
'libqvac-ggml-cpu-haswell.so',
'libqvac-ggml-cpu-ivybridge.so',
'libqvac-ggml-cpu-sandybridge.so',
'libqvac-ggml-cpu-skylake.so',
'libqvac-ggml-cpu-skylakex.so',
'libqvac-ggml-cpu-alderlake.so',
'libqvac-ggml-cpu-cannonlake.so',
'libqvac-ggml-cpu-cascadelake.so',
'libqvac-ggml-cpu-cooperlake.so',
'libqvac-ggml-cpu-icelake.so',
'libqvac-ggml-cpu-sapphirerapids.so',
'libqvac-ggml-cpu-piledriver.so',
'libqvac-ggml-cpu-zen4.so',
'libqvac-ggml-vulkan.so',
'libqvac-ggml-cpu.dylib',
'libqvac-ggml-metal.dylib',
'qvac-ggml-cpu.dll',
'qvac-ggml-vulkan.dll'
]
/**
* @param {string} probeDir absolute path to `<host>/qvac__llm-llamacpp`
* @returns {boolean}
*/
function backendsSlotHasCpu(probeDir) {
try {
if (!hostFs.existsSync(probeDir)) return false
return hostFs
.readdirSync(probeDir)
.some((n) => typeof n === 'string' && /ggml-cpu/i.test(n))
} catch {
return false
}
}
/**
* @param {string} root prebuilds-equivalent root (contains `<host>/qvac__llm-llamacpp`)
* @param {string} slot `<host>/qvac__llm-llamacpp`
* @returns {string} root if valid, else ''
*/
function acceptBackendsRoot(root, slot) {
if (!root) return ''
const abs = barePath.resolve(root)
return backendsSlotHasCpu(barePath.join(abs, slot)) ? abs : ''
}
/**
* Copy ggml backends from srcDir into host cache; return host root or ''.
* @param {string} srcDir
* @param {string} slot
* @param {string[]} [names]
* @returns {string}
*/
function materializeBackendsToHost(srcDir, slot, names) {
/** @type {string[]} */
let fileNames = names && names.length ? names.slice() : []
if (!fileNames.length) {
try {
fileNames = hostFs
.readdirSync(srcDir)
.filter((n) => typeof n === 'string' && /\.(so|dylib|dll)$/i.test(n))
} catch {
fileNames = []
}
}
if (!fileNames.length) {
for (const name of KNOWN_GGML_BACKEND_NAMES) {
try {
if (hostFs.existsSync(barePath.join(srcDir, name))) fileNames.push(name)
} catch {
/* ignore */
}
}
}
if (!fileNames.length) return ''
const hostRoot = bareOsQvacHostBackendsRoot()
const destDir = barePath.join(hostRoot, slot)
try {
hostFs.mkdirSync(destDir, { recursive: true })
} catch {
return ''
}
for (const name of fileNames) {
const from = barePath.join(srcDir, name)
const to = barePath.join(destDir, name)
try {
let srcSize = -1
try {
srcSize = hostFs.statSync(from).size
} catch {
continue
}
try {
if (hostFs.statSync(to).size === srcSize) continue
} catch {
/* copy */
}
if (typeof hostFs.copyFileSync === 'function') {
try {
hostFs.copyFileSync(from, to)
continue
} catch {
/* fall through */
}
}
hostFs.writeFileSync(to, hostFs.readFileSync(from))
} catch {
/* best-effort per file */
}
}
return acceptBackendsRoot(hostRoot, slot)
}
/**
* Prefer Bare `require.asset()` so packed `/app.bundle` assets extract to real paths.
* @param {string} host
* @param {string} slot
* @returns {string}
*/
function materializeBackendsViaAsset(host, slot) {
let llmReq
try {
const main = requireFromBind.resolve('@qvac/llm-llamacpp')
llmReq = createRequire(main)
} catch {
return ''
}
if (typeof llmReq.asset !== 'function') return ''
const hostRoot = bareOsQvacHostBackendsRoot()
const destDir = barePath.join(hostRoot, slot)
try {
hostFs.mkdirSync(destDir, { recursive: true })
} catch {
return ''
}
let wrote = 0
/** @type {string} */
let realAssetDir = ''
for (const name of KNOWN_GGML_BACKEND_NAMES) {
const rel = `./prebuilds/${host}/qvac__llm-llamacpp/${name}`
try {
const assetPath = String(llmReq.asset(rel) || '')
if (!assetPath) continue
const bundleLike = /(?:^|\/)app\.bundle(?:\/|$)/i.test(assetPath)
try {
const buf = hostFs.readFileSync(assetPath)
if (buf && buf.byteLength) {
hostFs.writeFileSync(barePath.join(destDir, name), buf)
wrote += 1
if (!bundleLike) realAssetDir = barePath.dirname(assetPath)
}
} catch {
if (!bundleLike) {
try {
if (typeof hostFs.copyFileSync === 'function') {
hostFs.copyFileSync(assetPath, barePath.join(destDir, name))
wrote += 1
realAssetDir = barePath.dirname(assetPath)
}
} catch {
/* ignore */
}
}
}
} catch {
/* asset missing for this name */
}
}
void wrote
const hostHit = acceptBackendsRoot(hostRoot, slot)
if (hostHit) return hostHit
// If assets landed in a real prebuilds tree, use that root directly.
if (realAssetDir && backendsSlotHasCpu(realAssetDir)) {
const prebuildsRoot = barePath.resolve(realAssetDir, '..', '..')
const hit = acceptBackendsRoot(prebuildsRoot, slot)
if (hit) return hit
}
return ''
}
const sdk =
sdkNs?.default && typeof sdkNs.default === 'object'
? { ...sdkNs, ...sdkNs.default }
: sdkNs
/** True when pack remapped @qvac/sdk to the CI stub (e.g. win32-arm64). */
export const qvacBindIsStub = Boolean(
sdk &&
(sdk.BARE_OS_QVAC_SDK_STUB === true ||
(llmPlugin &&
typeof llmPlugin === 'object' &&
/** @type {{ name?: string }} */ (llmPlugin).name === 'qvac-llm-plugin-stub'))
)
let llmLoadPatched = false
/**
* Host id matching `@qvac/llm-llamacpp/prebuilds/<host>/`.
* Uses bare-os's native platform/arch (the runtime may not expose a
* `globalThis.process`).
* @returns {string}
*/
export function bareOsQvacNativeHostId() {
let platform = ''
let arch = ''
try {
platform = typeof os.platform === 'function' ? os.platform() : ''
} catch {
platform = ''
}
try {
arch = typeof os.arch === 'function' ? os.arch() : ''
} catch {
arch = ''
}
const p = globalThis.process || {}
if (!platform) platform = String(p.platform || 'linux')
if (!arch) arch = String(p.arch || 'x64')
if (arch === 'x86_64') arch = 'x64'
if (arch === 'aarch64') arch = 'arm64'
return platform + '-' + arch
}
/**
* Package root for `@qvac/llm-llamacpp` (may be `/app.bundle/...` in standalone).
* @returns {string}
*/
export function bareOsQvacLlmPackageRoot() {
try {
return barePath.dirname(requireFromBind.resolve('@qvac/llm-llamacpp'))
} catch {
try {
return barePath.dirname(
requireFromBind.resolve('@qvac/llm-llamacpp/package.json')
)
} catch {
return ''
}
}
}
/**
* Hosts whose ggml backends (CPU + Metal) are statically linked into the addon
* and ship no external backend dylibs (the prebuilds dir only has the .bare).
* External `backendsDir` loading is unnecessary there.
* @returns {boolean}
*/
export function bareOsQvacHostHasBuiltinBackends() {
const host = bareOsQvacNativeHostId()
return host.startsWith('darwin-') || host.startsWith('ios-')
}
/**
* Materialize ggml backend .so files to a real host path.
* Bare standalone embeds them under `/app.bundle/...` which dlopen cannot open;
* llama.cpp then fails with `make_cpu_buft_list: no CPU backend found`.
*
* Returns the prebuilds-equivalent root to pass as `backendsDir`
* (native appends `<host>/qvac__llm-llamacpp`).
* @returns {string}
*/
export function bareOsQvacEnsureHostBackendsDir() {
const host = bareOsQvacNativeHostId()
const slot = barePath.join(host, 'qvac__llm-llamacpp')
if (bareOsQvacHostHasBuiltinBackends()) {
// CPU + Metal are built into the addon; no external ggml backend dylibs
// are shipped for darwin/ios. When a real prebuilds tree exists on disk,
// point backendsDir at it so ggml's load_all_from_path no-ops on the
// missing slot dir (it tolerates absent directories).
const pkgRoot = bareOsQvacLlmPackageRoot()
if (pkgRoot) {
const prebuilds = barePath.join(pkgRoot, 'prebuilds')
try {
if (hostFs.existsSync(prebuilds)) return prebuilds
} catch {
/* ignore */
}
}
return ''
}
const envOverride = String(
globalThis.process?.env?.BARE_OS_QVAC_BACKENDS_DIR || ''
).trim()
if (envOverride) {
const hit = acceptBackendsRoot(envOverride, slot)
if (hit) return hit
}
// Already materialized under ~/.bare-os/qvac/backends
{
const hit = acceptBackendsRoot(bareOsQvacHostBackendsRoot(), slot)
if (hit) return hit
}
// Sidecar next to the standalone binary (pack + install.sh).
const sidecarRoots = []
for (const dir of bundledExecCandidates()) {
sidecarRoots.push(barePath.join(dir, 'qvac-backends'))
}
try {
const home = String(
globalThis.process?.env?.HOME ||
globalThis.process?.env?.USERPROFILE ||
''
)
if (home) {
sidecarRoots.push(
barePath.join(home, '.local', 'share', 'bare-os', 'booter', 'qvac-backends')
)
sidecarRoots.push(
barePath.join(home, '.bare-os', 'qvac', 'backends')
)
}
} catch {
/* ignore */
}
for (const beside of sidecarRoots) {
const hit = acceptBackendsRoot(beside, slot)
if (hit) return hit
}
// Bare require.asset() → real host path (or readable bytes).
{
const hit = materializeBackendsViaAsset(host, slot)
if (hit) return hit
}
const pkgRoot = bareOsQvacLlmPackageRoot()
if (!pkgRoot) return ''
const srcDir = barePath.join(pkgRoot, 'prebuilds', slot)
const srcPrebuilds = barePath.join(pkgRoot, 'prebuilds')
const bundleLike = /(?:^|\/)app\.bundle(?:\/|$)/i.test(srcDir)
// Dev / unpacked: use package prebuilds directly when they exist on disk.
if (!bundleLike) {
const hit = acceptBackendsRoot(srcPrebuilds, slot)
if (hit) return hit
}
// Copy from package/bundle path into host cache when readable.
return materializeBackendsToHost(srcDir, slot)
}
/**
* Absolute backendsDir for llama.cpp (real host path after materialize).
* @returns {string}
*/
export function bareOsQvacResolveBackendsDir() {
return bareOsQvacEnsureHostBackendsDir()
}
/**
* llama.cpp `--fit` trial-loads often fail on hybrid Vulkan (and then CPU
* inherits the same opaque error). SDK schema rejects `fit`, so inject into
* the native config map after transform via the addon constructor path.
*/
export function bareOsQvacPatchLlmLoadDefaults() {
if (llmLoadPatched || qvacBindIsStub) return
const proto = LlmLlamacpp && LlmLlamacpp.prototype
if (!proto || typeof proto._load !== 'function') return
llmLoadPatched = true
const orig = proto._load
proto._load = async function bareOsQvacPatchedLlmLoad() {
const prev =
this._config && typeof this._config === 'object' ? this._config : {}
/** @type {Record<string, string>} */
const next = { ...prev }
// This fabric build rejects --no-mmap; only inject fit (not in SDK schema).
const fitEnv = String(
globalThis.process?.env?.BARE_OS_QVAC_FIT ?? 'off'
)
.trim()
.toLowerCase()
if (next.fit == null || next.fit === '') {
next.fit = fitEnv === 'on' ? 'on' : 'off'
}
delete next.no_mmap
delete next['no-mmap']
// Re-resolve every load — first patch-time resolve can miss a late sidecar.
const backendsDir = bareOsQvacResolveBackendsDir()
if (backendsDir) {
next.backendsDir = backendsDir
} else if (bareOsQvacHostHasBuiltinBackends()) {
// Builtin CPU + Metal on darwin/ios: no external backend dylibs to load.
} else if (next.backendsDir == null || next.backendsDir === '') {
// Avoid silent /app.bundle fallback: leave unset and let native fail loudly,
// but surface a clear JS warning once.
try {
const g = /** @type {any} */ (globalThis)
if (!g.__bareOsQvacBackendsDirWarned) {
g.__bareOsQvacBackendsDirWarned = true
console.warn(
'[qvac] ggml backendsDir unresolved — deploy qvac-backends/ beside bare-os-booter, or set BARE_OS_QVAC_BACKENDS_DIR (else: no CPU backend found)'
)
}
} catch {
/* ignore */
}
}
this._config = next
return orig.call(this)
}
}
/**
* Register llamacpp completion and return bound SDK surface (registry + load/completion).
* Writes Bare-OS host cache config before plugins() so downloads land under
* `$BARE_OS_HOST_DATA/qvac/models` instead of `~/.qvac/models`.
* @param {{ cacheDir?: string }} [opts]
* @returns {Record<string, any>}
*/
export function createBoundQvacApi(opts = {}) {
if (qvacBindIsStub) {
throw new Error(
'QVAC natives not packed for this host (use agent --config REST, or rebuild without --skip-qvac on a supported host)'
)
}
const prepared = bareOsQvacPrepareHostCacheConfig({
cacheDir: opts.cacheDir
})
bareOsQvacPatchLlmLoadDefaults()
const pluginsFn = sdk.plugins
if (typeof pluginsFn !== 'function') {
throw new Error('@qvac/sdk plugins() unavailable')
}
const bound = pluginsFn([llmPlugin])
return {
...sdk,
loadModel: bound.loadModel || sdk.loadModel,
completion: bound.completion || sdk.completion,
unloadModel: bound.unloadModel || sdk.unloadModel,
cancel: bound.cancel || sdk.cancel,
__bareOsQvacCacheDir: prepared.cacheDir,
__bareOsQvacConfigPath: prepared.configPath,
__bareOsQvacBackendsDir: bareOsQvacResolveBackendsDir()
}
}