diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd55bab..0f86c15c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Lazy-loaded embedding providers no longer mislabel model/runtime failures as "not installed" ([#118](https://github.com/jdutton/vibe-agent-toolkit/issues/118)).** `loadPipeline` in `transformers-embedding-provider.ts` wrapped both the dynamic `import('@xenova/transformers')` and the model download/inference in a single `catch` that always rethrew a fixed `@xenova/transformers is not installed` message, swallowing the real error (not even as `cause`) — so a model-download or `onnxruntime-node` native-backend failure on an installed package was reported as a missing dependency. The two failure modes are now separated: an import failure keeps the actionable install hint (now with the original error attached as `cause`), while a model/inference failure throws `Failed to load transformers model ''` preserving `cause`. The sibling `onnx-embedding-provider.ts` was audited: its install-hint `catch` was already correctly scoped to the import alone, but its model download (`ensureModelFiles`) and session creation (`InferenceSession.create`) previously bubbled raw errors with no provider/model context, so they now throw `Failed to download ONNX model ''` / `Failed to load ONNX model ''` with `cause` preserved. - **Transformers.js integration tests now skip on Windows CI instead of flaking.** `transformers-embedding-provider.integration.test.ts` and the Transformers.js block of `comparison.integration.test.ts` skip on Windows (in addition to skipping when the optional `@xenova/transformers` dependency is absent), matching the existing `onnx-embedding-provider` test. These tests download a model over the network and load the `onnxruntime-node` native backend — both flaky in Windows CI. Such a failure was previously mislabeled `@xenova/transformers is not installed` by an over-broad `catch` in the provider's `loadPipeline` (the package was installed; the model download/inference is what failed), which is also why an availability-only guard did not prevent it. - **Config-first skill discovery now honors `..` in `skills.include` patterns.** `vat build`, `vat verify`, and `vat skills validate` all funnel through `discoverSkillsFromConfig`, which previously passed every include pattern to a single downward-only crawl rooted at `projectRoot` — so an include like `"../../docs/skills/*/SKILL.md"` (common in monorepos where SKILL.md sources live alongside, not inside, the package) silently matched zero skills. `vat audit` accepted the same config only because it has a separate filesystem-first walker. Each include pattern is now split into a literal base + glob remainder via `picomatch.scan`, patterns are grouped by their resolved absolute base, and the crawler runs once per base — making config-first discovery agree with audit. User-supplied excludes stay anchored to `projectRoot` so patterns like `docs/private/**` keep their original meaning, and a pattern resolving to a nonexistent base now silently produces zero matches. diff --git a/packages/rag/src/embedding-providers/onnx-embedding-provider.ts b/packages/rag/src/embedding-providers/onnx-embedding-provider.ts index 16fd7511..7cfdc409 100644 --- a/packages/rag/src/embedding-providers/onnx-embedding-provider.ts +++ b/packages/rag/src/embedding-providers/onnx-embedding-provider.ts @@ -202,19 +202,31 @@ export class OnnxEmbeddingProvider implements EmbeddingProvider { modelPath = safePath.join(this.configModelPath, 'model.onnx'); vocabPath = safePath.join(this.configModelPath, 'vocab.txt'); } else { - const files = await ensureModelFiles(this.model, this.cacheDir); - modelPath = files.modelPath; - vocabPath = files.vocabPath; + try { + const files = await ensureModelFiles(this.model, this.cacheDir); + modelPath = files.modelPath; + vocabPath = files.vocabPath; + } catch (cause) { + throw new Error(`Failed to download ONNX model '${this.model}': ${String(cause)}`, { + cause, + }); + } } const sessionOptions = this.executionProviders ? { executionProviders: this.executionProviders } : undefined; - const session = await ort.InferenceSession.create(modelPath, sessionOptions); - const tokenizer = await BertTokenizer.fromVocabFile(vocabPath); + try { + const session = await ort.InferenceSession.create(modelPath, sessionOptions); + const tokenizer = await BertTokenizer.fromVocabFile(vocabPath); - return { session, tokenizer }; + return { session, tokenizer }; + } catch (cause) { + throw new Error(`Failed to load ONNX model '${this.model}': ${String(cause)}`, { + cause, + }); + } } /** diff --git a/packages/rag/src/embedding-providers/transformers-embedding-provider.ts b/packages/rag/src/embedding-providers/transformers-embedding-provider.ts index a8039aaa..1d993168 100644 --- a/packages/rag/src/embedding-providers/transformers-embedding-provider.ts +++ b/packages/rag/src/embedding-providers/transformers-embedding-provider.ts @@ -7,6 +7,8 @@ * Requires optional dependency: npm install @xenova/transformers */ +import type { pipeline as PipelineFactory } from '@xenova/transformers'; + import type { EmbeddingProvider } from '../interfaces/embedding.js'; /** @@ -31,16 +33,25 @@ type PipelineFunction = ( async function loadPipeline( model: string, ): Promise { + let pipeline: typeof PipelineFactory; try { - const { pipeline } = await import('@xenova/transformers'); - return (await pipeline('feature-extraction', model, { - quantized: true, - })) as PipelineFunction; - } catch { + ({ pipeline } = await import('@xenova/transformers')); + } catch (cause) { throw new Error( '@xenova/transformers is not installed. Install with: npm install @xenova/transformers', + { cause }, ); } + + try { + return (await pipeline('feature-extraction', model, { + quantized: true, + })) as PipelineFunction; + } catch (cause) { + throw new Error(`Failed to load transformers model '${model}': ${String(cause)}`, { + cause, + }); + } } /**