From cad9090bcab1da7ffee0b6b677d3bd69e68f9ca6 Mon Sep 17 00:00:00 2001 From: ruv Date: Tue, 15 Sep 2026 23:46:10 -0400 Subject: [PATCH] fix(router): lazy-load onnxruntime-node to stop crashing Windows imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onnx-local.ts and onnx-local-optimized.ts both did a top-level `await import('onnxruntime-node')` at module load time, which fires the native-binding load (onnxruntime_binding.node) as a side effect of merely importing the module — even for consumers that never touch the ONNX router at all (e.g. reasoningbank, via core/distill -> router/router). On Windows environments where the NAPI binary can't load, this crashes the whole import chain. Moves the load into a `loadOrt()` helper invoked lazily from initializeSession() (onnx-local.ts), and drops the eager load entirely from onnx-local-optimized.ts, which never referenced the resulting `ort` binding at all — the base class it extends is the only thing that needs it. Rebased from PR #155 (thanks to the original author) — cherry-picked just this lazy-load refactor onto current main, since #155 was based on a pre-#154/#228 snapshot and its @xenova/transformers hunks no longer apply (main has since migrated fully to @huggingface/transformers). Verified empirically (not just by reasoning about it): stubbed onnxruntime-node with a load-marker side effect, confirmed the marker fires on unconditional `import()` of the pre-fix module (bug reproduced) and does not fire post-fix — while still firing once `chat()` actually invokes initializeSession() (confirming lazy-load isn't just "never loads"). Closes #155. Co-Authored-By: RuFlo Claude-Session: https://claude.ai/code/session_01SjxUvZnyBdwtx4Y1jKAqt6 --- .../router/providers/onnx-local-optimized.ts | 20 ++++++------- .../src/router/providers/onnx-local.ts | 28 ++++++++++++++----- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/agentic-flow/src/router/providers/onnx-local-optimized.ts b/agentic-flow/src/router/providers/onnx-local-optimized.ts index 6f3ea8e14..dd9cfc861 100644 --- a/agentic-flow/src/router/providers/onnx-local-optimized.ts +++ b/agentic-flow/src/router/providers/onnx-local-optimized.ts @@ -8,18 +8,18 @@ * - Better generation parameters for code tasks * - System prompt caching * - * Note: onnxruntime-node is optional - will error if not installed + * Note: onnxruntime-node is optional - will error if not installed. + * + * NOTE (ruvnet/ruflo#2048): the previous top-level `await import('onnxruntime-node')` + * fired the native-binding load (`onnxruntime_binding.node`) at module + * import time. On Windows this crashes with "OS cannot run %1" — and the + * crash propagated to any consumer that transitively imports this file + * (e.g. `agentic-flow/reasoningbank` via `core/distill -> router/router`). + * This file does not use `ort` directly — the base `ONNXLocalProvider` + * it extends does, and that file now lazy-loads ort on first session + * init. So we just drop the eager top-level load here. */ -let ort: any = null; - -// Dynamic import for optional onnxruntime-node -try { - ort = await import('onnxruntime-node'); -} catch { - // Will be handled at runtime -} - import { get_encoding } from 'tiktoken'; import { ensurePhi4Model, ModelDownloader, PHI4_MODEL_PATH } from '../../utils/model-downloader.js'; import type { diff --git a/agentic-flow/src/router/providers/onnx-local.ts b/agentic-flow/src/router/providers/onnx-local.ts index 0bcec4b02..63774f3f6 100644 --- a/agentic-flow/src/router/providers/onnx-local.ts +++ b/agentic-flow/src/router/providers/onnx-local.ts @@ -3,17 +3,30 @@ * * Uses onnxruntime-node for true local CPU/GPU inference * Falls back gracefully when native module isn't available (Windows) + * + * NOTE (ruvnet/ruflo#2048): `onnxruntime-node` is loaded LAZILY on first + * `initializeSession()` call, not at module import. The previous top-level + * `await import('onnxruntime-node')` fired the native-binding load + * (`onnxruntime_binding.node`) at module load time, which crashed Windows + * environments where the NAPI binary cannot be loaded — even when the + * consumer (e.g. `agentic-flow/reasoningbank`) never actually invokes + * the router. Moving the import inside `loadOrt()` keeps importing + * `reasoningbank` side-effect-free with respect to native bindings. */ let ort: any = null; let ortAvailable = false; - -// Dynamic import for optional onnxruntime-node -try { - ort = await import('onnxruntime-node'); - ortAvailable = true; -} catch { - console.warn('[ONNX] onnxruntime-node not available - local inference disabled'); +let ortLoaded = false; + +async function loadOrt(): Promise { + if (ortLoaded) return; + ortLoaded = true; + try { + ort = await import('onnxruntime-node'); + ortAvailable = true; + } catch { + console.warn('[ONNX] onnxruntime-node not available - local inference disabled'); + } } import * as fs from 'fs'; @@ -107,6 +120,7 @@ export class ONNXLocalProvider implements LLMProvider { private async initializeSession(): Promise { if (this.session) return; + await loadOrt(); if (!ortAvailable || !ort) { throw new Error('onnxruntime-node not available - install with: npm install onnxruntime-node'); }