Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@
"typescript-eslint": "^8.51.0",
"vitest": "^4.0.18"
},
"overrides": {
"@huggingface/transformers>onnxruntime-node": "1.24.3"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use npm's nested-object syntax for the runtime override

When this checkout is used as the project root, npm rejects the pnpm-style > selector before resolving dependencies. With npm 11.4.2, both npm install --offline --ignore-scripts --package-lock=false --dry-run and npx --no-install codebase-context memory list fail with Override without name: @huggingface/transformers>onnxruntime-node. This blocks npm installation and local npx usage, contrary to the root AGENTS.md zero-infra requirement. The bundled npm package-json documentation specifies that “Keys can be nested to any arbitrary length”; represent this as an @huggingface/transformers object containing "onnxruntime-node": "1.24.3" instead.

AGENTS.md reference: AGENTS.md:L107-L107

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 npm installs fail

The new overrides key uses pnpm's parent>child syntax, which npm does not accept. Running npm install at the repository root treats this as an invalid package name and fails before installing dependencies. Use npm's nested form: "@huggingface/transformers": { "onnxruntime-node": "1.24.3" }. Keep the existing selector under pnpm.overrides.

},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
Expand Down
3 changes: 3 additions & 0 deletions src/core/reranker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -48,6 +49,8 @@ async function ensureModelLoaded(): Promise<void> {
if (initPromise) return initPromise;

initPromise = (async () => {
assertMinimumOnnxRuntimeNodeVersion();

const { AutoTokenizer, AutoModelForSequenceClassification, env } =
await import('@huggingface/transformers');

Expand Down
2 changes: 2 additions & 0 deletions src/embeddings/transformers.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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;
Expand Down
65 changes: 65 additions & 0 deletions src/utils/onnxruntime-version.ts
Original file line number Diff line number Diff line change
@@ -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.`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 OpenAI workaround still triggers reranking

The shared error recommends EMBEDDING_PROVIDER=openai to bypass local Transformers.js, but ensureModelLoaded also calls this guard for reranking. Search still runs the local reranker with OpenAI embeddings, so following that advice does not avoid this error on that path. Limit the advice to local embeddings, or explain that reranking also needs the runtime upgraded or disabled.

Suggested change
`or set EMBEDDING_PROVIDER=openai to bypass local Transformers.js.`
`or set EMBEDDING_PROVIDER=openai to bypass local embeddings. Local reranking still requires upgrading onnxruntime-node or disabling reranking.`

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

);
}
47 changes: 47 additions & 0 deletions tests/onnxruntime-version.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
overrides?: Record<string, string>;
pnpm?: { overrides?: Record<string, string> };
};

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();
Comment on lines +44 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Tests miss a disabled guard

The only test that calls assertMinimumOnnxRuntimeNodeVersion checks that it does not throw. It also passes when package lookup fails, because the guard silently returns in that case. None of the tests checks that an installed 1.21.0 is rejected, so a broken lookup or an unconditional return would leave this crash regression suite green. Add controlled fixtures or mocks that test an old runtime, a safe runtime, and a missing package separately.

});
});
Loading