feat(deps): enabled fastembed and onnxruntime peers to install on demand

- Moved `fastembed` and `onnxruntime-node` to optional peerDependencies.
- Fixed bundled installs that could not resolve `onnxruntime_binding.node`.
- Added shared `runtime-install` utilities for on-demand module resolution.
- Added tests for runtime-resolution parsing and exact peer-version checks.
This commit is contained in:
can1357
2026-06-12 14:18:58 +02:00
parent 9b643f601e
commit 9845ba1861
16 changed files with 641 additions and 294 deletions
+10 -2
View File
@@ -144,13 +144,21 @@
"@oh-my-pi/pi-ai": "catalog:",
"@oh-my-pi/pi-catalog": "catalog:",
"@oh-my-pi/pi-utils": "catalog:",
"fastembed": "catalog:",
"lru-cache": "catalog:",
"onnxruntime-node": "catalog:",
},
"devDependencies": {
"@types/bun": "catalog:",
"fastembed": "catalog:",
"onnxruntime-node": "catalog:",
},
"peerDependencies": {
"fastembed": "2.1.0",
"onnxruntime-node": "1.24.3",
},
"optionalPeers": [
"fastembed",
"onnxruntime-node",
],
},
"packages/natives": {
"name": "@oh-my-pi/pi-natives",
+2 -3
View File
@@ -10,13 +10,12 @@
- Changed collab links so full links with a write token grant mutation rights while links without a token now join as read-only
- `/collab` now prints a join hint with both link forms: the compact `omp join` link for terminals and a click-to-join browser deep link (`https://<relay-host>/#<link>`, displayed scheme-less, OSC 8-linked) — the relay serves the collab web client at `/`, and the room id + key ride in the URL fragment, so they never appear in any HTTP request. `/join`, `omp join`, and the web connect screen accept either form
- npm installs no longer download fastembed's ~270MB ONNX native dependency tree eagerly: `fastembed` and `onnxruntime-node` are external to the bundle and optional peers of `@oh-my-pi/pi-mnemopi`, fetched on demand only when Mnemopi local embeddings are first used
### Fixed
- Fixed read-only collab sessions so prompting, interrupts, and other write actions are blocked with a read-only warning instead of being applied
### Fixed
- Fixed Mnemopi local embeddings in bundled and compiled installs failing with `Cannot find module '../bin/napi-v3/.../onnxruntime_binding.node'`: the Bun bundle inlined fastembed's loader so its relative native require resolved against `dist/cli.js`. `fastembed`/`onnxruntime-node` are no longer bundled; on first use Mnemopi `bun install`s the pinned pair into `~/.omp/cache/fastembed-runtime/<version-key>` and loads the binding from there ([#2389](https://github.com/can1357/oh-my-pi/issues/2389))
- Fixed the interactive Model scope startup banner so models without an explicit thinking level do not show `:undefined`, and entries that were scoped without a `:level` are no longer rendered with the global default thinking level (which `applyRootSessionOptions` pre-fills on the cycling array for Ctrl+P) ([#2385](https://github.com/can1357/oh-my-pi/issues/2385)).
## [15.11.8] - 2026-06-12
@@ -59,6 +59,10 @@ async function main(): Promise<void> {
`process.env.PI_TINY_TRANSFORMERS_VERSION=${JSON.stringify(transformersVersion)}`,
"--external",
"mupdf",
"--external",
"fastembed",
"--external",
"onnxruntime-node",
"--root",
".",
"./packages/coding-agent/src/cli.ts",
@@ -72,6 +72,10 @@ async function main(): Promise<void> {
"@oh-my-pi/pi-natives",
"--external",
"@huggingface/transformers",
"--external",
"fastembed",
"--external",
"onnxruntime-node",
"--define",
'process.env.PI_BUNDLED="true"',
"./src/cli.ts",
@@ -1,179 +0,0 @@
import * as fs from "node:fs";
import * as Module from "node:module";
import * as path from "node:path";
/**
* Bun's compiled-binary module resolver only finds `<pkg>/index.js` for bare
* specifiers loaded from the *real* filesystem — it ignores `main`/`exports`
* (issue #1763). The tiny-model Transformers.js runtime is `bun install`ed into
* a cache directory at runtime, and its graph (`@huggingface/transformers` →
* `onnxruntime-node` → `onnxruntime-common`, plus an eager `require("sharp")`)
* all point `main`/`exports` at nested files, so the stock resolver cannot load
* any of them. We patch `Module._resolveFilename` to resolve those bare
* specifiers against the cache ourselves, honoring `main`/`exports`.
*
* This module is filesystem-pure aside from {@link installRuntimeModuleResolver}
* mutating the `node:module` resolver, so the resolution logic is unit-testable
* without a compiled binary.
*/
/** Conditions honored when resolving an `exports` map for a CommonJS `require`. */
const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, default: true };
/** Extension probes appended to a `main`/`exports` target that lacks one. */
const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Walk a conditional `exports` target (string, array of fallbacks, or a
* condition object) and return the first relative path that matches a runtime
* condition in declaration order. Returns `null` when nothing applies (e.g.
* `import`-only targets).
*/
export function selectConditionalTarget(target: unknown): string | null {
if (typeof target === "string") return target;
if (Array.isArray(target)) {
for (const entry of target) {
const resolved = selectConditionalTarget(entry);
if (resolved) return resolved;
}
return null;
}
if (isRecord(target)) {
for (const condition in target) {
if (!RUNTIME_CONDITIONS[condition]) continue;
const resolved = selectConditionalTarget(target[condition]);
if (resolved) return resolved;
}
}
return null;
}
/** Resolve a relative target inside a package to a concrete file path, probing extensions and `index`. */
function resolveFileTarget(pkgDir: string, relative: string): string | null {
const base = path.join(pkgDir, relative);
const candidates = [base, ...RUNTIME_EXTENSIONS.map(ext => base + ext)];
for (const candidate of candidates) {
try {
const stat = fs.statSync(candidate);
if (stat.isFile()) return candidate;
if (stat.isDirectory()) {
const indexed = resolveFileTarget(candidate, "index");
if (indexed) return indexed;
}
} catch {
// missing candidate — keep probing
}
}
return null;
}
function resolveExportsEntry(
pkgDir: string,
exports: Record<string, unknown>,
subpath: string | undefined,
): string | null {
let subpathMap = false;
for (const key in exports) {
subpathMap = key === "." || key.startsWith("./");
break;
}
if (subpathMap) {
const key = subpath ? `./${subpath}` : ".";
if (!(key in exports)) return null;
const target = selectConditionalTarget(exports[key]);
return target ? resolveFileTarget(pkgDir, target) : null;
}
// A bare condition map only describes the package root, so a subpath
// request falls through to plain path joining at the call site.
if (subpath) return null;
const target = selectConditionalTarget(exports);
return target ? resolveFileTarget(pkgDir, target) : null;
}
/**
* Split a bare specifier into its package name and optional subpath, handling
* scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
*/
export function splitBareSpecifier(specifier: string): { packageName: string; subpath: string | undefined } {
const segments = specifier.split("/");
const take = specifier.startsWith("@") ? 2 : 1;
const packageName = segments.slice(0, take).join("/");
const subpath = segments.length > take ? segments.slice(take).join("/") : undefined;
return { packageName, subpath };
}
/**
* Resolve a bare specifier against an installed `node_modules` directory,
* honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
* Returns an absolute file path, or `null` when the package/entry is absent.
*/
export function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null {
const { packageName, subpath } = splitBareSpecifier(specifier);
const pkgDir = path.join(runtimeNodeModules, ...packageName.split("/"));
const manifest = readManifest(pkgDir);
if (!manifest) return subpath ? resolveFileTarget(pkgDir, subpath) : null;
const { exports } = manifest;
if (typeof exports === "string" || isRecord(exports)) {
const map = typeof exports === "string" ? { ".": exports } : exports;
const resolved = resolveExportsEntry(pkgDir, map, subpath);
if (resolved) return resolved;
}
if (subpath) return resolveFileTarget(pkgDir, subpath);
if (typeof manifest.main === "string") {
const resolved = resolveFileTarget(pkgDir, manifest.main);
if (resolved) return resolved;
}
return resolveFileTarget(pkgDir, "index.js");
}
function readManifest(pkgDir: string): Record<string, unknown> | null {
try {
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
interface ModuleResolver {
_resolveFilename(request: string, parent: unknown, isMain: boolean, options?: unknown): string;
}
const PATCHED = Symbol.for("omp.tiny.compiledRuntimeResolver");
interface ResolverOptions {
/** Absolute path to the runtime cache's `node_modules`. */
runtimeNodeModules: string;
/** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
stubs?: Record<string, string>;
}
/**
* Patch `node:module`'s resolver (idempotently) so bare specifiers that the
* stock compiled-binary resolver cannot find fall back to the runtime cache.
* Stock resolution is tried first, so this never changes behavior for modules
* that already resolve (the worker's own bundled imports, node builtins).
*/
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: ResolverOptions): void {
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean };
if (target[PATCHED]) return;
const original = target._resolveFilename.bind(target);
target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
try {
return original(request, parent, isMain, options);
} catch (error) {
const stub = stubs[request];
if (stub) return stub;
const resolved = resolveRuntimeModule(runtimeNodeModules, request);
if (resolved) return resolved;
throw error;
}
};
target[PATCHED] = true;
}
+17 -95
View File
@@ -1,4 +1,3 @@
import * as fs from "node:fs/promises";
import { createRequire } from "node:module";
import * as path from "node:path";
import type {
@@ -7,10 +6,16 @@ import type {
TextGenerationStringOutput,
StoppingCriteria as TransformersStoppingCriteria,
} from "@huggingface/transformers";
import { getTinyModelsCacheDir, isCompiledBinary, prompt } from "@oh-my-pi/pi-utils";
import {
ensureRuntimeInstalled,
getTinyModelsCacheDir,
installRuntimeModuleResolver,
isCompiledBinary,
prompt,
resolveRuntimeModule,
} from "@oh-my-pi/pi-utils";
import packageJson from "../../package.json" with { type: "json" };
import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with { type: "text" };
import { installRuntimeModuleResolver, resolveRuntimeModule } from "./compiled-runtime";
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
import {
@@ -31,8 +36,6 @@ const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
const sourceRequire = createRequire(import.meta.url);
const INSTALL_LOCK_ATTEMPTS = 240;
const INSTALL_LOCK_SLEEP_MS = 250;
const tinyModelDevicePreference = resolveTinyModelDevicePreference();
const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
@@ -97,10 +100,6 @@ function errorText(error: unknown): string {
return error instanceof Error ? (error.stack ?? error.message) : String(error);
}
function isErrnoCode(error: unknown, code: string): boolean {
return typeof error === "object" && error !== null && "code" in error && error.code === code;
}
function sendLog(
transport: TinyTitleTransport,
level: "debug" | "warn" | "error",
@@ -118,69 +117,6 @@ function getTinyTitleRuntimeDir(): string {
);
}
async function acquireInstallLock(runtimeDir: string): Promise<() => Promise<void>> {
const lockDir = `${runtimeDir}.lock`;
await fs.mkdir(path.dirname(lockDir), { recursive: true });
for (let attempt = 0; attempt < INSTALL_LOCK_ATTEMPTS; attempt++) {
try {
await fs.mkdir(lockDir);
return async () => {
await fs.rm(lockDir, { recursive: true, force: true });
};
} catch (error) {
if (!isErrnoCode(error, "EEXIST")) throw error;
await Bun.sleep(INSTALL_LOCK_SLEEP_MS);
}
}
throw new Error(`Timed out waiting for tiny title runtime install lock: ${lockDir}`);
}
async function isCompiledRuntimeInstalled(runtimeDir: string): Promise<boolean> {
return Bun.file(path.join(runtimeDir, "node_modules", "@huggingface", "transformers", "package.json")).exists();
}
async function writeRuntimeManifest(runtimeDir: string): Promise<void> {
await fs.mkdir(runtimeDir, { recursive: true });
await Bun.write(
path.join(runtimeDir, "package.json"),
`${JSON.stringify(
{
private: true,
type: "module",
dependencies: {
[TRANSFORMERS_PACKAGE]: getTransformersVersionSpec(),
},
trustedDependencies: ["onnxruntime-node"],
},
null,
"\t",
)}\n`,
);
}
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
if (!stream) return "";
return new Response(stream).text();
}
async function runRuntimeInstall(runtimeDir: string): Promise<void> {
const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
env: { ...Bun.env, BUN_BE_BUN: "1" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
proc.exited,
]);
if (exitCode === 0) return;
const output = `${stdout}\n${stderr}`.trim();
throw new Error(
`Failed to install tiny title runtime with ${process.execPath} install (exit ${exitCode}): ${output}`,
);
}
function sendRuntimeInstallProgress(
transport: TinyTitleTransport,
requestId: string,
@@ -198,28 +134,6 @@ function sendRuntimeInstallProgress(
});
}
async function ensureCompiledTransformersRuntime(
transport: TinyTitleTransport,
requestId: string,
modelKey: TinyLocalModelKey,
): Promise<string> {
const runtimeDir = getTinyTitleRuntimeDir();
if (await isCompiledRuntimeInstalled(runtimeDir)) return runtimeDir;
sendRuntimeInstallProgress(transport, requestId, modelKey, "initiate");
const releaseLock = await acquireInstallLock(runtimeDir);
try {
if (await isCompiledRuntimeInstalled(runtimeDir)) return runtimeDir;
await writeRuntimeManifest(runtimeDir);
sendRuntimeInstallProgress(transport, requestId, modelKey, "download");
await runRuntimeInstall(runtimeDir);
sendRuntimeInstallProgress(transport, requestId, modelKey, "done");
return runtimeDir;
} finally {
await releaseLock();
}
}
/**
* Prepare the freshly-installed compiled runtime for loading: stub `sharp`
* (the tiny models are text-generation only, so the native image pipeline is
@@ -252,7 +166,15 @@ async function loadTransformers(
if (transformersRuntime) return transformersRuntime;
transformersRuntime = (async () => {
if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as TransformersRuntime);
const runtimeDir = await ensureCompiledTransformersRuntime(transport, requestId, modelKey);
const runtimeDir = await ensureRuntimeInstalled({
runtimeDir: getTinyTitleRuntimeDir(),
install: {
dependencies: { [TRANSFORMERS_PACKAGE]: getTransformersVersionSpec() },
trustedDependencies: ["onnxruntime-node"],
},
probePackage: TRANSFORMERS_PACKAGE,
onPhase: phase => sendRuntimeInstallProgress(transport, requestId, modelKey, phase),
});
const entry = await prepareCompiledRuntime(runtimeDir);
const require_ = createRequire(entry);
return configureTransformers(require_(entry) as TransformersRuntime);
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Changed
- Moved `fastembed` and `onnxruntime-node` from `dependencies` to optional `peerDependencies` pinned to exact versions. When the peers are absent (bundled CLI, compiled binary, or installs that skip optional peers), the local embedding path `bun install`s the pinned pair into `~/.omp/cache/fastembed-runtime/<version-key>` on first use and loads fastembed from there — restoring local embeddings in bundled distributions and removing ~270MB of eager native downloads from default installs ([#2389](https://github.com/can1357/oh-my-pi/issues/2389))
## [15.11.4] - 2026-06-12
### Added
+16 -4
View File
@@ -42,12 +42,24 @@
"@oh-my-pi/pi-ai": "catalog:",
"@oh-my-pi/pi-catalog": "catalog:",
"@oh-my-pi/pi-utils": "catalog:",
"fastembed": "catalog:",
"lru-cache": "catalog:",
"onnxruntime-node": "catalog:"
"lru-cache": "catalog:"
},
"peerDependencies": {
"fastembed": "2.1.0",
"onnxruntime-node": "1.24.3"
},
"peerDependenciesMeta": {
"fastembed": {
"optional": true
},
"onnxruntime-node": {
"optional": true
}
},
"devDependencies": {
"@types/bun": "catalog:"
"@types/bun": "catalog:",
"fastembed": "catalog:",
"onnxruntime-node": "catalog:"
},
"engines": {
"bun": ">=1.3.14"
+2 -11
View File
@@ -12,6 +12,7 @@ import {
import type { EmbeddingModel } from "fastembed";
import { LRUCache } from "lru-cache/raw";
import packageJson from "../../package.json" with { type: "json" };
import { loadFastembed } from "./fastembed-runtime";
import {
type EmbeddingOutput,
getMnemopiRuntimeOptions,
@@ -61,17 +62,7 @@ const providerIds = new WeakMap<object, number>();
let nextProviderId = 1;
async function defaultLocalModelInitializer(options: LocalModelInitOptions): Promise<LocalEmbeddingModel> {
// Preload ORT 1.24 before fastembed's bundled ORT 1.21 — only on Windows,
// where loading the older binding first triggers a DLL-reuse crash. The 1.24
// line also has no darwin/x64 prebuilt, so importing it unconditionally breaks
// the darwin-x64 `bun build --compile` (Bun folds process.platform/arch and
// fails to resolve a binding that doesn't ship). The `win32` literal guard is
// statically foldable, so Bun dead-code-eliminates this import on every
// non-Windows target; fastembed loads its own ORT 1.21 binding there.
if (process.platform === "win32") {
await import("onnxruntime-node");
}
const { FlagEmbedding } = await import("fastembed");
const { FlagEmbedding } = await loadFastembed();
return FlagEmbedding.init(options);
}
@@ -0,0 +1,89 @@
import { createRequire } from "node:module";
import * as path from "node:path";
import {
ensureRuntimeInstalled,
getFastembedRuntimeDir,
installRuntimeModuleResolver,
logger,
resolveRuntimeModule,
} from "@oh-my-pi/pi-utils";
import type * as Fastembed from "fastembed";
import packageManifest from "../../package.json" with { type: "json" };
type FastembedModule = typeof Fastembed;
/**
* `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).
*
* The pins live in `peerDependencies` as exact versions (not `catalog:`) so
* this module reads concrete specs even when the workspace manifest is
* inlined into a bundle; a workspace test asserts they match the catalog.
*/
const FASTEMBED_SPEC = packageManifest.peerDependencies.fastembed;
const ORT_SPEC = packageManifest.peerDependencies["onnxruntime-node"];
let fastembedLoad: Promise<FastembedModule> | null = null;
export function loadFastembed(): Promise<FastembedModule> {
fastembedLoad ??= loadFastembedOnce().catch(error => {
fastembedLoad = null;
throw error;
});
return fastembedLoad;
}
async function loadFastembedOnce(): Promise<FastembedModule> {
// Dynamic imports: both packages are optional peers that eagerly load
// 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,
// 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", {
error: String(error),
});
return loadFromRuntimeInstall();
}
}
async function loadFromRuntimeInstall(): Promise<FastembedModule> {
const versionKey = `fastembed-${FASTEMBED_SPEC}_ort-${ORT_SPEC}`.replace(/[^A-Za-z0-9._-]/g, "_");
const runtimeDir = await ensureRuntimeInstalled({
runtimeDir: path.join(getFastembedRuntimeDir(), versionKey),
install: { dependencies: { fastembed: FASTEMBED_SPEC, "onnxruntime-node": ORT_SPEC } },
probePackage: "fastembed",
});
const nodeModules = path.join(runtimeDir, "node_modules");
// The compiled-binary resolver ignores `main`/`exports` for real-FS bare
// specifiers (Bun #1763); route the runtime graph's requires (fastembed →
// onnxruntime-node, @anush008/tokenizers → platform binding, …) through
// the runtime cache.
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules });
if (process.platform === "win32") {
const ortEntry = resolveRuntimeModule(nodeModules, "onnxruntime-node");
if (ortEntry) createRequire(ortEntry)(ortEntry);
}
const entry = resolveRuntimeModule(nodeModules, "fastembed");
if (!entry) throw new Error(`fastembed runtime install at ${runtimeDir} has no loadable entry`);
const requireRuntime = createRequire(entry);
return requireRuntime(entry) as FastembedModule;
}
function isModuleResolutionError(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;
return typeof message === "string" && /cannot find (module|package)/i.test(message);
}
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test";
import rootManifest from "../../../package.json" with { type: "json" };
import packageManifest from "../package.json" with { type: "json" };
// 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.
describe("fastembed runtime version pins", () => {
const catalog = rootManifest.workspaces.catalog;
test("peer pins match the workspace catalog", () => {
expect(packageManifest.peerDependencies.fastembed).toBe(catalog.fastembed);
expect(packageManifest.peerDependencies["onnxruntime-node"]).toBe(catalog["onnxruntime-node"]);
});
test("pins are exact installable versions, not catalog or range specs", () => {
expect(packageManifest.peerDependencies.fastembed).toMatch(/^\d+\.\d+\.\d+$/);
expect(packageManifest.peerDependencies["onnxruntime-node"]).toMatch(/^\d+\.\d+\.\d+$/);
});
});
+5
View File
@@ -2,6 +2,11 @@
## [Unreleased]
### Added
- Added `runtime-install`: shared on-demand runtime dependency support — `ensureRuntimeInstalled()` (locked, idempotent `bun install` of a pinned dependency set into a cache dir) and a multi-root `installRuntimeModuleResolver()`/`resolveRuntimeModule()` for loading those graphs inside compiled binaries (Bun #1763). Extracted from the coding-agent tiny-model worker; now also backs Mnemopi's on-demand fastembed runtime ([#2389](https://github.com/can1357/oh-my-pi/issues/2389))
- Added `getFastembedRuntimeDir()` (~/.omp/cache/fastembed-runtime) alongside `getFastembedCacheDir()`
## [15.11.4] - 2026-06-12
### Added
+5
View File
@@ -359,6 +359,11 @@ export function getFastembedCacheDir(): string {
return dirs.rootSubdir(path.join("cache", "fastembed"), "cache");
}
/** Get the on-demand fastembed runtime install root (~/.omp/cache/fastembed-runtime). */
export function getFastembedRuntimeDir(): string {
return dirs.rootSubdir(path.join("cache", "fastembed-runtime"), "cache");
}
/** Get the natives directory (~/.omp/natives). */
export function getNativesDir(): string {
return dirs.rootSubdir("natives", "cache");
+1
View File
@@ -19,6 +19,7 @@ export * as procmgr from "./procmgr";
export * as prompt from "./prompt";
export * as ptree from "./ptree";
export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree";
export * from "./runtime-install";
export * from "./sanitize-text";
export * from "./snowflake";
export * from "./stream";
+322
View File
@@ -0,0 +1,322 @@
import * as fs from "node:fs";
import * as fsp from "node:fs/promises";
import * as Module from "node:module";
import * as path from "node:path";
/**
* On-demand runtime dependency support for native-heavy optional packages
* (Transformers.js, fastembed) that are never bundled into the CLI or the
* compiled binary. Consumers `bun install` a pinned dependency set into a
* cache directory on first use ({@link ensureRuntimeInstalled}) and load the
* entrypoint via `createRequire`.
*
* Bun's compiled-binary module resolver only finds `<pkg>/index.js` for bare
* specifiers loaded from the *real* filesystem — it ignores `main`/`exports`
* (issue #1763). Runtime-installed graphs (`@huggingface/transformers` →
* `onnxruntime-node` → `onnxruntime-common`, `fastembed` →
* `@anush008/tokenizers` → platform binding) all point `main`/`exports` at
* nested files, so the stock resolver cannot load any of them. We patch
* `Module._resolveFilename` to resolve those bare specifiers against the
* registered runtime caches ourselves, honoring `main`/`exports`.
*
* This module is filesystem-pure aside from {@link installRuntimeModuleResolver}
* mutating the `node:module` resolver, so the resolution logic is unit-testable
* without a compiled binary.
*/
/** Conditions honored when resolving an `exports` map for a CommonJS `require`. */
const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, default: true };
/** Extension probes appended to a `main`/`exports` target that lacks one. */
const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Walk a conditional `exports` target (string, array of fallbacks, or a
* condition object) and return the first relative path that matches a runtime
* condition in declaration order. Returns `null` when nothing applies (e.g.
* an `import`-only entry).
*/
export function selectConditionalTarget(target: unknown): string | null {
if (typeof target === "string") return target;
if (Array.isArray(target)) {
for (const entry of target) {
const resolved = selectConditionalTarget(entry);
if (resolved) return resolved;
}
return null;
}
if (isRecord(target)) {
for (const condition in target) {
if (!RUNTIME_CONDITIONS[condition]) continue;
const resolved = selectConditionalTarget(target[condition]);
if (resolved) return resolved;
}
}
return null;
}
/** Resolve a relative target inside a package to a concrete file path, probing extensions and `index`. */
function resolveFileTarget(pkgDir: string, relative: string): string | null {
const base = path.join(pkgDir, relative);
const candidates = [base, ...RUNTIME_EXTENSIONS.map(ext => base + ext)];
for (const candidate of candidates) {
try {
const stat = fs.statSync(candidate);
if (stat.isFile()) return candidate;
if (stat.isDirectory()) {
const indexed = resolveFileTarget(candidate, "index");
if (indexed) return indexed;
}
} catch {
// missing candidate — keep probing
}
}
return null;
}
function resolveExportsEntry(
pkgDir: string,
exports: Record<string, unknown>,
subpath: string | undefined,
): string | null {
let subpathMap = false;
for (const key in exports) {
subpathMap = key === "." || key.startsWith("./");
break;
}
if (subpathMap) {
const key = subpath ? `./${subpath}` : ".";
if (!(key in exports)) return null;
const target = selectConditionalTarget(exports[key]);
return target ? resolveFileTarget(pkgDir, target) : null;
}
// A bare condition map only describes the package root, so a subpath
// request falls through to plain path joining at the call site.
if (subpath) return null;
const target = selectConditionalTarget(exports);
return target ? resolveFileTarget(pkgDir, target) : null;
}
/**
* Split a bare specifier into its package name and optional subpath, handling
* scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
*/
export function splitBareSpecifier(specifier: string): { packageName: string; subpath: string | undefined } {
const segments = specifier.split("/");
const take = specifier.startsWith("@") ? 2 : 1;
const packageName = segments.slice(0, take).join("/");
const subpath = segments.length > take ? segments.slice(take).join("/") : undefined;
return { packageName, subpath };
}
/**
* Resolve a bare specifier against an installed `node_modules` directory,
* honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
* Returns an absolute file path, or `null` when the package/entry is absent.
*/
export function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null {
const { packageName, subpath } = splitBareSpecifier(specifier);
const pkgDir = path.join(runtimeNodeModules, ...packageName.split("/"));
const manifest = readManifest(pkgDir);
if (!manifest) return subpath ? resolveFileTarget(pkgDir, subpath) : null;
const { exports } = manifest;
if (typeof exports === "string" || isRecord(exports)) {
const map = typeof exports === "string" ? { ".": exports } : exports;
const resolved = resolveExportsEntry(pkgDir, map, subpath);
if (resolved) return resolved;
}
if (subpath) return resolveFileTarget(pkgDir, subpath);
if (typeof manifest.main === "string") {
const resolved = resolveFileTarget(pkgDir, manifest.main);
if (resolved) return resolved;
}
return resolveFileTarget(pkgDir, "index.js");
}
function readManifest(pkgDir: string): Record<string, unknown> | null {
try {
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
interface ModuleResolver {
_resolveFilename(request: string, parent: unknown, isMain: boolean, options?: unknown): string;
}
interface ResolverRegistration {
runtimeNodeModules: string;
stubs: Record<string, string>;
}
const REGISTRY = Symbol.for("omp.runtimeModuleResolver.registry");
const PATCHED = Symbol.for("omp.runtimeModuleResolver.patched");
/**
* The registration list lives on `globalThis` so a bundled copy and a
* source copy of this module in one process share the same registry — the
* resolver is patched once per process, and the patched closure must see
* every registration.
*/
function resolverRegistry(): ResolverRegistration[] {
const holder = globalThis as { [REGISTRY]?: ResolverRegistration[] };
holder[REGISTRY] ??= [];
return holder[REGISTRY];
}
export interface RuntimeResolverOptions {
/** Absolute path to the runtime cache's `node_modules`. */
runtimeNodeModules: string;
/** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
stubs?: Record<string, string>;
}
/**
* Patch `node:module`'s resolver (idempotently) so bare specifiers that the
* stock compiled-binary resolver cannot find fall back to the registered
* runtime caches. Stock resolution is tried first, so this never changes
* behavior for modules that already resolve (bundled imports, node builtins).
* Multiple runtime roots may register; they are consulted in registration
* order.
*/
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: RuntimeResolverOptions): void {
const registry = resolverRegistry();
const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules);
if (existing) Object.assign(existing.stubs, stubs);
else registry.push({ runtimeNodeModules, stubs: { ...stubs } });
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: boolean };
if (target[PATCHED]) return;
const original = target._resolveFilename.bind(target);
target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
try {
return original(request, parent, isMain, options);
} catch (error) {
for (const registration of resolverRegistry()) {
const stub = registration.stubs[request];
if (stub) return stub;
const resolved = resolveRuntimeModule(registration.runtimeNodeModules, request);
if (resolved) return resolved;
}
throw error;
}
};
target[PATCHED] = true;
}
/** Pinned dependency set materialized into a runtime cache directory. */
export interface RuntimeInstallSpec {
dependencies: Record<string, string>;
/** Packages whose lifecycle scripts bun may run during the install. */
trustedDependencies?: string[];
}
export type RuntimeInstallPhase = "initiate" | "download" | "done";
export interface EnsureRuntimeInstalledOptions {
/** Directory owning the runtime `package.json` + `node_modules`. */
runtimeDir: string;
install: RuntimeInstallSpec;
/** Package whose installed manifest marks the runtime complete; defaults to the first dependency. */
probePackage?: string;
/** Phase notifications (progress UI); not emitted when already installed. */
onPhase?: (phase: RuntimeInstallPhase) => void;
lockAttempts?: number;
lockSleepMs?: number;
}
function isErrnoCode(error: unknown, code: string): boolean {
return typeof error === "object" && error !== null && "code" in error && error.code === code;
}
async function acquireInstallLock(runtimeDir: string, attempts: number, sleepMs: number): Promise<() => Promise<void>> {
const lockDir = `${runtimeDir}.lock`;
await fsp.mkdir(path.dirname(lockDir), { recursive: true });
for (let attempt = 0; attempt < attempts; attempt++) {
try {
await fsp.mkdir(lockDir);
return async () => {
await fsp.rm(lockDir, { recursive: true, force: true });
};
} catch (error) {
if (!isErrnoCode(error, "EEXIST")) throw error;
await Bun.sleep(sleepMs);
}
}
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
}
async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
await fsp.mkdir(runtimeDir, { recursive: true });
const manifest: Record<string, unknown> = {
private: true,
type: "module",
dependencies: install.dependencies,
};
if (install.trustedDependencies?.length) manifest.trustedDependencies = install.trustedDependencies;
await Bun.write(path.join(runtimeDir, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
}
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
if (!stream) return "";
return new Response(stream).text();
}
async function runRuntimeInstall(runtimeDir: string): Promise<void> {
// `process.execPath` is plain bun in source/bundle mode and the compiled
// binary otherwise; BUN_BE_BUN makes the compiled binary act as bun.
const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
env: { ...Bun.env, BUN_BE_BUN: "1" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
proc.exited,
]);
if (exitCode === 0) return;
const output = `${stdout}\n${stderr}`.trim();
throw new Error(
`Failed to install runtime at ${runtimeDir} with ${process.execPath} install (exit ${exitCode}): ${output}`,
);
}
/**
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
* cross-process safe via a lock directory). Returns `runtimeDir`.
*/
export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
let probePackage = options.probePackage;
if (!probePackage) {
for (const name in install.dependencies) {
probePackage = name;
break;
}
}
if (!probePackage) throw new Error(`Runtime install at ${runtimeDir} declares no dependencies`);
const probeManifest = Bun.file(path.join(runtimeDir, "node_modules", ...probePackage.split("/"), "package.json"));
if (await probeManifest.exists()) return runtimeDir;
onPhase?.("initiate");
const releaseLock = await acquireInstallLock(runtimeDir, lockAttempts, lockSleepMs);
try {
if (await probeManifest.exists()) return runtimeDir;
await writeRuntimeManifest(runtimeDir, install);
onPhase?.("download");
await runRuntimeInstall(runtimeDir);
onPhase?.("done");
return runtimeDir;
} finally {
await releaseLock();
}
}
+137
View File
@@ -0,0 +1,137 @@
import { afterEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { resolveRuntimeModule, splitBareSpecifier } from "../src/runtime-install";
// Contract under test: runtime-installed packages (fastembed, Transformers.js
// graphs) load inside compiled binaries through resolveRuntimeModule, which
// must honor `exports` (CommonJS conditions), then `main` (including `.node`
// targets without an extension probe match), then `index.js` — the shapes the
// stock compiled-binary resolver gets wrong (Bun #1763).
const tempDirs: string[] = [];
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })));
});
async function makeNodeModules(packages: Record<string, { manifest: Record<string, unknown>; files: string[] }>) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "omp-runtime-install-"));
tempDirs.push(root);
const nodeModules = path.join(root, "node_modules");
for (const name in packages) {
const pkg = packages[name];
const pkgDir = path.join(nodeModules, ...name.split("/"));
await Bun.write(path.join(pkgDir, "package.json"), JSON.stringify({ name, ...pkg.manifest }));
for (const file of pkg.files) {
await Bun.write(path.join(pkgDir, file), "");
}
}
return nodeModules;
}
describe("splitBareSpecifier", () => {
test("splits scoped and unscoped specifiers with subpaths", () => {
expect(splitBareSpecifier("fastembed")).toEqual({ packageName: "fastembed", subpath: undefined });
expect(splitBareSpecifier("tar/lib/extract")).toEqual({ packageName: "tar", subpath: "lib/extract" });
expect(splitBareSpecifier("@anush008/tokenizers")).toEqual({
packageName: "@anush008/tokenizers",
subpath: undefined,
});
expect(splitBareSpecifier("@huggingface/transformers/types")).toEqual({
packageName: "@huggingface/transformers",
subpath: "types",
});
});
});
describe("resolveRuntimeModule", () => {
test("resolves conditional exports preferring require over import", async () => {
const nodeModules = await makeNodeModules({
fastembed: {
manifest: {
exports: {
".": {
import: { default: "./lib/esm/index.js" },
require: { default: "./lib/cjs/index.js" },
},
},
main: "./lib/cjs/index.js",
},
files: ["lib/esm/index.js", "lib/cjs/index.js"],
},
});
expect(resolveRuntimeModule(nodeModules, "fastembed")).toBe(
path.join(nodeModules, "fastembed", "lib", "cjs", "index.js"),
);
});
test("falls back to main pointing at a .node binding (napi-rs platform package)", async () => {
const nodeModules = await makeNodeModules({
"@anush008/tokenizers-darwin-arm64": {
manifest: { main: "tokenizers.darwin-arm64.node" },
files: ["tokenizers.darwin-arm64.node"],
},
});
expect(resolveRuntimeModule(nodeModules, "@anush008/tokenizers-darwin-arm64")).toBe(
path.join(nodeModules, "@anush008", "tokenizers-darwin-arm64", "tokenizers.darwin-arm64.node"),
);
});
test("probes extensions and directory index for extensionless main", async () => {
const nodeModules = await makeNodeModules({
"onnxruntime-node": {
manifest: { main: "dist/index" },
files: ["dist/index.js"],
},
"onnxruntime-common": {
manifest: { main: "dist" },
files: ["dist/index.js"],
},
});
expect(resolveRuntimeModule(nodeModules, "onnxruntime-node")).toBe(
path.join(nodeModules, "onnxruntime-node", "dist", "index.js"),
);
expect(resolveRuntimeModule(nodeModules, "onnxruntime-common")).toBe(
path.join(nodeModules, "onnxruntime-common", "dist", "index.js"),
);
});
test("resolves subpath requests through the exports map and via plain joining", async () => {
const nodeModules = await makeNodeModules({
mapped: {
manifest: { exports: { ".": "./index.js", "./util": { require: "./lib/util.cjs" } } },
files: ["index.js", "lib/util.cjs"],
},
plain: {
manifest: { main: "index.js" },
files: ["index.js", "lib/helper.js"],
},
});
expect(resolveRuntimeModule(nodeModules, "mapped/util")).toBe(
path.join(nodeModules, "mapped", "lib", "util.cjs"),
);
expect(resolveRuntimeModule(nodeModules, "plain/lib/helper")).toBe(
path.join(nodeModules, "plain", "lib", "helper.js"),
);
});
test("returns null for absent packages and import-only exports", async () => {
const nodeModules = await makeNodeModules({
"esm-only": {
manifest: { exports: { ".": { import: "./index.mjs" } } },
files: ["index.mjs"],
},
});
expect(resolveRuntimeModule(nodeModules, "missing-package")).toBeNull();
expect(resolveRuntimeModule(nodeModules, "esm-only")).toBeNull();
});
test("falls back to index.js when manifest has no usable entry", async () => {
const nodeModules = await makeNodeModules({
bare: { manifest: {}, files: ["index.js"] },
});
expect(resolveRuntimeModule(nodeModules, "bare")).toBe(path.join(nodeModules, "bare", "index.js"));
});
});