Skip to content
Merged
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
15 changes: 15 additions & 0 deletions gitnexus/src/cli/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import fs from 'fs/promises';
import { cliError } from './cli-message.js';
import { formatElapsed } from './format-elapsed.js';
import { isHfDownloadFailure } from '../core/embeddings/hf-env.js';
import { isLocalEmbeddingRuntimeBlockerMessage } from '../core/embeddings/runtime-support.js';
import { warnIfNpm11NpxRisk } from './resolve-invocation.js';

// Capture stderr.write at module load BEFORE anything (LadybugDB native
Expand Down Expand Up @@ -1191,6 +1192,20 @@ const analyzeCommandImpl = async (inputPath?: string, options?: AnalyzeOptions):
return;
}

// Local embedding runtime unsupported on this platform (macOS Intel ships no
// darwin/x64 ONNX native binding, #1515). The guard threw before importing
// transformers.js, so this is a clean, actionable GitNexus message. Checked
// before the network-heuristic isHfDownloadFailure branch below (and before
// the generic module-not-found "installation may be corrupt" hint) so the
// explicit platform message always takes priority.
if (isLocalEmbeddingRuntimeBlockerMessage(msg)) {
cliError(` ${msg.replace(/\n/g, '\n ')}\n`, {
recoveryHint: 'local-embedding-unsupported',
});
process.exitCode = 1;
return;
}

