Skip to content

Commit 7941d0f

Browse files
infra: align embed-llamacpp integration model helpers with diffusion-cpp
Make the integration-test model tooling uniform with diffusion-cpp: - ensureModel now takes an object ({ modelName, modelDir, manifest, download }), matching diffusion-cpp/llm-llamacpp; update all callsites - log the "no sha256/bytes pinned — integrity check SKIPPED" hint pointing at scripts/generate-model-manifest.mjs, same as diffusion - rename the pinning script to scripts/generate-model-manifest.mjs (verbatim diffusion copy) and the unit suite to test/unit/ensure-model-integrity.test.js - manifest integrity stays null until pinned in CI (HF_TOKEN + network); the suite tolerates null now and enforces shape once pinned Ref: QVAC-21937 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e2d56fa commit 7941d0f

5 files changed

Lines changed: 160 additions & 62 deletions

File tree

packages/embed-llamacpp/scripts/pin-model-manifest.mjs renamed to packages/embed-llamacpp/scripts/generate-model-manifest.mjs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@
44
// Populates sha256 + bytes in test/integration/models.manifest.json by
55
// downloading each pinned URL FRESH into a temp directory and hashing it.
66
//
7-
// This is the integration-test model cache manifest (single source of truth for
8-
// URLs + integrity, and the .github/actions/cache-models cache key).
9-
//
107
// IMPORTANT (integrity provenance): shas are computed from a clean download of
118
// the pinned URL, never from packages/embed-llamacpp/test/model (which may be a
129
// stale or poisoned restored cache). Run this on a machine/CI with network and
1310
// an HF_TOKEN for gated repos.
1411
//
1512
// Usage:
16-
// HF_TOKEN=hf_xxx node scripts/pin-model-manifest.mjs [--only <modelName>] [--force]
13+
// HF_TOKEN=hf_xxx node scripts/generate-model-manifest.mjs [--only <modelName>] [--force]
1714
//
1815
// Flags:
1916
// --only <name> Only (re)generate the named model entry.
@@ -31,7 +28,7 @@ import https from 'node:https'
3128
const __dirname = dirname(fileURLToPath(import.meta.url))
3229
const MANIFEST_PATH = resolve(__dirname, '../test/integration/models.manifest.json')
3330

34-
function parseArgs (argv) {
31+
function parseArgs(argv) {
3532
const args = { only: null, force: false }
3633
for (let i = 0; i < argv.length; i++) {
3734
if (argv[i] === '--only') args.only = argv[++i]
@@ -40,15 +37,15 @@ function parseArgs (argv) {
4037
return args
4138
}
4239

43-
function authHeaders (url) {
40+
function authHeaders(url) {
4441
const headers = { 'user-agent': 'qvac-manifest-generator' }
4542
if (url.includes('huggingface.co') && process.env.HF_TOKEN) {
4643
headers.authorization = `Bearer ${process.env.HF_TOKEN}`
4744
}
4845
return headers
4946
}
5047

51-
function download (url, dest, redirectsLeft = 10) {
48+
function download(url, dest, redirectsLeft = 10) {
5249
return new Promise((resolve, reject) => {
5350
const req = https.get(url, { headers: authHeaders(url) }, (res) => {
5451
if ([301, 302, 307, 308].includes(res.statusCode)) {
@@ -69,7 +66,7 @@ function download (url, dest, redirectsLeft = 10) {
6966

7067
// Hash is streamed because fs.readFile is hard-capped at 2 GiB
7168
// (kIoMaxLength) and most of these model files are larger than that.
72-
function sha256Stream (filePath) {
69+
function sha256Stream(filePath) {
7370
return new Promise((resolve, reject) => {
7471
const hash = createHash('sha256')
7572
const stream = createReadStream(filePath)
@@ -79,13 +76,13 @@ function sha256Stream (filePath) {
7976
})
8077
}
8178

82-
async function sha256AndSize (filePath) {
79+
async function sha256AndSize(filePath) {
8380
const sha256 = await sha256Stream(filePath)
8481
const { size } = await stat(filePath)
8582
return { sha256, bytes: size }
8683
}
8784

88-
async function main () {
85+
async function main() {
8986
const args = parseArgs(process.argv.slice(2))
9087
const manifest = JSON.parse(await readFile(MANIFEST_PATH, 'utf8'))
9188
const tmp = await mkdtemp(join(tmpdir(), 'qvac-manifest-'))

packages/embed-llamacpp/test/integration/addon.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ const DEVICE_API = isDarwinX64 || isLinuxArm64 ? 'cpu' : 'gpu'
747747
const MODEL_NAME_API = getModelConfigs()[0]?.modelName ?? 'embeddinggemma-300M-Q8_0.gguf'
748748

749749
async function setupModelApiBehavior(t) {
750-
await ensureModel(MODEL_NAME_API)
750+
await ensureModel({ modelName: MODEL_NAME_API })
751751
const { inference } = await createEmbeddingsTestInstance(
752752
t,
753753
MODEL_NAME_API,

packages/embed-llamacpp/test/integration/multi-gpu.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async function runMultiGpuTest(t, extraConfig, assertDevices) {
3737
return
3838
}
3939

40-
const [modelName, dirPath] = await ensureModel(MODEL.name)
40+
const [modelName, dirPath] = await ensureModel({ modelName: MODEL.name })
4141
const modelPath = path.join(dirPath, modelName)
4242
const specLogger = attachSpecLogger({ forwardToConsole: true })
4343

packages/embed-llamacpp/test/integration/utils.js

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const os = require('bare-os')
88
// Lazily loaded: requiring index.js pulls in the native binding, which isn't
99
// needed for the model download/integrity helpers and lets those run in unit
1010
// tests without a compiled addon.
11-
function getGGMLBert () {
11+
function getGGMLBert() {
1212
return require('../../index.js')
1313
}
1414

@@ -217,7 +217,7 @@ let _manifestCache
217217
// Loads and caches the model manifest (single source of truth for model URLs +
218218
// sha256/bytes integrity). Returns null when absent so callers fall back to the
219219
// URL declared in MODEL_CONFIGS.
220-
function loadManifest (manifestPath = DEFAULT_MANIFEST_PATH) {
220+
function loadManifest(manifestPath = DEFAULT_MANIFEST_PATH) {
221221
if (manifestPath === DEFAULT_MANIFEST_PATH && _manifestCache !== undefined) {
222222
return _manifestCache
223223
}
@@ -231,13 +231,13 @@ function loadManifest (manifestPath = DEFAULT_MANIFEST_PATH) {
231231
return parsed
232232
}
233233

234-
function resolveModelEntry (modelName, { manifest } = {}) {
234+
function resolveModelEntry(modelName, { manifest } = {}) {
235235
const m = manifest !== undefined ? manifest : loadManifest()
236236
if (!m || !m.models) return null
237237
return m.models[modelName] || null
238238
}
239239

240-
function entryHasIntegrity (entry) {
240+
function entryHasIntegrity(entry) {
241241
if (!entry) return false
242242
const hasSha = typeof entry.sha256 === 'string' && entry.sha256.length === 64
243243
const hasBytes = Number.isInteger(entry.bytes)
@@ -246,12 +246,20 @@ function entryHasIntegrity (entry) {
246246

247247
// Streaming sha256 via bare-crypto. Returns null when bare-crypto is
248248
// unavailable so callers can degrade rather than fail.
249-
async function sha256File (filePath) {
249+
async function sha256File(filePath) {
250250
let crypto
251-
try { crypto = require('bare-crypto') } catch (_) { return null }
251+
try {
252+
crypto = require('bare-crypto')
253+
} catch (_) {
254+
return null
255+
}
252256
return await new Promise((resolve, reject) => {
253257
let hash
254-
try { hash = crypto.createHash('sha256') } catch (_) { return resolve(null) }
258+
try {
259+
hash = crypto.createHash('sha256')
260+
} catch (_) {
261+
return resolve(null)
262+
}
255263
const stream = fs.createReadStream(filePath)
256264
stream.on('data', (chunk) => hash.update(chunk))
257265
stream.on('error', reject)
@@ -262,7 +270,7 @@ async function sha256File (filePath) {
262270
// Verifies a model file against a manifest entry. Byte-length is checked first
263271
// (cheap) so a size mismatch fails fast before hashing a multi-GB file.
264272
// { ok: true } when it passes (or when no integrity value is pinned yet).
265-
async function verifyModelFile (filePath, entry) {
273+
async function verifyModelFile(filePath, entry) {
266274
let stats
267275
try {
268276
stats = fs.statSync(filePath)
@@ -279,7 +287,9 @@ async function verifyModelFile (filePath, entry) {
279287
if (hasSha) {
280288
const got = await sha256File(filePath)
281289
if (got === null) {
282-
console.log('[download] WARNING: sha256 pinned for this model but bare-crypto is unavailable — integrity NOT verified')
290+
console.log(
291+
'[download] WARNING: sha256 pinned for this model but bare-crypto is unavailable — integrity NOT verified'
292+
)
283293
return { ok: true, unverified: true }
284294
}
285295
if (got !== entry.sha256.toLowerCase()) {
@@ -293,8 +303,12 @@ async function verifyModelFile (filePath, entry) {
293303

294304
// Counts real download attempts so warm-vs-cold behaviour is unit-testable.
295305
let _downloadCount = 0
296-
function getDownloadCount () { return _downloadCount }
297-
function resetDownloadCount () { _downloadCount = 0 }
306+
function getDownloadCount() {
307+
return _downloadCount
308+
}
309+
function resetDownloadCount() {
310+
_downloadCount = 0
311+
}
298312

299313
/**
300314
* Model configurations for testing
@@ -356,10 +370,12 @@ function prestagedModelDir(modelName) {
356370

357371
/**
358372
* Ensures the model file exists, downloading it if necessary
359-
* @param {string} modelName - The model name to ensure
373+
* @param {Object} opts
374+
* @param {string} opts.modelName - The model name to ensure
360375
* @returns {Promise<[string, string]>} Returns [modelName, modelDir]
361376
*/
362-
async function ensureModel (modelName, { modelDir = path.resolve(__dirname, '../model'), manifest, download } = {}) {
377+
async function ensureModel({ modelName, modelDir, manifest, download } = {}) {
378+
const dir = modelDir || path.resolve(__dirname, '../model')
363379
const modelConfig = getModelConfig(modelName)
364380

365381
// Model URL + sha256/bytes come from models.manifest.json (by modelName); the
@@ -373,7 +389,7 @@ async function ensureModel (modelName, { modelDir = path.resolve(__dirname, '../
373389
throw new Error(`Unknown model: ${modelName}`)
374390
}
375391

376-
const modelPath = path.join(modelDir, modelName)
392+
const modelPath = path.join(dir, modelName)
377393
const doDownload = download || downloadFileWithRetries
378394
const urls = hasManifestUrls ? entry.urls : [modelConfig.downloadUrl]
379395
const hasIntegrity = entryHasIntegrity(entry)
@@ -383,17 +399,27 @@ async function ensureModel (modelName, { modelDir = path.resolve(__dirname, '../
383399
const res = await verifyModelFile(modelPath, entry)
384400
if (res.ok) {
385401
console.log(`[download] ${modelName}: cached copy verified, skipping download`)
386-
return [modelName, modelDir]
402+
return [modelName, dir]
387403
}
388-
console.log(`[download] ${modelName}: cached copy failed integrity (${res.reason}); deleting and re-downloading`)
389-
try { fs.unlinkSync(modelPath) } catch (_) {}
404+
console.log(
405+
`[download] ${modelName}: cached copy failed integrity (${res.reason}); deleting and re-downloading`
406+
)
407+
try {
408+
fs.unlinkSync(modelPath)
409+
} catch (_) {}
390410
} else {
391411
const stat = fs.statSync(modelPath)
392-
if (stat.size > 0) {
393-
return [modelName, modelDir]
412+
if (stat.size === 0) {
413+
console.log(`[download] Removing zero-byte cached file: ${modelName}`)
414+
fs.unlinkSync(modelPath)
415+
} else {
416+
if (entry) {
417+
console.log(
418+
`[download] ${modelName}: no sha256/bytes pinned in manifest — integrity check SKIPPED (run scripts/generate-model-manifest.mjs to pin)`
419+
)
420+
}
421+
return [modelName, dir]
394422
}
395-
console.log(`[download] Removing zero-byte cached file: ${modelName}`)
396-
fs.unlinkSync(modelPath)
397423
}
398424
}
399425

@@ -403,18 +429,18 @@ async function ensureModel (modelName, { modelDir = path.resolve(__dirname, '../
403429
// load() writes sibling files next to the model (e.g. openclCacheDir).
404430
const staged = prestagedModelDir(modelName)
405431
if (staged) {
406-
fs.mkdirSync(modelDir, { recursive: true })
432+
fs.mkdirSync(dir, { recursive: true })
407433
console.log(`[prestage] Using pre-staged model ${modelName} (copying into writable modelDir)`)
408434
fs.copyFileSync(path.join(staged, modelName), modelPath)
409435
const stat = fs.statSync(modelPath)
410436
if (stat.size === 0) {
411437
fs.unlinkSync(modelPath)
412438
throw new Error(`[prestage] copied model ${modelName} is empty`)
413439
}
414-
return [modelName, modelDir]
440+
return [modelName, dir]
415441
}
416442

417-
fs.mkdirSync(modelDir, { recursive: true })
443+
fs.mkdirSync(dir, { recursive: true })
418444
console.log(`[download] Downloading test model: ${modelName}...`)
419445
_downloadCount++
420446

@@ -423,15 +449,19 @@ async function ensureModel (modelName, { modelDir = path.resolve(__dirname, '../
423449
if (hasIntegrity) {
424450
const res = await verifyModelFile(modelPath, entry)
425451
if (!res.ok) {
426-
try { fs.unlinkSync(modelPath) } catch (_) {}
427-
throw new Error(`[download] ${modelName}: freshly downloaded file failed integrity: ${res.reason}`)
452+
try {
453+
fs.unlinkSync(modelPath)
454+
} catch (_) {}
455+
throw new Error(
456+
`[download] ${modelName}: freshly downloaded file failed integrity: ${res.reason}`
457+
)
428458
}
429459
}
430460

431461
const stat = fs.statSync(modelPath)
432462
console.log(`[download] Model ready: ${(stat.size / 1024 / 1024).toFixed(1)}MB`)
433463

434-
return [modelName, modelDir]
464+
return [modelName, dir]
435465
}
436466

437467
/**
@@ -471,7 +501,7 @@ async function createEmbeddingsTestInstance(
471501
gpuLayers = null,
472502
batchSize = '1024'
473503
) {
474-
const [, modelDir] = await ensureModel(modelName)
504+
const [, modelDir] = await ensureModel({ modelName })
475505
const modelPath = path.join(modelDir, modelName)
476506

477507
t.ok(fs.existsSync(modelPath), 'Model file should exist')

0 commit comments

Comments
 (0)