feat(embeddings): add optional transformers.js companion for local embeddings#434
feat(embeddings): add optional transformers.js companion for local embeddings#434arabold wants to merge 5 commits into
Conversation
…local embeddings Externalize the heavy @huggingface/transformers dependency (~370MB with the ONNX runtime) into an optional companion package, @arabold/docs-mcp-server-transformers, so the default server install stays small. The integration code stays in this repo and loads the dependency lazily via a dynamic import, only when a `transformers:` embedding model is selected. - Add packages/transformers companion (npm workspace) that re-exports pipeline, env and the pipeline type from @huggingface/transformers. - Add transformersLoader with a literal-string dynamic import (so Rollup externalizes it), caching, and a TransformersCompanionMissingError that only fires when the companion package itself is missing. - Add TransformersJSEmbeddings (LangChain Embeddings) built on the loader, using minimal structural types so the main typecheck/bundle never pull in the heavy package. - Wire the transformers provider through EmbeddingFactory and EmbeddingConfig; no credentials required. Fix BAAI/bge-small-en-v1.5 dimension 512 -> 384. - Externalize the companion in vite.config.ts; enable npm workspaces and add the companion as a wildcard devDependency. - Publish the companion in lockstep with the main package from the release pipeline (semantic-release + manual paths); prepack builds dist before publish. - Bake the companion into the Docker image (TRANSFORMERS_CACHE=/models + volume). - Document local/offline embeddings and the companion install in the guides. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Capture the rationale and contract for the optional local-embeddings companion retroactively, so the why and how are documented: - proposal.md: motivation, what changes, impact - design.md: companion re-export + lazy dynamic import, structural types, reuse of generic dimension resolution, lockstep release, Docker bundling, risks and trade-offs - specs/local-embeddings: new capability (provider, optional distribution, lazy loading + missing-companion handling, device/cache config, version compatibility) - specs/embedding-resolution: deltas for the transformers provider and the bge-small-en-v1.5 384 dimension fix - tasks.md: implementation breakdown (already complete) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an optional Transformers.js companion workspace package to enable fully local/offline embeddings (no API key) without bloating the default install and bundle size for hosted-embeddings users. The main server integrates the new transformers: provider via a cached, literal-string dynamic import so the heavy @huggingface/transformers + ONNX runtime is only required when actually used.
Changes:
- Introduces
@arabold/docs-mcp-server-transformersas an npm-workspace companion that re-exports Transformers.js (pipeline/env), and wires the repo to build/publish it in lockstep. - Adds lazy loader + LangChain
Embeddingsimplementation fortransformers:models, and plumbs the provider through config/factory (including fixingBAAI/bge-small-en-v1.5to 384 dims). - Updates bundling (Vite external), Docker image packaging, release workflow, and docs/OpenSpec artifacts.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Externalizes the optional companion package so dynamic import is preserved and never bundled. |
| src/store/embeddings/transformersLoader.ts | Adds cached lazy import + missing-companion error classification. |
| src/store/embeddings/transformersLoader.test.ts | Tests loader error mapping and missing-companion error shape. |
| src/store/embeddings/TransformersJSEmbeddings.ts | Implements local/offline embeddings on top of Transformers.js with cache/device env support. |
| src/store/embeddings/TransformersJSEmbeddings.test.ts | Unit tests for query/batch embedding behavior, caching, env controls. |
| src/store/embeddings/EmbeddingFactory.ts | Adds transformers provider creation + credential availability rules. |
| src/store/embeddings/EmbeddingFactory.test.ts | Adds coverage ensuring transformers provider doesn’t require credentials. |
| src/store/embeddings/EmbeddingConfig.ts | Extends provider union + corrects known dimension for bge-small-en-v1.5. |
| src/store/embeddings/EmbeddingConfig.test.ts | Adds parsing test for transformers provider and corrected dimension. |
| docs/guides/embedding-models.md | Documents companion install + transformers: usage, cache/device env vars. |
| README.md | Updates top-level docs pointers to mention offline/local embeddings. |
| packages/transformers/package.json | Defines the companion package (name/version/exports/deps/scripts). |
| packages/transformers/src/index.ts | Thin re-exporter for Transformers.js (pipeline, env, pipeline type). |
| packages/transformers/tsconfig.json | TypeScript build config for the companion package output. |
| packages/transformers/README.md | Companion usage/install documentation. |
| package.json | Enables workspaces, builds companion before main build, adds wildcard devDep. |
| package-lock.json | Workspace linking + adds Transformers.js/ONNX dependency graph for dev/CI/Docker. |
| Dockerfile | Copies workspace metadata for npm ci, and ships companion in runtime image with /models cache volume. |
| .releaserc.json | Publishes companion package via a second semantic-release npm step and commits its package.json. |
| .github/workflows/release.yml | Adds manual publishing for the companion and commits its version bump. |
| openspec/changes/add-transformers-companion/* | Adds OpenSpec proposal/design/spec/tasks documenting the change retroactively. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "main": "./dist/index.js", | ||
| "types": "./src/index.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./src/index.ts", | ||
| "import": "./dist/index.js" | ||
| } | ||
| }, |
There was a problem hiding this comment.
Good catch - fixed in 69ea534. Both types and exports["."].types now point to the emitted ./dist/index.d.ts (the package emits declarations via tsc, and prepack builds dist/ before publish).
There was a problem hiding this comment.
Correction: I reverted this in b4244d6 and kept types on ./src/index.ts. The main package type-checks the companion through the dynamic import() in transformersLoader.ts, and the lint/typecheck CI job does not build the companion, so pointing types at ./dist/index.d.ts breaks tsc with TS2307 (the declaration file does not exist yet). The companion publishes both src and dist, so consumers still get types from source. This keeps the main repo type-check independent of the companion build, which is the intended setup.
| super( | ||
| `❌ Unsupported embedding provider: ${provider}\n` + | ||
| " Supported providers: openai, vertex, gemini, aws, microsoft, sagemaker\n" + | ||
| " Supported providers: openai, vertex, gemini, aws, microsoft, sagemaker, transformers\n" + | ||
| " See README.md for configuration options or run with --help for more details.", |
There was a problem hiding this comment.
This one predates this PR - on main, createEmbeddingModel() already has no case "sagemaker" even though the union and UnsupportedProviderError message list it (areCredentialsAvailable() does handle it, which is the inconsistency). This PR only adds the transformers provider and does not touch the sagemaker path, so I have left it out of scope to keep the change focused. Happy to file a follow-up to either implement the SageMaker case or drop it from the supported list.
The CI Test job builds nothing before running Vitest, so the optional companion package has no dist/ output. Vitest statically resolves the dynamic import in transformersLoader.ts and fails with ERR_RESOLVE_PACKAGE_ENTRY_FAIL, taking down unrelated suites (cli, DocumentStore) that only import the loader transitively. Map the companion specifier to a lightweight test stub via test.alias so resolution always succeeds without building or loading the heavy package. Production is unaffected: the build still externalizes the literal import and never bundles @huggingface/transformers or the ONNX runtime. Also point the companion's `types`/`exports.types` at the emitted dist/index.d.ts instead of the TypeScript source, per npm convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reverts the `types`/`exports.types` change to `./dist/index.d.ts`: the main package type-checks the companion through the dynamic import in transformersLoader.ts, and `dist/` is not built during the lint/typecheck CI job, so pointing at the declaration output breaks `tsc` with TS2307. Pointing `types` at the published `./src/index.ts` (the companion ships both `src` and `dist`) lets the main repo type-check without building the companion, which is the intended lightweight, build-independent setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Test CI job runs `npm ci` + `npm test` with no build, so the companion package has no dist/ output. Two resolution paths then break: - In-process Vitest resolves the dynamic import in transformersLoader.ts. - The CLI/MCP/telemetry e2e tests spawn the server; when the main dist is absent they fall back to `vite-node src/index.ts`, whose top-level Vite resolver also resolves the companion specifier. A `test.alias` only covers the first path, not the spawned vite-node process, so it was insufficient. Instead add a `pretest` step that builds the companion (mirroring the existing `pretest:e2e` pattern). With the companion built, both resolution paths succeed; the heavy module is still only loaded lazily when local embeddings are actually used. Replaces the earlier test alias + stub approach. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Why
Community PR #390 proposed adding
@huggingface/transformersso users can run fully offline, local embeddings with no API key. The problem: that dependency plus the ONNX runtime is ~370 MB. Making it a hard dependency would bloat install size and the Docker image for the large majority of users who use a hosted embedding provider (OpenAI, Gemini, etc.).This PR delivers the same offline-embeddings capability while keeping the heavy dependency optional.
Approach
The heavy dependency is externalized into a thin npm-workspace companion package,
@arabold/docs-mcp-server-transformers, that simply re-exportspipeline/envfrom@huggingface/transformers. Integration code stays in the main repo and loads the companion lazily via a literal-string dynamic import, so:npm i -g @arabold/docs-mcp-server-transformersand select thetransformersprovider.Key pieces:
packages/transformers/- companion re-exporter package, versioned in lockstep with the main package.transformersLoader.ts- cached lazy import plus precise missing-companion error mapping (only treats the bare package specifier as "not installed", so a broken internal path isn't misreported).TransformersJSEmbeddings.ts- LangChainEmbeddingsimplementation on top of the loader.transformersadded to the provider union and factory;areCredentialsAvailablereturns true (no API key needed). Reusesmain's generic dimension-resolution (from the recent Fix embedding dimension resolution for custom models #431 embedding-dimension rework) with noDocumentStorechanges.BAAI/bge-small-en-v1.5dimension from 512 to 384.Distribution
.releaserc.json,release.yml): the companion publishes in lockstep with the main package via a second@semantic-release/npminstance.TRANSFORMERS_CACHE=/modelsexposed as a volume.ci.yml,eval.yml): no changes needed -npm ciresolves the workspace andnpm run buildcompiles the companion first (verified with a clean install).Trade-offs worth a careful look
"*") devDependency so lockstep version bumps never break workspace linking.Notes
add-transformers-companion) capturing the rationale, design, and spec deltas retroactively.