// HF download failure — show clean guidance without the raw stack trace.
// Checked before writeFatalToStderr so the user sees one focused message
// rather than a stack-trace dump followed by a second remediation block.
Expand Down
1 change: 1 addition & 0 deletions gitnexus/src/cli/cli-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type RecoveryHint =
| 'heap-oom-respawn'
| 'native-worker-abort'
| 'hf-endpoint-unreachable'
| 'local-embedding-unsupported'
| 'large-repo'
| 'npm-resolution'
| 'module-not-found';
Expand Down
37 changes: 37 additions & 0 deletions gitnexus/src/cli/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getRuntimeCapabilities, getRuntimeFingerprint } from '../core/platform/capabilities.js';
import { resolveEmbeddingConfig } from '../core/embeddings/config.js';
import { isHttpMode } from '../core/embeddings/http-client.js';
import { getLocalEmbeddingRuntimeBlocker } from '../core/embeddings/runtime-support.js';
import { checkLbugNative } from '../core/lbug/native-check.js';
import { getExtensionInstallPolicy } from '../core/lbug/extension-loader.js';
import { t } from './i18n/index.js';
Expand Down Expand Up @@ -50,6 +51,33 @@ export function padDisplayEnd(value: string, columns: number): string {

const label = (key: Parameters<typeof t>[0], width: number): string => padDisplayEnd(t(key), width);

/**
* Embedding-runtime support status for the `doctor` Embeddings section.
* Pure and DI-friendly so it can be unit-tested without running the whole
* command. Delegates the platform decision to
* {@link getLocalEmbeddingRuntimeBlocker} so the wording stays in one place.
*
* - HTTP mode: always supported (never touches the native runtime).
* - Local mode on an unsupported platform (macOS Intel, #1515): reports the
* blocker as `detail` so the caller can surface the full guidance.
*/
export function localEmbeddingDoctorStatus(opts: {
httpMode: boolean;
platform?: NodeJS.Platform;
arch?: NodeJS.Architecture;
}): { status: string; detail: string | null } {
if (opts.httpMode) {
return { status: '✓ http endpoint configured', detail: null };
}
const platform = opts.platform ?? process.platform;
const arch = opts.arch ?? process.arch;
const blocker = getLocalEmbeddingRuntimeBlocker({ platform, arch });
if (blocker) {
return { status: `✗ local embeddings unavailable on ${platform}/${arch}`, detail: blocker };
}
return { status: '✓ local embeddings supported', detail: null };
}

export const doctorCommand = async () => {
const fingerprint = getRuntimeFingerprint();
const capabilities = getRuntimeCapabilities();
Expand Down Expand Up @@ -102,4 +130,13 @@ export const doctorCommand = async () => {
console.log(
` ${label('doctor.labels.subBatch', 12)}${t('doctor.chunks', { count: embeddingConfig.subBatchSize })}`,
);
// Surface local-runtime support so macOS Intel users see up front that local
// embeddings can't load here (the bundled ONNX Runtime ships no darwin/x64
// native binding, #1515) — rather than discovering it only when
// `analyze --embeddings` fails. Literal label like the 'native' line above.
const support = localEmbeddingDoctorStatus({ httpMode: isHttpMode() });
console.log(` ${padDisplayEnd('Support:', 12)}${support.status}`);
if (support.detail) {
process.stderr.write(`\n${support.detail.replace(/^/gm, ' ')}\n\n`);
}
};
27 changes: 21 additions & 6 deletions gitnexus/src/core/embeddings/embedder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ if (!process.env.ORT_LOG_LEVEL) {
process.env.ORT_LOG_LEVEL = '3';
}

import {
pipeline,
env,
type FeatureExtractionPipeline,
type ProgressInfo,
} from '@huggingface/transformers';
// Type-only import: erased at compile time so loading this module never pulls
// in @huggingface/transformers (and its native onnxruntime-node binding) at
// runtime. The runtime values (pipeline, env) are dynamically imported inside
// initEmbedder, after the platform guard has passed (#1515).
import type { FeatureExtractionPipeline, ProgressInfo } from '@huggingface/transformers';
import { existsSync } from 'fs';
import { execFileSync } from 'child_process';
import { join, dirname } from 'path';
Expand All @@ -28,6 +27,7 @@ import { DEFAULT_EMBEDDING_CONFIG, type EmbeddingConfig, type ModelProgress } fr
import { isHttpMode, getHttpDimensions, httpEmbed } from './http-client.js';
import { resolveEmbeddingConfig } from './config.js';
import { applyHfEnvOverrides, isHfDownloadFailure, withHfDownloadRetry } from './hf-env.js';
import { getLocalEmbeddingRuntimeBlocker } from './runtime-support.js';
import { logger } from '../logger.js';

/**
Expand Down Expand Up @@ -143,6 +143,17 @@ export const initEmbedder = async (
);
}

// Fail fast on platforms where the bundled native ONNX Runtime binding is not
// shipped (macOS Intel, #1515). Must run before any transformers.js /
// onnxruntime-node import or resolution — otherwise the native module load
// crashes with a raw "Cannot find module ...onnxruntime_binding.node" that
// ONNX_WEB_BACKEND=wasm cannot rescue (#1516). HTTP mode was already handled
// above, so this only blocks the local-runtime path.
const runtimeBlocker = getLocalEmbeddingRuntimeBlocker();
if (runtimeBlocker) {
throw new Error(runtimeBlocker);
}

// Return existing instance if available
if (embedderInstance) {
return embedderInstance;
Expand All @@ -166,6 +177,10 @@ export const initEmbedder = async (

initPromise = (async () => {
try {
// Lazy-load transformers.js only after the runtime guard has passed, so
// unsupported platforms never reach the native ONNX import (#1515).
const { pipeline, env } = await import('@huggingface/transformers');

// Configure transformers.js environment
env.allowLocalModels = false;
// Bridge user-controlled env vars to transformers.js: HF_HOME →
Expand Down
79 changes: 79 additions & 0 deletions gitnexus/src/core/embeddings/runtime-support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Local embedding runtime support guard.
*
* The bundled local embedding stack (`@huggingface/transformers` →
* `onnxruntime-node`) only ships native ONNX Runtime bindings for a subset of
* platform/arch pairs. On macOS Intel (`darwin`/`x64`), `onnxruntime-node`
* ships no `bin/napi-v6/darwin/x64/onnxruntime_binding.node`, so *importing*
* transformers.js throws a raw `Cannot find module ...onnxruntime_binding.node`
* before any device/backend selection can run (#1515). `ONNX_WEB_BACKEND=wasm`
* cannot rescue this — the failure is at native-module import time, not backend
* selection (#1516).
*
* This module is intentionally free of any native or transformers.js import (at
* module scope or inside its functions) so it can be consulted *before* the
* dynamic import that would crash. HTTP embedding mode never touches the native
* runtime, so callers in HTTP mode must skip this guard.
*/

/**
* Stable lead line of the macOS-Intel blocker message. Also used to recognise
* the thrown error in the CLI error handler without coupling to the full
* wording (see {@link isLocalEmbeddingRuntimeBlockerMessage}).
*/
const LOCAL_EMBEDDING_BLOCKER_LEAD =
'Local semantic embeddings are unavailable on macOS Intel (darwin/x64).';

export interface LocalEmbeddingRuntimeOptions {
platform?: NodeJS.Platform;
arch?: NodeJS.Architecture;
}

/**
* Return a human-readable explanation when the *local* embedding runtime cannot
* load on this platform, or `null` when local embeddings are expected to work.
*
* Only `darwin`/`x64` is blocked today: it is the one platform/arch pair where
* the bundled `onnxruntime-node` ships no native binding (#1515). Every other
* platform returns `null` and follows the normal device-probe path, so genuine
* ONNX failures on supported platforms are never masked by this message.
*
* Accepts an explicit `{ platform, arch }` for testing; defaults to the current
* process values.
*/
export const getLocalEmbeddingRuntimeBlocker = (
options: LocalEmbeddingRuntimeOptions = {},
): string | null => {
const platform = options.platform ?? process.platform;
const arch = options.arch ?? process.arch;

if (platform === 'darwin' && arch === 'x64') {
return [
LOCAL_EMBEDDING_BLOCKER_LEAD,
'The bundled ONNX Runtime package (onnxruntime-node) does not ship a',
'darwin/x64 native binding, so the local embedding model cannot load here.',
'ONNX_WEB_BACKEND=wasm does not help: the failure happens while importing',
'the native runtime, before any backend can be selected. Forcing',
'GITNEXUS_EMBEDDING_DEVICE=wasm (or cpu) does not help either, for the same reason.',
'',
'Use one of these instead:',
' - Run analyze without --embeddings (all other indexing still works).',
' - Point GITNEXUS_EMBEDDING_URL (with GITNEXUS_EMBEDDING_MODEL) at an',
' OpenAI-compatible /v1/embeddings endpoint to embed over HTTP.',
' - Run GitNexus on Linux or in Docker, where the native binding ships.',
' - Run GitNexus on Apple Silicon (darwin/arm64), which ships a binding.',
' - Use a future GitNexus build that restores darwin/x64 ONNX support.',
].join('\n');
}

return null;
};

/**
* True when `message` is the macOS-Intel local-embedding blocker produced by
* {@link getLocalEmbeddingRuntimeBlocker}. Lets the CLI surface a clean,
* actionable message instead of a raw stack trace, without coupling to the
* full wording.
*/
export const isLocalEmbeddingRuntimeBlockerMessage = (message: string): boolean =>
message.includes(LOCAL_EMBEDDING_BLOCKER_LEAD);
21 changes: 20 additions & 1 deletion gitnexus/src/mcp/core/embedder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
* For MCP, we only need to compute query embeddings, not batch embed.
*/

import { pipeline, env, type FeatureExtractionPipeline } from '@huggingface/transformers';
// Type-only import: erased at compile time so loading this module never pulls
// in @huggingface/transformers (and its native onnxruntime-node binding) at
// runtime. The runtime values (pipeline, env) are dynamically imported inside
// initEmbedder, after the platform guard has passed (#1515).
import type { FeatureExtractionPipeline } from '@huggingface/transformers';
import {
isHttpMode,
getHttpDimensions,
Expand All @@ -17,6 +21,7 @@ import {
isHfDownloadFailure,
withHfDownloadRetry,
} from '../../core/embeddings/hf-env.js';
import { getLocalEmbeddingRuntimeBlocker } from '../../core/embeddings/runtime-support.js';
import { silenceStdout, restoreStdout, realStderrWrite } from '../../core/lbug/pool-adapter.js';

import { logger } from '../../core/logger.js';
Expand All @@ -36,6 +41,16 @@ export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {
throw new Error('initEmbedder() should not be called in HTTP mode.');
}

// Fail fast on platforms where the bundled native ONNX Runtime binding is not
// shipped (macOS Intel, #1515). Must run before any transformers.js /
// onnxruntime-node import or resolution — otherwise the native module load
// crashes with a raw "Cannot find module ...onnxruntime_binding.node" that
// ONNX_WEB_BACKEND=wasm cannot rescue (#1516).
const runtimeBlocker = getLocalEmbeddingRuntimeBlocker();
if (runtimeBlocker) {
throw new Error(runtimeBlocker);
}

if (embedderInstance) {
return embedderInstance;
}
Expand All @@ -48,6 +63,10 @@ export const initEmbedder = async (): Promise<FeatureExtractionPipeline> => {

initPromise = (async () => {
try {
// Lazy-load transformers.js only after the runtime guard has passed, so
// unsupported platforms never reach the native ONNX import (#1515).
const { pipeline, env } = await import('@huggingface/transformers');

env.allowLocalModels = false;
// Bridge user-controlled env vars to transformers.js: HF_HOME →
// env.cacheDir, HF_ENDPOINT → env.remoteHost (#1205). Centralised in
Expand Down
Loading
Loading