diff --git a/package.json b/package.json index a855889..fa92a6a 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,9 @@ "typescript-eslint": "^8.51.0", "vitest": "^4.0.18" }, + "overrides": { + "@huggingface/transformers>onnxruntime-node": "1.24.3" + }, "pnpm": { "onlyBuiltDependencies": [ "esbuild", diff --git a/src/core/reranker.ts b/src/core/reranker.ts index 9dabeac..f579289 100644 --- a/src/core/reranker.ts +++ b/src/core/reranker.ts @@ -11,6 +11,7 @@ import type { SearchResult } from '../types/index.js'; import type { PreTrainedTokenizer } from '@huggingface/transformers'; import os from 'os'; +import { assertMinimumOnnxRuntimeNodeVersion } from '../utils/onnxruntime-version.js'; const DEFAULT_RERANKER_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2'; @@ -48,6 +49,8 @@ async function ensureModelLoaded(): Promise { if (initPromise) return initPromise; initPromise = (async () => { + assertMinimumOnnxRuntimeNodeVersion(); + const { AutoTokenizer, AutoModelForSequenceClassification, env } = await import('@huggingface/transformers'); diff --git a/src/embeddings/transformers.ts b/src/embeddings/transformers.ts index bb358d1..db0b2b4 100644 --- a/src/embeddings/transformers.ts +++ b/src/embeddings/transformers.ts @@ -1,6 +1,7 @@ import { EmbeddingProvider, DEFAULT_MODEL } from './types.js'; import type { FeatureExtractionPipeline } from '@huggingface/transformers'; import os from 'os'; +import { assertMinimumOnnxRuntimeNodeVersion } from '../utils/onnxruntime-version.js'; import { assertOptionalDependency } from '../utils/optional-dependency.js'; /** @@ -58,6 +59,7 @@ export class TransformersEmbeddingProvider implements EmbeddingProvider { if (this.initPromise) return this.initPromise; assertOptionalDependency('@huggingface/transformers', '^4.2.0'); + assertMinimumOnnxRuntimeNodeVersion(); this.initPromise = this._initialize(); return this.initPromise; diff --git a/src/utils/onnxruntime-version.ts b/src/utils/onnxruntime-version.ts new file mode 100644 index 0000000..2e0df77 --- /dev/null +++ b/src/utils/onnxruntime-version.ts @@ -0,0 +1,65 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +/** microsoft/onnxruntime#24579 — mutex crash on macOS in 1.21.x and earlier. */ +export const MIN_ONNXRUNTIME_NODE_VERSION = '1.24.0'; + +const resolve = createRequire(import.meta.url).resolve; + +export function parseSemverTriple(version: string): [number, number, number] { + const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim()); + if (!match) { + return [0, 0, 0]; + } + return [Number(match[1]), Number(match[2]), Number(match[3])]; +} + +export function isSemverAtLeast(actual: string, minimum: string): boolean { + const [aMajor, aMinor, aPatch] = parseSemverTriple(actual); + const [mMajor, mMinor, mPatch] = parseSemverTriple(minimum); + if (aMajor !== mMajor) { + return aMajor > mMajor; + } + if (aMinor !== mMinor) { + return aMinor > mMinor; + } + return aPatch >= mPatch; +} + +function readInstalledOnnxRuntimeVersion(): string | null { + try { + const transformersPkg = resolve('@huggingface/transformers/package.json'); + const onnxPkg = resolve('onnxruntime-node/package.json', { + paths: [path.dirname(transformersPkg)] + }); + const pkg = JSON.parse(readFileSync(onnxPkg, 'utf8')) as { version?: string }; + return typeof pkg.version === 'string' ? pkg.version : null; + } catch { + try { + const onnxPkg = resolve('onnxruntime-node/package.json'); + const pkg = JSON.parse(readFileSync(onnxPkg, 'utf8')) as { version?: string }; + return typeof pkg.version === 'string' ? pkg.version : null; + } catch { + return null; + } + } +} + +/** Fail fast before ONNX embedding/rerank work when the native runtime is too old. */ +export function assertMinimumOnnxRuntimeNodeVersion(minimum = MIN_ONNXRUNTIME_NODE_VERSION): void { + const installed = readInstalledOnnxRuntimeVersion(); + if (!installed) { + return; + } + + if (isSemverAtLeast(installed, minimum)) { + return; + } + + throw new Error( + `onnxruntime-node@${installed} can crash on macOS during embedding (mutex lock failed; see microsoft/onnxruntime#24579). ` + + `Install @huggingface/transformers@^4.2.0 so onnxruntime-node resolves to >=${minimum}, ` + + `or set EMBEDDING_PROVIDER=openai to bypass local Transformers.js.` + ); +} diff --git a/tests/onnxruntime-version.test.ts b/tests/onnxruntime-version.test.ts new file mode 100644 index 0000000..f3ae407 --- /dev/null +++ b/tests/onnxruntime-version.test.ts @@ -0,0 +1,47 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +import { + MIN_ONNXRUNTIME_NODE_VERSION, + assertMinimumOnnxRuntimeNodeVersion, + isSemverAtLeast, + parseSemverTriple +} from '../src/utils/onnxruntime-version.js'; + +const root = resolve(import.meta.dirname, '..'); + +type PackageJson = { + peerDependencies?: Record; + overrides?: Record; + pnpm?: { overrides?: Record }; +}; + +describe('onnxruntime-node macOS mutex regression (#68)', () => { + const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as PackageJson; + const lockfile = readFileSync(resolve(root, 'pnpm-lock.yaml'), 'utf8'); + + it('requires Transformers.js v4 peers (ships onnxruntime-node 1.24.x)', () => { + expect(packageJson.peerDependencies?.['@huggingface/transformers']).toBe('^4.2.0'); + }); + + it('pins onnxruntime-node 1.24.3 for npm and pnpm installs', () => { + expect(packageJson.overrides?.['@huggingface/transformers>onnxruntime-node']).toBe('1.24.3'); + expect(packageJson.pnpm?.overrides?.['@huggingface/transformers>onnxruntime-node']).toBe('1.24.3'); + }); + + it('locks onnxruntime-node 1.24.3 in pnpm-lock.yaml', () => { + expect(lockfile).toContain("'@huggingface/transformers>onnxruntime-node': 1.24.3"); + expect(lockfile).toMatch(/onnxruntime-node@1\.24\.3/); + }); + + it('compares semver triples for the minimum safe runtime', () => { + expect(parseSemverTriple('1.21.0')).toEqual([1, 21, 0]); + expect(isSemverAtLeast('1.24.3', MIN_ONNXRUNTIME_NODE_VERSION)).toBe(true); + expect(isSemverAtLeast('1.21.0', MIN_ONNXRUNTIME_NODE_VERSION)).toBe(false); + }); + + it('accepts onnxruntime-node 1.24.x when installed with Transformers.js', () => { + expect(() => assertMinimumOnnxRuntimeNodeVersion()).not.toThrow(); + }); +});