601 lines
18 KiB
JavaScript
601 lines
18 KiB
JavaScript
/**
|
|
* QVAC GGUF storage: HDMS /mnt/models (durable) + host cache for llama.cpp load.
|
|
*/
|
|
|
|
import path from '#host-path'
|
|
import fs from '#host-fs'
|
|
import {
|
|
bareOsQvacHostModelsDir,
|
|
bareOsQvacHostConfigPath
|
|
} from '../host/paths.js'
|
|
|
|
export const BARE_OS_QVAC_MODELS_HDMS_LABEL = 'models'
|
|
export const BARE_OS_QVAC_MODELS_GUEST_ROOT = '/mnt/models'
|
|
|
|
/**
|
|
* @param {string} [hostCacheDir]
|
|
* @returns {string}
|
|
*/
|
|
export function bareOsQvacModelsHostCacheDir(hostCacheDir) {
|
|
const d = String(hostCacheDir || '').trim()
|
|
return d || bareOsQvacHostModelsDir()
|
|
}
|
|
|
|
/**
|
|
* Guest path for a GGUF basename under /mnt/models.
|
|
* @param {string} fileName
|
|
*/
|
|
export function bareOsQvacModelsGuestPath(fileName) {
|
|
const base = String(fileName || '')
|
|
.replace(/^\/+/, '')
|
|
.split('/')
|
|
.pop()
|
|
return BARE_OS_QVAC_MODELS_GUEST_ROOT + '/' + (base || 'model.gguf')
|
|
}
|
|
|
|
/**
|
|
* Host path for a GGUF basename under the cache dir.
|
|
* @param {string} fileName
|
|
* @param {string} [hostCacheDir]
|
|
*/
|
|
export function bareOsQvacModelsHostPath(fileName, hostCacheDir) {
|
|
const base = String(fileName || '')
|
|
.replace(/^\/+/, '')
|
|
.split('/')
|
|
.pop()
|
|
return path.join(bareOsQvacModelsHostCacheDir(hostCacheDir), base || 'model.gguf')
|
|
}
|
|
|
|
/**
|
|
* Strip leading content-hash prefix used by QVAC cache filenames.
|
|
* `5b8aae816570a09d_Qwen3-0.6B-Q4_0.gguf` → `Qwen3-0.6B-Q4_0.gguf`
|
|
* @param {string} fileName
|
|
*/
|
|
export function bareOsQvacModelsStripHashPrefix(fileName) {
|
|
const base =
|
|
String(fileName || '')
|
|
.replace(/^\/+/, '')
|
|
.split('/')
|
|
.pop() || ''
|
|
const m = /^[0-9a-f]{8,}_(.+)$/i.exec(base)
|
|
return m ? m[1] : base
|
|
}
|
|
|
|
/**
|
|
* Whether host file size matches an expected guest size (skip materialize).
|
|
* @param {string} hostPath
|
|
* @param {number} guestSize
|
|
*/
|
|
export function bareOsQvacHostGgufMatchesSize(hostPath, guestSize) {
|
|
const want = Number(guestSize)
|
|
if (!Number.isFinite(want) || want <= 0) return false
|
|
try {
|
|
const st = fs.statSync(hostPath)
|
|
return st.isFile() && st.size === want
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* True for a host filesystem path Bare's module resolver cannot load from
|
|
* `bare:/app.bundle` (absolute POSIX / Windows, not a URL specifier).
|
|
* @param {string} filePath
|
|
*/
|
|
export function bareOsQvacIsHostJsonConfigPath(filePath) {
|
|
const p = String(filePath || '')
|
|
if (!/\.json$/i.test(p)) return false
|
|
// Windows drive paths (`C:\...`) look like a URL scheme if checked first.
|
|
if (/^[A-Za-z]:[\\/]/.test(p)) return true
|
|
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(p)) return false
|
|
return p.startsWith('/')
|
|
}
|
|
|
|
/**
|
|
* Read a host JSON file for QVAC config. Returns undefined when the path is
|
|
* not a host .json file or the read/parse fails (caller may delegate).
|
|
* @param {unknown} id
|
|
* @returns {unknown | undefined}
|
|
*/
|
|
export function bareOsQvacTryReadHostJsonConfig(id) {
|
|
const filePath = String(id || '')
|
|
if (!bareOsQvacIsHostJsonConfigPath(filePath)) return undefined
|
|
try {
|
|
const raw = fs.readFileSync(filePath, 'utf8')
|
|
return JSON.parse(raw)
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bare's `@qvac/sdk` resolve-config.bare.js loads config via free `require(path)`.
|
|
* In standalone that identifier is `globalThis.require` from ctx-bare's
|
|
* `createRequire(bare:/app.bundle/.../bare-os-ctx-bare.js)`. That resolver
|
|
* cannot see host files (`MODULE_NOT_FOUND` with `bare:/home/...` candidates).
|
|
* Always wrap — do not skip when require already exists.
|
|
*/
|
|
export function bareOsQvacInstallConfigRequireShim() {
|
|
const g = globalThis
|
|
const prev = typeof g.require === 'function' ? g.require : null
|
|
if (prev && prev.__bareOsQvacConfigRequire) return prev
|
|
|
|
function bareOsQvacConfigRequire(id, ...rest) {
|
|
const parsed = bareOsQvacTryReadHostJsonConfig(id)
|
|
if (parsed !== undefined) return parsed
|
|
if (typeof prev === 'function') {
|
|
return Reflect.apply(prev, this, [id, ...rest])
|
|
}
|
|
throw new ReferenceError(
|
|
'require is not defined (Bare: only host .json config paths are shimmed)'
|
|
)
|
|
}
|
|
bareOsQvacConfigRequire.__bareOsQvacConfigRequire = true
|
|
if (prev) {
|
|
for (const key of ['cache', 'extensions', 'main', 'resolve', 'addon', 'asset']) {
|
|
try {
|
|
if (prev[key] !== undefined) bareOsQvacConfigRequire[key] = prev[key]
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
g.require = bareOsQvacConfigRequire
|
|
return bareOsQvacConfigRequire
|
|
}
|
|
|
|
/**
|
|
* Write runtime qvac.config.json with absolute cacheDirectory and set QVAC_CONFIG_PATH.
|
|
* Must run before `@qvac/sdk` plugins() so setSDKConfig sees it once.
|
|
* @param {{
|
|
* cacheDir?: string,
|
|
* loggerLevel?: string,
|
|
* loggerConsoleOutput?: boolean
|
|
* }} [opts]
|
|
* @returns {{ cacheDir: string, configPath: string }}
|
|
*/
|
|
export function bareOsQvacPrepareHostCacheConfig(opts = {}) {
|
|
bareOsQvacInstallConfigRequireShim()
|
|
const cacheDir = bareOsQvacModelsHostCacheDir(opts.cacheDir)
|
|
const configPath = bareOsQvacHostConfigPath()
|
|
const qvacRoot = path.dirname(configPath)
|
|
fs.mkdirSync(cacheDir, { recursive: true })
|
|
fs.mkdirSync(qvacRoot, { recursive: true })
|
|
const procEnv =
|
|
globalThis.process && globalThis.process.env
|
|
? globalThis.process.env
|
|
: null
|
|
// Quiet by default — SDK debug + llamacpp chatter floods the agent TTY.
|
|
// Override with BARE_OS_QVAC_LOG_LEVEL / QVAC_LOG_LEVEL (e.g. debug).
|
|
const envLevel = procEnv
|
|
? String(
|
|
procEnv.BARE_OS_QVAC_LOG_LEVEL || procEnv.QVAC_LOG_LEVEL || ''
|
|
).trim()
|
|
: ''
|
|
const loggerLevel = opts.loggerLevel || envLevel || 'error'
|
|
let loggerConsoleOutput = opts.loggerConsoleOutput
|
|
if (loggerConsoleOutput === undefined && procEnv) {
|
|
const ec = String(procEnv.BARE_OS_QVAC_LOGGER_CONSOLE || '').trim()
|
|
if (ec === '1' || ec === 'true') loggerConsoleOutput = true
|
|
else if (ec === '0' || ec === 'false') loggerConsoleOutput = false
|
|
}
|
|
if (loggerConsoleOutput === undefined) {
|
|
// Keep console on so real errors still surface; level gates noise.
|
|
loggerConsoleOutput = true
|
|
}
|
|
const body = {
|
|
plugins: ['@qvac/sdk/llamacpp-completion/plugin'],
|
|
cacheDirectory: cacheDir,
|
|
loggerLevel,
|
|
loggerConsoleOutput: Boolean(loggerConsoleOutput),
|
|
httpDownloadConcurrency: 3,
|
|
httpConnectionTimeoutMs: 15000
|
|
}
|
|
fs.writeFileSync(configPath, JSON.stringify(body, null, 2) + '\n')
|
|
if (procEnv) {
|
|
procEnv.QVAC_CONFIG_PATH = configPath
|
|
// Also pin env so @qvac/logging picks the same level before config apply.
|
|
if (!procEnv.QVAC_LOG_LEVEL) procEnv.QVAC_LOG_LEVEL = loggerLevel
|
|
}
|
|
return { cacheDir, configPath }
|
|
}
|
|
|
|
/**
|
|
* Ensure writable HDMS label `models` exists (best-effort).
|
|
* @param {{
|
|
* ctx?: Record<string, unknown> | null,
|
|
* hdmsController?: {
|
|
* active?: boolean,
|
|
* byLabel?: Map<string, unknown>,
|
|
* create?: (ctx: Record<string, unknown>, label: string) => Promise<void>
|
|
* } | null
|
|
* }} opts
|
|
* @returns {Promise<{ ok: boolean, created?: boolean, reason?: string, drive?: unknown }>}
|
|
*/
|
|
export async function bareOsQvacEnsureModelsHdms(opts = {}) {
|
|
const hc = opts.hdmsController
|
|
const ctx = opts.ctx
|
|
if (!hc || typeof hc.create !== 'function') {
|
|
return { ok: false, reason: 'no_hdms_controller' }
|
|
}
|
|
if (!hc.active) return { ok: false, reason: 'hdms_inactive' }
|
|
if (hc.byLabel && hc.byLabel.has(BARE_OS_QVAC_MODELS_HDMS_LABEL)) {
|
|
const slot = /** @type {{ drive?: unknown }} */ (
|
|
hc.byLabel.get(BARE_OS_QVAC_MODELS_HDMS_LABEL)
|
|
)
|
|
return { ok: true, created: false, drive: slot && slot.drive }
|
|
}
|
|
if (!ctx || typeof ctx !== 'object') {
|
|
return { ok: false, reason: 'no_ctx' }
|
|
}
|
|
try {
|
|
await hc.create(ctx, BARE_OS_QVAC_MODELS_HDMS_LABEL)
|
|
const slot = hc.byLabel
|
|
? /** @type {{ drive?: unknown }} */ (
|
|
hc.byLabel.get(BARE_OS_QVAC_MODELS_HDMS_LABEL)
|
|
)
|
|
: null
|
|
return { ok: true, created: true, drive: slot && slot.drive }
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e
|
|
? String(/** @type {{ message: unknown }} */ (e).message)
|
|
: String(e)
|
|
if (/already exists/i.test(msg)) {
|
|
const slot = hc.byLabel
|
|
? /** @type {{ drive?: unknown }} */ (
|
|
hc.byLabel.get(BARE_OS_QVAC_MODELS_HDMS_LABEL)
|
|
)
|
|
: null
|
|
return { ok: true, created: false, drive: slot && slot.drive }
|
|
}
|
|
return { ok: false, reason: msg }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} vfs
|
|
* @param {string} guestPath
|
|
* @returns {Promise<{ size: number } | null>}
|
|
*/
|
|
export async function bareOsQvacGuestGgufStat(vfs, guestPath) {
|
|
if (!vfs || typeof vfs !== 'object') return null
|
|
const v = /** @type {Record<string, any>} */ (vfs)
|
|
try {
|
|
if (typeof v.stat === 'function') {
|
|
const st = await v.stat(guestPath)
|
|
const size = Number(st && (st.size ?? st.byteLength))
|
|
if (Number.isFinite(size) && size > 0) return { size }
|
|
}
|
|
} catch {
|
|
/* missing */
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Stream Hyperdrive path → host file (avoid buffering full GGUF).
|
|
* @param {{ createReadStream?: (p: string) => import('stream').Readable, get?: Function }} drive
|
|
* @param {string} drivePath e.g. /Qwen3-0.6B-Q4_0.gguf
|
|
* @param {string} hostPath
|
|
* @returns {Promise<number>} bytes written
|
|
*/
|
|
export async function bareOsQvacStreamDriveToHost(drive, drivePath, hostPath) {
|
|
const rel = String(drivePath || '').startsWith('/')
|
|
? String(drivePath)
|
|
: '/' + String(drivePath || '').replace(/^\/+/, '')
|
|
if (drive && typeof drive.createReadStream === 'function') {
|
|
await new Promise((resolve, reject) => {
|
|
let settled = false
|
|
const fail = (err) => {
|
|
if (settled) return
|
|
settled = true
|
|
reject(err)
|
|
}
|
|
const done = () => {
|
|
if (settled) return
|
|
settled = true
|
|
resolve(undefined)
|
|
}
|
|
try {
|
|
const rs = drive.createReadStream(rel)
|
|
const ws = fs.createWriteStream(hostPath)
|
|
rs.on('error', fail)
|
|
ws.on('error', fail)
|
|
ws.on('finish', done)
|
|
if (typeof rs.pipe === 'function') {
|
|
rs.pipe(ws)
|
|
} else {
|
|
rs.on('data', (chunk) => {
|
|
ws.write(chunk)
|
|
})
|
|
rs.on('end', () => ws.end())
|
|
}
|
|
} catch (e) {
|
|
fail(e)
|
|
}
|
|
})
|
|
return fs.statSync(hostPath).size
|
|
}
|
|
if (drive && typeof drive.get === 'function') {
|
|
const buf = await drive.get(rel)
|
|
if (!buf || !(buf.byteLength > 0 || buf.length > 0)) {
|
|
throw new Error('empty_drive_get')
|
|
}
|
|
const u8 = Buffer.isBuffer(buf)
|
|
? buf
|
|
: Buffer.from(
|
|
buf.buffer || buf,
|
|
buf.byteOffset || 0,
|
|
buf.byteLength || buf.length
|
|
)
|
|
fs.writeFileSync(hostPath, u8)
|
|
return u8.byteLength
|
|
}
|
|
throw new Error('drive_no_stream')
|
|
}
|
|
|
|
/**
|
|
* Materialize guest GGUF → host cache path.
|
|
* Prefers Hyperdrive createReadStream when `drive` is provided; else vfs.readFile.
|
|
* Skips when host size already matches guest size.
|
|
* @param {{
|
|
* vfs?: Record<string, any> | null,
|
|
* guestPath: string,
|
|
* hostPath: string,
|
|
* drive?: { createReadStream?: Function, get?: Function } | null,
|
|
* drivePath?: string,
|
|
* guestSize?: number
|
|
* }} o
|
|
* @returns {Promise<{ ok: boolean, bytes?: number, reason?: string, skipped?: boolean }>}
|
|
*/
|
|
export async function bareOsQvacMaterializeGgufToHost(o) {
|
|
const guestPath = String(o.guestPath || '')
|
|
const hostPath = String(o.hostPath || '')
|
|
if (!guestPath || !hostPath) {
|
|
return { ok: false, reason: 'missing_args' }
|
|
}
|
|
try {
|
|
fs.mkdirSync(path.dirname(hostPath), { recursive: true })
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
let guestSize = Number(o.guestSize)
|
|
if (!(Number.isFinite(guestSize) && guestSize > 0) && o.vfs) {
|
|
const st = await bareOsQvacGuestGgufStat(o.vfs, guestPath)
|
|
if (st) guestSize = st.size
|
|
}
|
|
if (
|
|
Number.isFinite(guestSize) &&
|
|
guestSize > 0 &&
|
|
bareOsQvacHostGgufMatchesSize(hostPath, guestSize)
|
|
) {
|
|
return { ok: true, bytes: guestSize, skipped: true, reason: 'size_match' }
|
|
}
|
|
|
|
const base =
|
|
guestPath.replace(/^\/+/, '').split('/').pop() || path.basename(hostPath)
|
|
const drivePath =
|
|
o.drivePath ||
|
|
(base.startsWith('/') ? base : '/' + base)
|
|
|
|
if (o.drive) {
|
|
try {
|
|
const bytes = await bareOsQvacStreamDriveToHost(
|
|
o.drive,
|
|
drivePath,
|
|
hostPath
|
|
)
|
|
return { ok: true, bytes }
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e
|
|
? String(/** @type {{ message: unknown }} */ (e).message)
|
|
: String(e)
|
|
// Fall through to vfs.readFile
|
|
if (!o.vfs || typeof o.vfs.readFile !== 'function') {
|
|
return { ok: false, reason: msg }
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (!o.vfs || typeof o.vfs.readFile !== 'function') {
|
|
return { ok: false, reason: 'vfs_no_readFile' }
|
|
}
|
|
const buf = await o.vfs.readFile(guestPath)
|
|
if (!buf || !(buf.byteLength > 0 || buf.length > 0)) {
|
|
return { ok: false, reason: 'empty_guest' }
|
|
}
|
|
const u8 = Buffer.isBuffer(buf)
|
|
? buf
|
|
: Buffer.from(
|
|
buf.buffer || buf,
|
|
buf.byteOffset || 0,
|
|
buf.byteLength || buf.length
|
|
)
|
|
fs.writeFileSync(hostPath, u8)
|
|
return { ok: true, bytes: u8.byteLength }
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e
|
|
? String(/** @type {{ message: unknown }} */ (e).message)
|
|
: String(e)
|
|
return { ok: false, reason: msg }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mirror host GGUF into /mnt/models/<basename>.
|
|
* Skips when guest size already matches host.
|
|
* @param {{
|
|
* vfs: Record<string, any>,
|
|
* hostPath: string,
|
|
* guestName?: string
|
|
* }} o
|
|
* @returns {Promise<{ ok: boolean, guestPath?: string, reason?: string, skipped?: boolean }>}
|
|
*/
|
|
export async function bareOsQvacMirrorHostGgufToHdms(o) {
|
|
const hostPath = String(o.hostPath || '')
|
|
if (!hostPath || !o.vfs || typeof o.vfs.writeFile !== 'function') {
|
|
return { ok: false, reason: 'missing_args' }
|
|
}
|
|
const base =
|
|
String(o.guestName || '')
|
|
.replace(/^\/+/, '')
|
|
.split('/')
|
|
.pop() || path.basename(hostPath)
|
|
const guestPath = bareOsQvacModelsGuestPath(base)
|
|
let hostSize = 0
|
|
try {
|
|
hostSize = fs.statSync(hostPath).size
|
|
} catch {
|
|
return { ok: false, reason: 'host_missing' }
|
|
}
|
|
if (!(hostSize > 0)) return { ok: false, reason: 'empty_host' }
|
|
const guest = await bareOsQvacGuestGgufStat(o.vfs, guestPath)
|
|
if (guest && guest.size === hostSize) {
|
|
return { ok: true, guestPath, skipped: true, reason: 'size_match' }
|
|
}
|
|
try {
|
|
const buf = fs.readFileSync(hostPath)
|
|
if (!buf || !buf.byteLength) return { ok: false, reason: 'empty_host' }
|
|
await o.vfs.writeFile(guestPath, buf)
|
|
return { ok: true, guestPath }
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e
|
|
? String(/** @type {{ message: unknown }} */ (e).message)
|
|
: String(e)
|
|
return { ok: false, reason: msg }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List GGUF basenames under /mnt/models (best-effort).
|
|
* @param {Record<string, any>} vfs
|
|
* @returns {Promise<string[]>}
|
|
*/
|
|
export async function bareOsQvacListHdmsGgufs(vfs) {
|
|
if (!vfs || typeof vfs.readdir !== 'function') return []
|
|
try {
|
|
const entries = await vfs.readdir(BARE_OS_QVAC_MODELS_GUEST_ROOT)
|
|
if (!Array.isArray(entries)) return []
|
|
return entries
|
|
.map((e) => {
|
|
if (typeof e === 'string') return e
|
|
if (e && typeof e === 'object' && 'name' in e) return String(e.name)
|
|
return ''
|
|
})
|
|
.filter((n) => /\.gguf$/i.test(n))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List GGUF basenames in host cache dir.
|
|
* @param {string} [hostCacheDir]
|
|
* @returns {string[]}
|
|
*/
|
|
export function bareOsQvacListHostGgufs(hostCacheDir) {
|
|
const dir = bareOsQvacModelsHostCacheDir(hostCacheDir)
|
|
try {
|
|
return fs
|
|
.readdirSync(dir)
|
|
.filter((n) => typeof n === 'string' && /\.gguf$/i.test(n))
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find a guest GGUF matching model id / cache basename.
|
|
* @param {Record<string, any>} vfs
|
|
* @param {string} hintName e.g. Qwen3-0.6B-Q4_0.gguf or hashed cache name
|
|
* @returns {Promise<string | null>} guest absolute path
|
|
*/
|
|
export async function bareOsQvacFindHdmsGguf(vfs, hintName) {
|
|
const names = await bareOsQvacListHdmsGgufs(vfs)
|
|
if (!names.length) return null
|
|
const hint = String(hintName || '')
|
|
const stripped = bareOsQvacModelsStripHashPrefix(hint)
|
|
for (const n of names) {
|
|
if (n === hint || n === stripped) return bareOsQvacModelsGuestPath(n)
|
|
if (stripped && n.endsWith(stripped)) return bareOsQvacModelsGuestPath(n)
|
|
if (hint && n.includes(stripped)) return bareOsQvacModelsGuestPath(n)
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Copy any HDMS GGUFs missing/mismatched on the host cache (before loadModel).
|
|
* @param {{
|
|
* vfs: Record<string, any>,
|
|
* hostCacheDir?: string,
|
|
* drive?: { createReadStream?: Function, get?: Function } | null
|
|
* }} o
|
|
* @returns {Promise<{ materialized: string[], skipped: string[], errors: string[] }>}
|
|
*/
|
|
export async function bareOsQvacMaterializeHdmsModelsToHost(o) {
|
|
/** @type {string[]} */
|
|
const materialized = []
|
|
/** @type {string[]} */
|
|
const skipped = []
|
|
/** @type {string[]} */
|
|
const errors = []
|
|
const cacheDir = bareOsQvacModelsHostCacheDir(o.hostCacheDir)
|
|
try {
|
|
fs.mkdirSync(cacheDir, { recursive: true })
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
const names = await bareOsQvacListHdmsGgufs(o.vfs)
|
|
for (const name of names) {
|
|
const guestPath = bareOsQvacModelsGuestPath(name)
|
|
const hostPath = bareOsQvacModelsHostPath(name, cacheDir)
|
|
const r = await bareOsQvacMaterializeGgufToHost({
|
|
vfs: o.vfs,
|
|
guestPath,
|
|
hostPath,
|
|
drive: o.drive || null,
|
|
drivePath: '/' + name
|
|
})
|
|
if (r.ok && r.skipped) skipped.push(name)
|
|
else if (r.ok) materialized.push(name)
|
|
else errors.push(name + ': ' + (r.reason || 'fail'))
|
|
}
|
|
return { materialized, skipped, errors }
|
|
}
|
|
|
|
/**
|
|
* Mirror host-cache GGUFs into HDMS after download/load (best-effort).
|
|
* @param {{
|
|
* vfs: Record<string, any>,
|
|
* hostCacheDir?: string
|
|
* }} o
|
|
* @returns {Promise<{ mirrored: string[], skipped: string[], errors: string[] }>}
|
|
*/
|
|
export async function bareOsQvacMirrorHostModelsToHdms(o) {
|
|
/** @type {string[]} */
|
|
const mirrored = []
|
|
/** @type {string[]} */
|
|
const skipped = []
|
|
/** @type {string[]} */
|
|
const errors = []
|
|
const names = bareOsQvacListHostGgufs(o.hostCacheDir)
|
|
for (const name of names) {
|
|
const hostPath = bareOsQvacModelsHostPath(name, o.hostCacheDir)
|
|
const r = await bareOsQvacMirrorHostGgufToHdms({
|
|
vfs: o.vfs,
|
|
hostPath,
|
|
guestName: name
|
|
})
|
|
if (r.ok && r.skipped) skipped.push(name)
|
|
else if (r.ok) mirrored.push(name)
|
|
else errors.push(name + ': ' + (r.reason || 'fail'))
|
|
}
|
|
return { mirrored, skipped, errors }
|
|
}
|