Orginize
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* 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 './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 }
|
||||
}
|
||||
Reference in New Issue
Block a user