Skip to content

feat(embeddings): add optional transformers.js companion for local embeddings#434

Open
arabold wants to merge 5 commits into
mainfrom
arabold/feat-transformers-companion
Open

feat(embeddings): add optional transformers.js companion for local embeddings#434
arabold wants to merge 5 commits into
mainfrom
arabold/feat-transformers-companion

Conversation

@arabold

@arabold arabold commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Why

Community PR #390 proposed adding @huggingface/transformers so 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-exports pipeline/env from @huggingface/transformers. Integration code stays in the main repo and loads the companion lazily via a literal-string dynamic import, so:

  • Users who want local embeddings run npm i -g @arabold/docs-mcp-server-transformers and select the transformers provider.
  • Everyone else never downloads transformers or ONNX. The main bundle never imports it, even at the type level (the loader uses minimal structural types).

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 - LangChain Embeddings implementation on top of the loader.
  • Provider plumbing - transformers added to the provider union and factory; areCredentialsAvailable returns true (no API key needed). Reuses main's generic dimension-resolution (from the recent Fix embedding dimension resolution for custom models #431 embedding-dimension rework) with no DocumentStore changes.
  • Also corrects the known BAAI/bge-small-en-v1.5 dimension from 512 to 384.

Distribution

  • Release (.releaserc.json, release.yml): the companion publishes in lockstep with the main package via a second @semantic-release/npm instance.
  • Docker: companion is bundled into the image so the container works offline out of the box, with TRANSFORMERS_CACHE=/models exposed as a volume.
  • CI (ci.yml, eval.yml): no changes needed - npm ci resolves the workspace and npm run build compiles the companion first (verified with a clean install).

Trade-offs worth a careful look

  • For a known model, a missing companion surfaces at first embed rather than at init (the generic resolver only probes dimensions for unknown models). This is documented in the design.
  • The root depends on the companion as a wildcard ("*") devDependency so lockstep version bumps never break workspace linking.

Notes

  • Includes an OpenSpec change (add-transformers-companion) capturing the rationale, design, and spec deltas retroactively.
  • Tests cover the loader error mapping, the embeddings class, and the new provider/dimension config paths.

arabold and others added 2 commits June 6, 2026 10:25
…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>
Copilot AI review requested due to automatic review settings June 6, 2026 17:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-transformers as 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 Embeddings implementation for transformers: models, and plumbs the provider through config/factory (including fixing BAAI/bge-small-en-v1.5 to 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.

Comment on lines +6 to +13
"main": "./dist/index.js",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./dist/index.js"
}
},

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 34 to 37
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.",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

arabold and others added 3 commits June 6, 2026 16:32
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants