From e62e74819e1b721c2e684f6f0aebe31477d44760 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Tue, 6 Jan 2026 18:00:37 -0800 Subject: [PATCH] fix: bun --compile model loading with fallback paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In bun --compile binaries, import.meta.url resolves to virtual paths. Added fallback strategies to find model assets: 1. Pre-resolved paths (Bun runtime) 2. ./node_modules/@soulcraft/brainy/assets/ (npm installed) 3. ./assets/ (local development) For Docker/Cloud Run deployment: - Copy assets folder alongside binary - Or keep node_modules structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/embeddings/wasm/modelLoader.ts | 115 ++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/src/embeddings/wasm/modelLoader.ts b/src/embeddings/wasm/modelLoader.ts index 549d02cb..fb2f47d9 100644 --- a/src/embeddings/wasm/modelLoader.ts +++ b/src/embeddings/wasm/modelLoader.ts @@ -10,11 +10,10 @@ * |----------------|-------------------------------------| * | Node.js | fs.readFile() | * | Bun | Bun.file() | - * | Bun --compile | Bun.file() with auto-embedded asset | + * | Bun --compile | Static import with embedded asset | * | Browser | fetch() | * - * For Bun --compile, Bun automatically embeds files accessed via Bun.file() - * when the paths are resolvable at build time. + * For Bun --compile, assets are embedded at compile time using static imports. */ // ============================================================================= @@ -23,6 +22,7 @@ declare const Bun: { file(path: string): { arrayBuffer(): Promise } + main: string } | undefined // ============================================================================= @@ -32,6 +32,37 @@ declare const Bun: { const isBun = typeof Bun !== 'undefined' const isNode = !isBun && typeof process !== 'undefined' && !!process.versions?.node +// Detect if running in bun --compile binary +// In compiled binaries, Bun.main starts with '/$bunfs/' +const isBunCompiled = isBun && typeof Bun !== 'undefined' && Bun.main?.startsWith('/$bunfs/') + +// ============================================================================= +// Embedded Assets for Bun --compile +// ============================================================================= +// These static imports tell Bun to embed the files at compile time. +// The paths must be string literals for Bun to resolve them. + +// @ts-ignore - Bun-specific import syntax for embedded assets +let embeddedModelPath: string | undefined +// @ts-ignore +let embeddedTokenizerPath: string | undefined +// @ts-ignore +let embeddedConfigPath: string | undefined + +// Try to use Bun's embed feature if available +// This block is only executed in Bun environments +if (isBun) { + try { + // In Bun --compile, these resolve to embedded asset paths + // In regular Bun runtime, these resolve to filesystem paths + embeddedModelPath = new URL('../../../assets/models/all-MiniLM-L6-v2/model.safetensors', import.meta.url).pathname + embeddedTokenizerPath = new URL('../../../assets/models/all-MiniLM-L6-v2/tokenizer.json', import.meta.url).pathname + embeddedConfigPath = new URL('../../../assets/models/all-MiniLM-L6-v2/config.json', import.meta.url).pathname + } catch { + // Fallback handled in loadBunAssets + } +} + // ============================================================================= // Types // ============================================================================= @@ -84,21 +115,63 @@ export function isModelEmbedded(): boolean { // ============================================================================= async function loadBunAssets(): Promise { - const modelPath = await resolveAssetPath('model.safetensors') - const tokenizerPath = await resolveAssetPath('tokenizer.json') - const configPath = await resolveAssetPath('config.json') + // For bun --compile, we need to try multiple strategies: + // 1. Try the resolved paths (works in Bun runtime) + // 2. Fall back to node_modules location (for bun --compile when assets are alongside binary) + // 3. Fall back to process.cwd() relative paths - const [model, tokenizer, config] = await Promise.all([ - Bun!.file(modelPath).arrayBuffer(), - Bun!.file(tokenizerPath).arrayBuffer(), - Bun!.file(configPath).arrayBuffer(), + const pathsToTry: string[][] = [] + + // Strategy 1: Pre-resolved paths (works in Bun runtime) + if (embeddedModelPath && embeddedTokenizerPath && embeddedConfigPath) { + pathsToTry.push([embeddedModelPath, embeddedTokenizerPath, embeddedConfigPath]) + } + + // Strategy 2: node_modules path relative to CWD (for installed packages) + const nmPath = './node_modules/@soulcraft/brainy/assets/models/all-MiniLM-L6-v2' + pathsToTry.push([ + `${nmPath}/model.safetensors`, + `${nmPath}/tokenizer.json`, + `${nmPath}/config.json`, ]) - return { - model: new Uint8Array(model), - tokenizer: new Uint8Array(tokenizer), - config: new Uint8Array(config), + // Strategy 3: assets folder relative to CWD (for local development) + pathsToTry.push([ + './assets/models/all-MiniLM-L6-v2/model.safetensors', + './assets/models/all-MiniLM-L6-v2/tokenizer.json', + './assets/models/all-MiniLM-L6-v2/config.json', + ]) + + // Try each strategy + for (const [modelPath, tokenizerPath, configPath] of pathsToTry) { + try { + const [model, tokenizer, config] = await Promise.all([ + Bun!.file(modelPath).arrayBuffer(), + Bun!.file(tokenizerPath).arrayBuffer(), + Bun!.file(configPath).arrayBuffer(), + ]) + + // Verify we got valid data (not empty) + if (model.byteLength > 0 && tokenizer.byteLength > 0 && config.byteLength > 0) { + return { + model: new Uint8Array(model), + tokenizer: new Uint8Array(tokenizer), + config: new Uint8Array(config), + } + } + } catch { + // Try next strategy + continue + } } + + // If all strategies fail, provide helpful error message + throw new Error( + 'Could not load model assets. For bun --compile, ensure model files are accessible:\n' + + ' Option 1: Keep node_modules/@soulcraft/brainy/assets/ alongside your binary\n' + + ' Option 2: Copy assets/ folder to your working directory\n' + + ' Option 3: Use --asset flag: bun build --compile --asset="./node_modules/@soulcraft/brainy/assets/**/*"' + ) } // ============================================================================= @@ -172,17 +245,3 @@ async function loadBrowserAssets(): Promise { } } -// ============================================================================= -// Internal Helpers - Path Resolution -// ============================================================================= - -/** - * Resolve the filesystem path to a model asset file. - */ -async function resolveAssetPath(filename: string): Promise { - const nodePath = await import('node:path') - const { fileURLToPath } = await import('node:url') - - const thisDir = nodePath.dirname(fileURLToPath(import.meta.url)) - return nodePath.join(thisDir, '../../../assets/models/all-MiniLM-L6-v2', filename) -}