fix(mnemopi): forced fastembed onnxruntime override
- Forced the on-demand fastembed runtime manifest to override fastembed's archived onnxruntime-node 1.21.0 transitive dependency with the package's pinned 1.26.0 runtime.\n- Treated native dlopen failures from the direct fastembed import as recoverable so macOS ARM64 can fall back to the controlled runtime cache.\n- Added coverage for the runtime install plan and cache key policy.\n\nFixes #2920
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Forced the on-demand fastembed runtime install to override fastembed's archived `onnxruntime-node@1.21.0` transitive pin with Mnemopi's `onnxruntime-node@1.26.0` pin, fixing local embedding startup on macOS ARM64. ([#2920](https://github.com/can1357/oh-my-pi/issues/2920))
|
||||
|
||||
## [16.0.6] - 2026-06-18
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getFastembedRuntimeDir,
|
||||
installRuntimeModuleResolver,
|
||||
logger,
|
||||
type RuntimeInstallSpec,
|
||||
resolveRuntimeModule,
|
||||
} from "@oh-my-pi/pi-utils";
|
||||
import type * as Fastembed from "fastembed";
|
||||
@@ -12,13 +13,21 @@ import packageManifest from "../../package.json" with { type: "json" };
|
||||
|
||||
type FastembedModule = typeof Fastembed;
|
||||
|
||||
/** Runtime install inputs for the optional fastembed embedding stack. */
|
||||
export interface FastembedRuntimeInstallPlan {
|
||||
/** Cache directory key; changes when runtime resolution policy changes. */
|
||||
versionKey: string;
|
||||
/** Dependency graph written to the runtime cache package manifest. */
|
||||
install: RuntimeInstallSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* `fastembed` and `onnxruntime-node` are optional peers (~270MB of native
|
||||
* assets across platforms), never bundled and never installed eagerly. When
|
||||
* the direct import cannot resolve — bundled `dist/cli.js`, compiled binary,
|
||||
* or a consumer that skipped the optional peers — the pinned pair is
|
||||
* `bun install`ed into a per-version runtime cache on first use and loaded
|
||||
* from there (#2389).
|
||||
* a consumer that skipped the optional peers, or a native loader failure from
|
||||
* fastembed's nested ORT — the pinned pair is `bun install`ed into a
|
||||
* per-version runtime cache on first use and loaded from there (#2389, #2920).
|
||||
*
|
||||
* The pins live in `peerDependencies` as exact versions (not `catalog:`) so
|
||||
* this module reads concrete specs even when the workspace manifest is
|
||||
@@ -27,6 +36,16 @@ type FastembedModule = typeof Fastembed;
|
||||
const FASTEMBED_SPEC = packageManifest.peerDependencies.fastembed;
|
||||
const ORT_SPEC = packageManifest.peerDependencies["onnxruntime-node"];
|
||||
|
||||
/** Build the deterministic fastembed runtime install plan used by local embeddings. */
|
||||
export function fastembedRuntimeInstallPlan(): FastembedRuntimeInstallPlan {
|
||||
return {
|
||||
versionKey: `fastembed-${FASTEMBED_SPEC}_ort-${ORT_SPEC}_forced-ort`.replace(/[^A-Za-z0-9._-]/g, "_"),
|
||||
install: {
|
||||
dependencies: { fastembed: FASTEMBED_SPEC, "onnxruntime-node": ORT_SPEC },
|
||||
overrides: { "onnxruntime-common": ORT_SPEC, "onnxruntime-node": ORT_SPEC },
|
||||
},
|
||||
};
|
||||
}
|
||||
let fastembedLoad: Promise<FastembedModule> | null = null;
|
||||
|
||||
export function loadFastembed(): Promise<FastembedModule> {
|
||||
@@ -42,15 +61,15 @@ async function loadFastembedOnce(): Promise<FastembedModule> {
|
||||
// native addons and may be absent at runtime — a static import would load
|
||||
// the addon at module-init and crash every consumer without the peers.
|
||||
try {
|
||||
// Preload ORT 1.24 before fastembed's nested ORT 1.21 — only on Windows,
|
||||
// Preload the pinned ORT before fastembed's nested ORT — only on Windows,
|
||||
// where loading the older binding first triggers a DLL-reuse crash.
|
||||
if (process.platform === "win32") {
|
||||
await import("onnxruntime-node");
|
||||
}
|
||||
return await import("fastembed");
|
||||
} catch (error) {
|
||||
if (!isModuleResolutionError(error)) throw error;
|
||||
logger.debug("mnemopi: fastembed not resolvable, using on-demand runtime install", {
|
||||
if (!isRecoverableFastembedLoadError(error)) throw error;
|
||||
logger.debug("mnemopi: fastembed not loadable, using on-demand runtime install", {
|
||||
error: String(error),
|
||||
});
|
||||
return loadFromRuntimeInstall();
|
||||
@@ -58,10 +77,10 @@ async function loadFastembedOnce(): Promise<FastembedModule> {
|
||||
}
|
||||
|
||||
async function loadFromRuntimeInstall(): Promise<FastembedModule> {
|
||||
const versionKey = `fastembed-${FASTEMBED_SPEC}_ort-${ORT_SPEC}`.replace(/[^A-Za-z0-9._-]/g, "_");
|
||||
const plan = fastembedRuntimeInstallPlan();
|
||||
const runtimeDir = await ensureRuntimeInstalled({
|
||||
runtimeDir: path.join(getFastembedRuntimeDir(), versionKey),
|
||||
install: { dependencies: { fastembed: FASTEMBED_SPEC, "onnxruntime-node": ORT_SPEC } },
|
||||
runtimeDir: path.join(getFastembedRuntimeDir(), plan.versionKey),
|
||||
install: plan.install,
|
||||
probePackage: "fastembed",
|
||||
});
|
||||
const nodeModules = path.join(runtimeDir, "node_modules");
|
||||
@@ -80,10 +99,10 @@ async function loadFromRuntimeInstall(): Promise<FastembedModule> {
|
||||
return requireRuntime(entry) as FastembedModule;
|
||||
}
|
||||
|
||||
function isModuleResolutionError(error: unknown): boolean {
|
||||
function isRecoverableFastembedLoadError(error: unknown): boolean {
|
||||
if (typeof error !== "object" || error === null) return false;
|
||||
const { name, code, message } = error as { name?: unknown; code?: unknown; message?: unknown };
|
||||
if (name === "ResolveMessage") return true;
|
||||
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
|
||||
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ERR_DLOPEN_FAILED") return true;
|
||||
return typeof message === "string" && /cannot find (module|package)/i.test(message);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import rootManifest from "../../../package.json" with { type: "json" };
|
||||
import packageManifest from "../package.json" with { type: "json" };
|
||||
import { fastembedRuntimeInstallPlan } from "../src/core/fastembed-runtime";
|
||||
|
||||
// The fastembed/onnxruntime-node peers are pinned as exact versions (not
|
||||
// `catalog:`) because `core/fastembed-runtime.ts` reads them to `bun install`
|
||||
// the on-demand embedding runtime — including from bundles where the inlined
|
||||
// manifest would otherwise carry an uninstallable `catalog:` spec (#2389).
|
||||
// This pins the contract: the runtime install materializes exactly the
|
||||
// versions the workspace develops and tests against.
|
||||
// versions the workspace develops and tests against, and forces fastembed's
|
||||
// transitive ORT request away from its archived 1.21.0 pin (#2920).
|
||||
describe("fastembed runtime version pins", () => {
|
||||
const catalog = rootManifest.workspaces.catalog;
|
||||
|
||||
@@ -20,4 +22,17 @@ describe("fastembed runtime version pins", () => {
|
||||
expect(packageManifest.peerDependencies.fastembed).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
expect(packageManifest.peerDependencies["onnxruntime-node"]).toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
test("runtime install overrides fastembed's transitive onnxruntime pin", () => {
|
||||
const plan = fastembedRuntimeInstallPlan();
|
||||
expect(plan.install.dependencies).toEqual({
|
||||
fastembed: packageManifest.peerDependencies.fastembed,
|
||||
"onnxruntime-node": packageManifest.peerDependencies["onnxruntime-node"],
|
||||
});
|
||||
expect(plan.install.overrides).toEqual({
|
||||
"onnxruntime-common": packageManifest.peerDependencies["onnxruntime-node"],
|
||||
"onnxruntime-node": packageManifest.peerDependencies["onnxruntime-node"],
|
||||
});
|
||||
expect(plan.versionKey).toContain("forced-ort");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user