@graphorin/embedder-transformersjs 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +6 -4
- package/src/index.ts +342 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @graphorin/embedder-transformersjs
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#154](https://github.com/o-stepper/graphorin/pull/154) [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04) Thanks [@o-stepper](https://github.com/o-stepper)! - W-072: every export map's `import` condition becomes `default`, and the Node floor rises to `>=22.12.0`.
|
|
8
|
+
|
|
9
|
+
CJS consumers previously hit a bewildering `ERR_PACKAGE_PATH_NOT_EXPORTED` instead of a clear ESM-only signal. With the `default` condition, plain `require('@graphorin/core')` works via Node's stable `require(esm)` - which shipped in 22.12, hence the engines bump across every workspace manifest (packages, examples, benchmarks, docs; enforced by the widened mvp-readiness sweep). No dual-instance hazard: there is no CJS build, `require()` returns the same ESM module instance. ESM consumers are unaffected (`default` serves both paths; `types` stays first). The pack gate now runs attw under the full `node16` profile (was `esm-only`) and adds a runtime `require(esm)` smoke against the packed tarballs. Installs on Node 22.0-22.11 with `engine-strict` will refuse - upgrade Node (see the migration guide).
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Tarballs now ship `src/` so the published `dist/**/*.d.ts.map` files actually work (W-136): the maps referenced `../src/*.ts` that the `files` whitelist excluded, so go-to-definition fell back into `.d.ts` and the shipped maps were dead weight. The pack gate gains a `map-integrity` leg: every source referenced by a shipped map must resolve inside the tarball (or be embedded via `sourcesContent`), with an anti-vacuous guard - a package whose tsdown config emits declaration maps must contain a non-zero number of `.d.ts.map` files, so a cache-restored dist that silently dropped maps fails the gate instead of passing vacuously. `mvp-readiness` now requires `src` in every publishable `files` array.
|
|
14
|
+
|
|
15
|
+
- [#164](https://github.com/o-stepper/graphorin/pull/164) [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00) Thanks [@o-stepper](https://github.com/o-stepper)! - Every published package now declares its tree-shaking contract via `sideEffects` (W-137): 18 packages audited to a pure module scope get `false`, the CLI declares its bin entry (`["./dist/bin/*"]`), and `@graphorin/security` gets an explicit `true` - its secrets subsystem registers built-in resolvers and the SecretValue caller-context provider at import time, so marking it pure would let bundlers drop those registrations. `mvp-readiness` now fails any publishable manifest without a declared `sideEffects`, closing the drift for future packages.
|
|
16
|
+
|
|
17
|
+
- Updated dependencies [[`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`832f22e`](https://github.com/o-stepper/graphorin/commit/832f22e570b8c3175c1adeb4c150070cbd131534), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`4ee256e`](https://github.com/o-stepper/graphorin/commit/4ee256e30fe9190cef6c48dc6785464757707156), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`32bbd03`](https://github.com/o-stepper/graphorin/commit/32bbd03b588136a355e4b5ad6ac5e19b36b4d8ab), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`764239b`](https://github.com/o-stepper/graphorin/commit/764239b97e0e0202442e91272583f13adeb12d00), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04), [`fe98522`](https://github.com/o-stepper/graphorin/commit/fe98522ce2477c9a7dc09029f9dcfdb1f7c9aa04)]:
|
|
18
|
+
- @graphorin/core@0.7.0
|
|
19
|
+
|
|
20
|
+
## 0.6.1
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [#142](https://github.com/o-stepper/graphorin/pull/142) [`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430) Thanks [@o-stepper](https://github.com/o-stepper)! - Version constants and version-bearing strings now derive from each package's manifest at build time (`VERSION = pkg.version`; writer ids, client/server info, OTLP framework attributes, build-info metrics interpolate it). No behavioral change at the current version: the rendered strings are byte-identical. A release bump no longer edits source; the new `check-version-consistency` gate fails any reintroduced hardcoded framework version.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [[`436d6ca`](https://github.com/o-stepper/graphorin/commit/436d6ca5ebbd16df094e915682d3915c279a8430)]:
|
|
27
|
+
- @graphorin/core@0.6.1
|
|
28
|
+
|
|
3
29
|
## 0.6.0
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -81,4 +81,4 @@ MIT © 2026 Oleksiy Stepurenko.
|
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
84
|
-
**Project Graphorin** · v0.
|
|
84
|
+
**Project Graphorin** · v0.7.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,7 @@ import { EmbedOptions, EmbedderProvider } from "@graphorin/core/contracts";
|
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
declare const VERSION = "0.6.0";
|
|
5
|
+
declare const VERSION: string;
|
|
7
6
|
/**
|
|
8
7
|
* Pooling strategy. Defaults to `'mean'` per the multilingual-e5 model
|
|
9
8
|
* card.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;AAwFY,cAvEC,OAuED,EAAA,MAAA;AAWZ;AAeA;AAWA;AAWA;;;AAqEU,KApLE,OAAA,GAoLF,MAAA,GAAA,KAAA,GAAA,aAAA,GAAA,KAAA,GAAA,YAAA,GAAA,MAAA;;;;;;AAmFM,UAhQC,6BAAA,CAgQmC;EAsBpC;EAYA,SAAA,KAAA,CAAA,EAAA,MAAmB;;qBA9Rd;;;;;;;;;;;;;;;;;;;;;;;6BAuBQ;;;;;;;;;;;;;;;;;;;;;;;;;KA0BjB,gBAAA;YAEY;;WAAuC;MAC1D;QACG;;;;;;;;;KAUI,eAAA;;;;;MAIP,QAAQ;;;;;;;cAWA,sBAAA,SAA+B,KAAA;;;;;;;;;;iBAW5B,4BAAA,WACL,gCACR;;;;;;cASU,sBAAA,YAAkC;;uBAexB;;;;;;;;;;;;;;eAqDZ,8BACD,eACL,QAAQ,cAAc;;;iBAkFX,oCAAA,CAAA;;;;;;;iBAsBA,SAAA;;;;;;;;;iBAYA,mBAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { version } from "./package.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
|
|
3
4
|
//#region src/index.ts
|
|
@@ -11,8 +12,8 @@ import { createHash } from "node:crypto";
|
|
|
11
12
|
*
|
|
12
13
|
* @packageDocumentation
|
|
13
14
|
*/
|
|
14
|
-
/** Canonical version constant
|
|
15
|
-
const VERSION =
|
|
15
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
16
|
+
const VERSION = version;
|
|
16
17
|
const DEFAULT_MODEL = "Xenova/multilingual-e5-base";
|
|
17
18
|
const DEFAULT_DIM = 768;
|
|
18
19
|
/**
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#model","#pooling","#normalize","#revision","#cacheDir","#dtype","#device","#pipelineFactory","#taskPrefix","#resolvedDim","#getExtractor","out: Float32Array[]","#extractor","#loading","CACHED_PIPELINE_FACTORY: PipelineFactory | null","KNOWN_DIMS: ReadonlyMap<string, number>"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/embedder-transformersjs - default in-process embedder.\n *\n * Wraps `@huggingface/transformers@^4.1.0` to produce dense embeddings\n * inside the Graphorin process. Default model\n * `Xenova/multilingual-e5-base` (768-dim, multilingual; DEC-130 /\n * ADR-025).\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type { EmbedderProvider, EmbedOptions } from '@graphorin/core/contracts';\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.6.0';\n\n/**\n * Pooling strategy. Defaults to `'mean'` per the multilingual-e5 model\n * card.\n *\n * @stable\n */\nexport type Pooling = 'mean' | 'cls' | 'first_token' | 'eos' | 'last_token' | 'none';\n\n/**\n * Configuration accepted by {@link createTransformersJsEmbedder}.\n *\n * @stable\n */\nexport interface TransformersJsEmbedderOptions {\n /** Default `'Xenova/multilingual-e5-base'` (768-dim). */\n readonly model?: string;\n /** Default `'mean'`. */\n readonly pooling?: Pooling;\n /** Default `true`. */\n readonly normalize?: boolean;\n /** Optional model revision pin (`'main'` if unset). */\n readonly revision?: string;\n /**\n * Optional cache directory. When unset, the embedder honours\n * `process.env.GRAPHORIN_CACHE_DIR`, otherwise falls back to the\n * Hugging Face default (`os.homedir()/.cache/huggingface/hub`).\n */\n readonly cacheDir?: string;\n /**\n * Optional dtype hint (`'fp32' | 'fp16' | 'q8' | 'q4'`). When unset,\n * the runtime picks the model's recommended default.\n */\n readonly dtype?: string;\n /** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */\n readonly device?: string;\n /**\n * Override the underlying `pipeline` factory - used by the test\n * suite to inject a stub. Production callers should leave this\n * unset so the package lazily loads `@huggingface/transformers`.\n */\n readonly pipelineFactory?: PipelineFactory;\n /**\n * Optional dimensionality hint. When the caller knows the output\n * dimension up-front, it is included in the canonical id without\n * waiting for the first `embed()` call.\n */\n readonly dim?: number;\n /**\n * Disable the automatic E5 `query:` / `passage:` prefixing (PS-10). The\n * prefixes are applied by default for E5-family models (the multilingual-e5\n * default and any model whose id carries an `e5` token), because the E5 model\n * card requires them and omitting them measurably degrades retrieval. Set\n * this to `true` only if your inputs are already prefixed or you use a\n * non-standard E5 export. Toggling it changes the canonical `configHash`\n * (and thus the embedder id), which triggers a re-embedding migration.\n */\n readonly disableTaskPrefix?: boolean;\n}\n\n/**\n * Tiny structural shape of `@huggingface/transformers`' feature-\n * extraction pipeline used by this package. Declared inline so the\n * embedder does not import the heavy peer at build time.\n *\n * @internal\n */\nexport type FeatureExtractor = (\n texts: string | readonly string[],\n options?: { pooling?: Pooling; normalize?: boolean; signal?: AbortSignal },\n) => Promise<{\n data: Float32Array;\n dims: readonly number[];\n tolist?(): unknown;\n}>;\n\n/**\n * Pipeline-factory shape used for dependency injection in tests.\n *\n * @internal\n */\nexport type PipelineFactory = (\n task: 'feature-extraction',\n model: string,\n opts: { revision?: string; cache_dir?: string; dtype?: string; device?: string },\n) => Promise<FeatureExtractor>;\n\nconst DEFAULT_MODEL = 'Xenova/multilingual-e5-base';\nconst DEFAULT_DIM = 768;\n\n/**\n * Raised when the underlying transformer model cannot be loaded\n * (offline / corporate firewall / wrong cache dir).\n *\n * @stable\n */\nexport class EmbedderModelLoadError extends Error {\n override readonly name = 'EmbedderModelLoadError';\n}\n\n/**\n * Build a `TransformersJsEmbedder` instance. Lazy: the underlying\n * pipeline is constructed on the first `embed()` call so packaging\n * the embedder does not pay the model-load cost.\n *\n * @stable\n */\nexport function createTransformersJsEmbedder(\n options: TransformersJsEmbedderOptions = {},\n): TransformersJsEmbedder {\n return new TransformersJsEmbedder(options);\n}\n\n/**\n * `EmbedderProvider` implementation backed by `@huggingface/transformers`.\n *\n * @stable\n */\nexport class TransformersJsEmbedder implements EmbedderProvider {\n readonly #model: string;\n readonly #pooling: Pooling;\n readonly #normalize: boolean;\n readonly #revision: string | undefined;\n readonly #cacheDir: string | undefined;\n readonly #dtype: string | undefined;\n readonly #device: string | undefined;\n readonly #pipelineFactory: PipelineFactory | undefined;\n /** Whether to apply E5 `query:` / `passage:` prefixes (PS-10). */\n readonly #taskPrefix: boolean;\n #extractor: FeatureExtractor | null = null;\n #loading: Promise<FeatureExtractor> | null = null;\n #resolvedDim: number | null;\n\n constructor(options: TransformersJsEmbedderOptions) {\n this.#model = options.model ?? DEFAULT_MODEL;\n this.#pooling = options.pooling ?? 'mean';\n this.#normalize = options.normalize ?? true;\n this.#revision = options.revision;\n this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;\n this.#dtype = options.dtype;\n this.#device = options.device;\n this.#pipelineFactory = options.pipelineFactory;\n this.#resolvedDim = options.dim ?? guessDefaultDim(this.#model);\n this.#taskPrefix = isE5Model(this.#model) && options.disableTaskPrefix !== true;\n }\n\n id(): string {\n return `transformersjs:${this.#model}@${this.dim()}`;\n }\n\n /**\n * Output dimension - the explicit `dim` option, a known-model\n * default, or the width resolved from the first `embed()`.\n * periphery-05 (the PS-11 fix ported from the Ollama embedder):\n * throws for an unknown model with no `dim` hint instead of silently\n * assuming 768 - a wrong assumed width bakes a wrong-width id AND a\n * wrong-width vec0 table, and the id then CHANGES after the first\n * `embed()` resolves the truth, which `lock-on-first` reads as an\n * embedder swap.\n */\n dim(): number {\n if (this.#resolvedDim !== null) return this.#resolvedDim;\n throw new Error(\n `[graphorin/embedder-transformersjs] Unknown embedding width for model '${this.#model}'. ` +\n `Pass { dim: <width> } to createTransformersJsEmbedder(...) (or call embed() once before ` +\n `registration so the width is resolved from the model output).`,\n );\n }\n\n configHash(): string {\n return canonicalConfigHash({\n adapter: 'transformersjs',\n model: this.#model,\n pooling: this.#pooling,\n normalize: this.#normalize,\n revision: this.#revision ?? null,\n dtype: this.#dtype ?? null,\n device: this.#device ?? null,\n // PS-10: the prefix policy changes the embeddings, so it must change the\n // id. Only added when active (E5 + not disabled) so non-E5 ids - and the\n // historical hash of an E5 model with prefixing turned off - are stable.\n ...(this.#taskPrefix ? { taskPrefix: 'e5' as const } : {}),\n });\n }\n\n async embed(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions = {},\n ): Promise<ReadonlyArray<Float32Array>> {\n if (texts.length === 0) return [];\n const extractor = await this.#getExtractor();\n // PS-10: E5 models require an asymmetric `query:` / `passage:` prefix.\n // Default to `passage` (the indexing role) when the caller doesn't specify.\n const inputs = this.#taskPrefix\n ? texts.map((t) => `${opts.taskType ?? 'passage'}: ${t}`)\n : [...texts];\n const result = await extractor(inputs, {\n pooling: this.#pooling,\n normalize: this.#normalize,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const lastDim = result.dims[result.dims.length - 1] ?? this.#resolvedDim ?? DEFAULT_DIM;\n // periphery-05: a width already published (via `dim` option or a\n // prior embed) must not silently drift - the vec0 table and the\n // canonical id were derived from it.\n if (this.#resolvedDim !== null && lastDim !== this.#resolvedDim) {\n throw new Error(\n `[graphorin/embedder-transformersjs] Model '${this.#model}' produced ${lastDim}-dim ` +\n `vectors but the embedder is bound to ${this.#resolvedDim} - check the 'dim' option.`,\n );\n }\n if (this.#resolvedDim === null) {\n this.#resolvedDim = lastDim;\n }\n const dim = lastDim;\n const out: Float32Array[] = [];\n for (let i = 0; i < texts.length; i++) {\n const start = i * dim;\n out.push(result.data.slice(start, start + dim));\n }\n return out;\n }\n\n async #getExtractor(): Promise<FeatureExtractor> {\n if (this.#extractor !== null) return this.#extractor;\n if (this.#loading !== null) return this.#loading;\n const loader = this.#pipelineFactory ?? (await loadDefaultPipelineFactory());\n this.#loading = loader('feature-extraction', this.#model, {\n ...(this.#revision !== undefined ? { revision: this.#revision } : {}),\n ...(this.#cacheDir !== undefined ? { cache_dir: this.#cacheDir } : {}),\n ...(this.#dtype !== undefined ? { dtype: this.#dtype } : {}),\n ...(this.#device !== undefined ? { device: this.#device } : {}),\n })\n .then((pipe) => {\n this.#extractor = pipe;\n this.#loading = null;\n return pipe;\n })\n .catch((err) => {\n this.#loading = null;\n throw new EmbedderModelLoadError(\n `[graphorin/embedder-transformersjs] failed to load model '${this.#model}'. ` +\n 'See the offline-install guide for instructions on pre-downloading the model.',\n { cause: err },\n );\n });\n return this.#loading;\n }\n}\n\n/** @internal */\nlet CACHED_PIPELINE_FACTORY: PipelineFactory | null = null;\n\nasync function loadDefaultPipelineFactory(): Promise<PipelineFactory> {\n if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;\n try {\n const mod = (await import('@huggingface/transformers')) as unknown as {\n pipeline: PipelineFactory;\n };\n CACHED_PIPELINE_FACTORY = mod.pipeline;\n return CACHED_PIPELINE_FACTORY;\n } catch (err) {\n throw new EmbedderModelLoadError(\n \"[graphorin/embedder-transformersjs] required peer '@huggingface/transformers' is not installed.\",\n { cause: err },\n );\n }\n}\n\n/** Test-only cache reset. */\nexport function _resetPipelineFactoryCacheForTesting(): void {\n CACHED_PIPELINE_FACTORY = null;\n}\n\nconst KNOWN_DIMS: ReadonlyMap<string, number> = new Map([\n ['Xenova/multilingual-e5-small', 384],\n ['Xenova/multilingual-e5-base', 768],\n ['Xenova/multilingual-e5-large', 1024],\n ['Xenova/bge-m3', 1024],\n ['onnx-community/all-MiniLM-L6-v2-ONNX', 384],\n]);\n\nfunction guessDefaultDim(model: string): number | null {\n return KNOWN_DIMS.get(model) ?? null;\n}\n\n/**\n * True when a model id belongs to the E5 family, which requires asymmetric\n * `query:` / `passage:` prefixes (PS-10). Matches an `e5` token bounded by a\n * path / dash / underscore so it covers `multilingual-e5-base`, `e5-large`,\n * `intfloat/e5-mistral`, etc. without false-matching unrelated names.\n */\nexport function isE5Model(model: string): boolean {\n return /(?:^|[/_-])e5(?:[/_-]|$)/i.test(model);\n}\n\n/**\n * Canonical-JSON deterministic hash of an embedder configuration.\n * Object keys are sorted lexicographically; primitives flow through as\n * `JSON.stringify` would render them. Used by the multi-table per-\n * embedder vec0 layout to tell drift apart from a true model swap.\n *\n * @stable\n */\nexport function canonicalConfigHash(config: unknown): string {\n const canonical = canonicalize(config);\n return createHash('sha256').update(canonical, 'utf8').digest('hex');\n}\n\nfunction canonicalize(value: unknown): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'number') return Number.isFinite(value) ? value.toString() : 'null';\n if (typeof value === 'string') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(',')}]`;\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`;\n }\n return 'null';\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["VERSION: string","pkg.version","#model","#pooling","#normalize","#revision","#cacheDir","#dtype","#device","#pipelineFactory","#taskPrefix","#resolvedDim","#getExtractor","out: Float32Array[]","#extractor","#loading","CACHED_PIPELINE_FACTORY: PipelineFactory | null","KNOWN_DIMS: ReadonlyMap<string, number>"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/embedder-transformersjs - default in-process embedder.\n *\n * Wraps `@huggingface/transformers@^4.1.0` to produce dense embeddings\n * inside the Graphorin process. Default model\n * `Xenova/multilingual-e5-base` (768-dim, multilingual; DEC-130 /\n * ADR-025).\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type { EmbedderProvider, EmbedOptions } from '@graphorin/core/contracts';\n\n/** Canonical version constant, derived from `package.json` at build time. */\nimport pkg from '../package.json' with { type: 'json' };\n\nexport const VERSION: string = pkg.version;\n\n/**\n * Pooling strategy. Defaults to `'mean'` per the multilingual-e5 model\n * card.\n *\n * @stable\n */\nexport type Pooling = 'mean' | 'cls' | 'first_token' | 'eos' | 'last_token' | 'none';\n\n/**\n * Configuration accepted by {@link createTransformersJsEmbedder}.\n *\n * @stable\n */\nexport interface TransformersJsEmbedderOptions {\n /** Default `'Xenova/multilingual-e5-base'` (768-dim). */\n readonly model?: string;\n /** Default `'mean'`. */\n readonly pooling?: Pooling;\n /** Default `true`. */\n readonly normalize?: boolean;\n /** Optional model revision pin (`'main'` if unset). */\n readonly revision?: string;\n /**\n * Optional cache directory. When unset, the embedder honours\n * `process.env.GRAPHORIN_CACHE_DIR`, otherwise falls back to the\n * Hugging Face default (`os.homedir()/.cache/huggingface/hub`).\n */\n readonly cacheDir?: string;\n /**\n * Optional dtype hint (`'fp32' | 'fp16' | 'q8' | 'q4'`). When unset,\n * the runtime picks the model's recommended default.\n */\n readonly dtype?: string;\n /** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */\n readonly device?: string;\n /**\n * Override the underlying `pipeline` factory - used by the test\n * suite to inject a stub. Production callers should leave this\n * unset so the package lazily loads `@huggingface/transformers`.\n */\n readonly pipelineFactory?: PipelineFactory;\n /**\n * Optional dimensionality hint. When the caller knows the output\n * dimension up-front, it is included in the canonical id without\n * waiting for the first `embed()` call.\n */\n readonly dim?: number;\n /**\n * Disable the automatic E5 `query:` / `passage:` prefixing (PS-10). The\n * prefixes are applied by default for E5-family models (the multilingual-e5\n * default and any model whose id carries an `e5` token), because the E5 model\n * card requires them and omitting them measurably degrades retrieval. Set\n * this to `true` only if your inputs are already prefixed or you use a\n * non-standard E5 export. Toggling it changes the canonical `configHash`\n * (and thus the embedder id), which triggers a re-embedding migration.\n */\n readonly disableTaskPrefix?: boolean;\n}\n\n/**\n * Tiny structural shape of `@huggingface/transformers`' feature-\n * extraction pipeline used by this package. Declared inline so the\n * embedder does not import the heavy peer at build time.\n *\n * @internal\n */\nexport type FeatureExtractor = (\n texts: string | readonly string[],\n options?: { pooling?: Pooling; normalize?: boolean; signal?: AbortSignal },\n) => Promise<{\n data: Float32Array;\n dims: readonly number[];\n tolist?(): unknown;\n}>;\n\n/**\n * Pipeline-factory shape used for dependency injection in tests.\n *\n * @internal\n */\nexport type PipelineFactory = (\n task: 'feature-extraction',\n model: string,\n opts: { revision?: string; cache_dir?: string; dtype?: string; device?: string },\n) => Promise<FeatureExtractor>;\n\nconst DEFAULT_MODEL = 'Xenova/multilingual-e5-base';\nconst DEFAULT_DIM = 768;\n\n/**\n * Raised when the underlying transformer model cannot be loaded\n * (offline / corporate firewall / wrong cache dir).\n *\n * @stable\n */\nexport class EmbedderModelLoadError extends Error {\n override readonly name = 'EmbedderModelLoadError';\n}\n\n/**\n * Build a `TransformersJsEmbedder` instance. Lazy: the underlying\n * pipeline is constructed on the first `embed()` call so packaging\n * the embedder does not pay the model-load cost.\n *\n * @stable\n */\nexport function createTransformersJsEmbedder(\n options: TransformersJsEmbedderOptions = {},\n): TransformersJsEmbedder {\n return new TransformersJsEmbedder(options);\n}\n\n/**\n * `EmbedderProvider` implementation backed by `@huggingface/transformers`.\n *\n * @stable\n */\nexport class TransformersJsEmbedder implements EmbedderProvider {\n readonly #model: string;\n readonly #pooling: Pooling;\n readonly #normalize: boolean;\n readonly #revision: string | undefined;\n readonly #cacheDir: string | undefined;\n readonly #dtype: string | undefined;\n readonly #device: string | undefined;\n readonly #pipelineFactory: PipelineFactory | undefined;\n /** Whether to apply E5 `query:` / `passage:` prefixes (PS-10). */\n readonly #taskPrefix: boolean;\n #extractor: FeatureExtractor | null = null;\n #loading: Promise<FeatureExtractor> | null = null;\n #resolvedDim: number | null;\n\n constructor(options: TransformersJsEmbedderOptions) {\n this.#model = options.model ?? DEFAULT_MODEL;\n this.#pooling = options.pooling ?? 'mean';\n this.#normalize = options.normalize ?? true;\n this.#revision = options.revision;\n this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;\n this.#dtype = options.dtype;\n this.#device = options.device;\n this.#pipelineFactory = options.pipelineFactory;\n this.#resolvedDim = options.dim ?? guessDefaultDim(this.#model);\n this.#taskPrefix = isE5Model(this.#model) && options.disableTaskPrefix !== true;\n }\n\n id(): string {\n return `transformersjs:${this.#model}@${this.dim()}`;\n }\n\n /**\n * Output dimension - the explicit `dim` option, a known-model\n * default, or the width resolved from the first `embed()`.\n * periphery-05 (the PS-11 fix ported from the Ollama embedder):\n * throws for an unknown model with no `dim` hint instead of silently\n * assuming 768 - a wrong assumed width bakes a wrong-width id AND a\n * wrong-width vec0 table, and the id then CHANGES after the first\n * `embed()` resolves the truth, which `lock-on-first` reads as an\n * embedder swap.\n */\n dim(): number {\n if (this.#resolvedDim !== null) return this.#resolvedDim;\n throw new Error(\n `[graphorin/embedder-transformersjs] Unknown embedding width for model '${this.#model}'. ` +\n `Pass { dim: <width> } to createTransformersJsEmbedder(...) (or call embed() once before ` +\n `registration so the width is resolved from the model output).`,\n );\n }\n\n configHash(): string {\n return canonicalConfigHash({\n adapter: 'transformersjs',\n model: this.#model,\n pooling: this.#pooling,\n normalize: this.#normalize,\n revision: this.#revision ?? null,\n dtype: this.#dtype ?? null,\n device: this.#device ?? null,\n // PS-10: the prefix policy changes the embeddings, so it must change the\n // id. Only added when active (E5 + not disabled) so non-E5 ids - and the\n // historical hash of an E5 model with prefixing turned off - are stable.\n ...(this.#taskPrefix ? { taskPrefix: 'e5' as const } : {}),\n });\n }\n\n async embed(\n texts: ReadonlyArray<string>,\n opts: EmbedOptions = {},\n ): Promise<ReadonlyArray<Float32Array>> {\n if (texts.length === 0) return [];\n const extractor = await this.#getExtractor();\n // PS-10: E5 models require an asymmetric `query:` / `passage:` prefix.\n // Default to `passage` (the indexing role) when the caller doesn't specify.\n const inputs = this.#taskPrefix\n ? texts.map((t) => `${opts.taskType ?? 'passage'}: ${t}`)\n : [...texts];\n const result = await extractor(inputs, {\n pooling: this.#pooling,\n normalize: this.#normalize,\n ...(opts.signal !== undefined ? { signal: opts.signal } : {}),\n });\n const lastDim = result.dims[result.dims.length - 1] ?? this.#resolvedDim ?? DEFAULT_DIM;\n // periphery-05: a width already published (via `dim` option or a\n // prior embed) must not silently drift - the vec0 table and the\n // canonical id were derived from it.\n if (this.#resolvedDim !== null && lastDim !== this.#resolvedDim) {\n throw new Error(\n `[graphorin/embedder-transformersjs] Model '${this.#model}' produced ${lastDim}-dim ` +\n `vectors but the embedder is bound to ${this.#resolvedDim} - check the 'dim' option.`,\n );\n }\n if (this.#resolvedDim === null) {\n this.#resolvedDim = lastDim;\n }\n const dim = lastDim;\n const out: Float32Array[] = [];\n for (let i = 0; i < texts.length; i++) {\n const start = i * dim;\n out.push(result.data.slice(start, start + dim));\n }\n return out;\n }\n\n async #getExtractor(): Promise<FeatureExtractor> {\n if (this.#extractor !== null) return this.#extractor;\n if (this.#loading !== null) return this.#loading;\n const loader = this.#pipelineFactory ?? (await loadDefaultPipelineFactory());\n this.#loading = loader('feature-extraction', this.#model, {\n ...(this.#revision !== undefined ? { revision: this.#revision } : {}),\n ...(this.#cacheDir !== undefined ? { cache_dir: this.#cacheDir } : {}),\n ...(this.#dtype !== undefined ? { dtype: this.#dtype } : {}),\n ...(this.#device !== undefined ? { device: this.#device } : {}),\n })\n .then((pipe) => {\n this.#extractor = pipe;\n this.#loading = null;\n return pipe;\n })\n .catch((err) => {\n this.#loading = null;\n throw new EmbedderModelLoadError(\n `[graphorin/embedder-transformersjs] failed to load model '${this.#model}'. ` +\n 'See the offline-install guide for instructions on pre-downloading the model.',\n { cause: err },\n );\n });\n return this.#loading;\n }\n}\n\n/** @internal */\nlet CACHED_PIPELINE_FACTORY: PipelineFactory | null = null;\n\nasync function loadDefaultPipelineFactory(): Promise<PipelineFactory> {\n if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;\n try {\n const mod = (await import('@huggingface/transformers')) as unknown as {\n pipeline: PipelineFactory;\n };\n CACHED_PIPELINE_FACTORY = mod.pipeline;\n return CACHED_PIPELINE_FACTORY;\n } catch (err) {\n throw new EmbedderModelLoadError(\n \"[graphorin/embedder-transformersjs] required peer '@huggingface/transformers' is not installed.\",\n { cause: err },\n );\n }\n}\n\n/** Test-only cache reset. */\nexport function _resetPipelineFactoryCacheForTesting(): void {\n CACHED_PIPELINE_FACTORY = null;\n}\n\nconst KNOWN_DIMS: ReadonlyMap<string, number> = new Map([\n ['Xenova/multilingual-e5-small', 384],\n ['Xenova/multilingual-e5-base', 768],\n ['Xenova/multilingual-e5-large', 1024],\n ['Xenova/bge-m3', 1024],\n ['onnx-community/all-MiniLM-L6-v2-ONNX', 384],\n]);\n\nfunction guessDefaultDim(model: string): number | null {\n return KNOWN_DIMS.get(model) ?? null;\n}\n\n/**\n * True when a model id belongs to the E5 family, which requires asymmetric\n * `query:` / `passage:` prefixes (PS-10). Matches an `e5` token bounded by a\n * path / dash / underscore so it covers `multilingual-e5-base`, `e5-large`,\n * `intfloat/e5-mistral`, etc. without false-matching unrelated names.\n */\nexport function isE5Model(model: string): boolean {\n return /(?:^|[/_-])e5(?:[/_-]|$)/i.test(model);\n}\n\n/**\n * Canonical-JSON deterministic hash of an embedder configuration.\n * Object keys are sorted lexicographically; primitives flow through as\n * `JSON.stringify` would render them. Used by the multi-table per-\n * embedder vec0 layout to tell drift apart from a true model swap.\n *\n * @stable\n */\nexport function canonicalConfigHash(config: unknown): string {\n const canonical = canonicalize(config);\n return createHash('sha256').update(canonical, 'utf8').digest('hex');\n}\n\nfunction canonicalize(value: unknown): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (typeof value === 'number') return Number.isFinite(value) ? value.toString() : 'null';\n if (typeof value === 'string') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(',')}]`;\n }\n if (typeof value === 'object') {\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`;\n }\n return 'null';\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBA,MAAaA,UAAkBC;AAwF/B,MAAM,gBAAgB;AACtB,MAAM,cAAc;;;;;;;AAQpB,IAAa,yBAAb,cAA4C,MAAM;CAChD,AAAkB,OAAO;;;;;;;;;AAU3B,SAAgB,6BACd,UAAyC,EAAE,EACnB;AACxB,QAAO,IAAI,uBAAuB,QAAQ;;;;;;;AAQ5C,IAAa,yBAAb,MAAgE;CAC9D,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;;CAET,CAASC;CACT,aAAsC;CACtC,WAA6C;CAC7C;CAEA,YAAY,SAAwC;AAClD,QAAKR,QAAS,QAAQ,SAAS;AAC/B,QAAKC,UAAW,QAAQ,WAAW;AACnC,QAAKC,YAAa,QAAQ,aAAa;AACvC,QAAKC,WAAY,QAAQ;AACzB,QAAKC,WAAY,QAAQ,YAAY,QAAQ,IAAI;AACjD,QAAKC,QAAS,QAAQ;AACtB,QAAKC,SAAU,QAAQ;AACvB,QAAKC,kBAAmB,QAAQ;AAChC,QAAKE,cAAe,QAAQ,OAAO,gBAAgB,MAAKT,MAAO;AAC/D,QAAKQ,aAAc,UAAU,MAAKR,MAAO,IAAI,QAAQ,sBAAsB;;CAG7E,KAAa;AACX,SAAO,kBAAkB,MAAKA,MAAO,GAAG,KAAK,KAAK;;;;;;;;;;;;CAapD,MAAc;AACZ,MAAI,MAAKS,gBAAiB,KAAM,QAAO,MAAKA;AAC5C,QAAM,IAAI,MACR,0EAA0E,MAAKT,MAAO,0JAGvF;;CAGH,aAAqB;AACnB,SAAO,oBAAoB;GACzB,SAAS;GACT,OAAO,MAAKA;GACZ,SAAS,MAAKC;GACd,WAAW,MAAKC;GAChB,UAAU,MAAKC,YAAa;GAC5B,OAAO,MAAKE,SAAU;GACtB,QAAQ,MAAKC,UAAW;GAIxB,GAAI,MAAKE,aAAc,EAAE,YAAY,MAAe,GAAG,EAAE;GAC1D,CAAC;;CAGJ,MAAM,MACJ,OACA,OAAqB,EAAE,EACe;AACtC,MAAI,MAAM,WAAW,EAAG,QAAO,EAAE;EAOjC,MAAM,SAAS,OANG,MAAM,MAAKE,cAAe,EAG7B,MAAKF,aAChB,MAAM,KAAK,MAAM,GAAG,KAAK,YAAY,UAAU,IAAI,IAAI,GACvD,CAAC,GAAG,MAAM,EACyB;GACrC,SAAS,MAAKP;GACd,WAAW,MAAKC;GAChB,GAAI,KAAK,WAAW,SAAY,EAAE,QAAQ,KAAK,QAAQ,GAAG,EAAE;GAC7D,CAAC;EACF,MAAM,UAAU,OAAO,KAAK,OAAO,KAAK,SAAS,MAAM,MAAKO,eAAgB;AAI5E,MAAI,MAAKA,gBAAiB,QAAQ,YAAY,MAAKA,YACjD,OAAM,IAAI,MACR,8CAA8C,MAAKT,MAAO,aAAa,QAAQ,4CACrC,MAAKS,YAAa,4BAC7D;AAEH,MAAI,MAAKA,gBAAiB,KACxB,OAAKA,cAAe;EAEtB,MAAM,MAAM;EACZ,MAAME,MAAsB,EAAE;AAC9B,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACrC,MAAM,QAAQ,IAAI;AAClB,OAAI,KAAK,OAAO,KAAK,MAAM,OAAO,QAAQ,IAAI,CAAC;;AAEjD,SAAO;;CAGT,OAAMD,eAA2C;AAC/C,MAAI,MAAKE,cAAe,KAAM,QAAO,MAAKA;AAC1C,MAAI,MAAKC,YAAa,KAAM,QAAO,MAAKA;AAExC,QAAKA,WADU,MAAKN,mBAAqB,MAAM,4BAA4B,EACpD,sBAAsB,MAAKP,OAAQ;GACxD,GAAI,MAAKG,aAAc,SAAY,EAAE,UAAU,MAAKA,UAAW,GAAG,EAAE;GACpE,GAAI,MAAKC,aAAc,SAAY,EAAE,WAAW,MAAKA,UAAW,GAAG,EAAE;GACrE,GAAI,MAAKC,UAAW,SAAY,EAAE,OAAO,MAAKA,OAAQ,GAAG,EAAE;GAC3D,GAAI,MAAKC,WAAY,SAAY,EAAE,QAAQ,MAAKA,QAAS,GAAG,EAAE;GAC/D,CAAC,CACC,MAAM,SAAS;AACd,SAAKM,YAAa;AAClB,SAAKC,UAAW;AAChB,UAAO;IACP,CACD,OAAO,QAAQ;AACd,SAAKA,UAAW;AAChB,SAAM,IAAI,uBACR,6DAA6D,MAAKb,MAAO,kFAEzE,EAAE,OAAO,KAAK,CACf;IACD;AACJ,SAAO,MAAKa;;;;AAKhB,IAAIC,0BAAkD;AAEtD,eAAe,6BAAuD;AACpE,KAAI,4BAA4B,KAAM,QAAO;AAC7C,KAAI;AAIF,6BAHa,MAAM,OAAO,8BAGI;AAC9B,SAAO;UACA,KAAK;AACZ,QAAM,IAAI,uBACR,mGACA,EAAE,OAAO,KAAK,CACf;;;;AAKL,SAAgB,uCAA6C;AAC3D,2BAA0B;;AAG5B,MAAMC,aAA0C,IAAI,IAAI;CACtD,CAAC,gCAAgC,IAAI;CACrC,CAAC,+BAA+B,IAAI;CACpC,CAAC,gCAAgC,KAAK;CACtC,CAAC,iBAAiB,KAAK;CACvB,CAAC,wCAAwC,IAAI;CAC9C,CAAC;AAEF,SAAS,gBAAgB,OAA8B;AACrD,QAAO,WAAW,IAAI,MAAM,IAAI;;;;;;;;AASlC,SAAgB,UAAU,OAAwB;AAChD,QAAO,4BAA4B,KAAK,MAAM;;;;;;;;;;AAWhD,SAAgB,oBAAoB,QAAyB;CAC3D,MAAM,YAAY,aAAa,OAAO;AACtC,QAAO,WAAW,SAAS,CAAC,OAAO,WAAW,OAAO,CAAC,OAAO,MAAM;;AAGrE,SAAS,aAAa,OAAwB;AAC5C,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,SAAS;AACxD,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,MAAM,GAAG,MAAM,UAAU,GAAG;AAClF,KAAI,OAAO,UAAU,SAAU,QAAO,KAAK,UAAU,MAAM;AAC3D,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,IAAI,MAAM,IAAI,aAAa,CAAC,KAAK,IAAI,CAAC;AAE/C,KAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,MAAM;AAEZ,SAAO,IADM,OAAO,KAAK,IAAI,CAAC,MAAM,CACpB,KAAK,MAAM,GAAG,KAAK,UAAU,EAAE,CAAC,GAAG,aAAa,IAAI,GAAG,GAAG,CAAC,KAAK,IAAI,CAAC;;AAEvF,QAAO"}
|
package/dist/package.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/embedder-transformersjs\",\n \"version\": \"0.7.0\",\n \"description\": \"Default in-process embedder for the Graphorin framework. Wraps `@huggingface/transformers@^4.1.0` to produce dense vectors with the multilingual `Xenova/multilingual-e5-base` model (768-dim) by default. Implements the `EmbedderProvider` contract from `@graphorin/core/contracts` with deterministic `configHash`, batched embed, lazy model download (cache honours `GRAPHORIN_CACHE_DIR`), and an actionable error if the model cannot be fetched.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/embedder-transformersjs\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/embedder-transformersjs\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"embeddings\",\n \"transformersjs\",\n \"huggingface\",\n \"multilingual\"\n ],\n \"type\": \"module\",\n \"sideEffects\": false,\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"@huggingface/transformers\": \"^4.1.0\"\n },\n \"peerDependenciesMeta\": {\n \"@huggingface/transformers\": {\n \"optional\": false\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@huggingface/transformers\": \"^4.1.0\"\n }\n}\n"],"mappings":";cAEa"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphorin/embedder-transformersjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Default in-process embedder for the Graphorin framework. Wraps `@huggingface/transformers@^4.1.0` to produce dense vectors with the multilingual `Xenova/multilingual-e5-base` model (768-dim) by default. Implements the `EmbedderProvider` contract from `@graphorin/core/contracts` with deterministic `configHash`, batched embed, lazy model download (cache honours `GRAPHORIN_CACHE_DIR`), and an actionable error if the model cannot be fetched.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Oleksiy Stepurenko",
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
"multilingual"
|
|
25
25
|
],
|
|
26
26
|
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
27
28
|
"engines": {
|
|
28
|
-
"node": ">=22.
|
|
29
|
+
"node": ">=22.12.0"
|
|
29
30
|
},
|
|
30
31
|
"main": "./dist/index.js",
|
|
31
32
|
"module": "./dist/index.js",
|
|
@@ -33,18 +34,19 @@
|
|
|
33
34
|
"exports": {
|
|
34
35
|
".": {
|
|
35
36
|
"types": "./dist/index.d.ts",
|
|
36
|
-
"
|
|
37
|
+
"default": "./dist/index.js"
|
|
37
38
|
},
|
|
38
39
|
"./package.json": "./package.json"
|
|
39
40
|
},
|
|
40
41
|
"files": [
|
|
41
42
|
"dist",
|
|
43
|
+
"src",
|
|
42
44
|
"README.md",
|
|
43
45
|
"CHANGELOG.md",
|
|
44
46
|
"LICENSE"
|
|
45
47
|
],
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@graphorin/core": "0.
|
|
49
|
+
"@graphorin/core": "0.7.0"
|
|
48
50
|
},
|
|
49
51
|
"peerDependencies": {
|
|
50
52
|
"@huggingface/transformers": "^4.1.0"
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @graphorin/embedder-transformersjs - default in-process embedder.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `@huggingface/transformers@^4.1.0` to produce dense embeddings
|
|
5
|
+
* inside the Graphorin process. Default model
|
|
6
|
+
* `Xenova/multilingual-e5-base` (768-dim, multilingual; DEC-130 /
|
|
7
|
+
* ADR-025).
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import type { EmbedderProvider, EmbedOptions } from '@graphorin/core/contracts';
|
|
14
|
+
|
|
15
|
+
/** Canonical version constant, derived from `package.json` at build time. */
|
|
16
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
17
|
+
|
|
18
|
+
export const VERSION: string = pkg.version;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pooling strategy. Defaults to `'mean'` per the multilingual-e5 model
|
|
22
|
+
* card.
|
|
23
|
+
*
|
|
24
|
+
* @stable
|
|
25
|
+
*/
|
|
26
|
+
export type Pooling = 'mean' | 'cls' | 'first_token' | 'eos' | 'last_token' | 'none';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration accepted by {@link createTransformersJsEmbedder}.
|
|
30
|
+
*
|
|
31
|
+
* @stable
|
|
32
|
+
*/
|
|
33
|
+
export interface TransformersJsEmbedderOptions {
|
|
34
|
+
/** Default `'Xenova/multilingual-e5-base'` (768-dim). */
|
|
35
|
+
readonly model?: string;
|
|
36
|
+
/** Default `'mean'`. */
|
|
37
|
+
readonly pooling?: Pooling;
|
|
38
|
+
/** Default `true`. */
|
|
39
|
+
readonly normalize?: boolean;
|
|
40
|
+
/** Optional model revision pin (`'main'` if unset). */
|
|
41
|
+
readonly revision?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Optional cache directory. When unset, the embedder honours
|
|
44
|
+
* `process.env.GRAPHORIN_CACHE_DIR`, otherwise falls back to the
|
|
45
|
+
* Hugging Face default (`os.homedir()/.cache/huggingface/hub`).
|
|
46
|
+
*/
|
|
47
|
+
readonly cacheDir?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Optional dtype hint (`'fp32' | 'fp16' | 'q8' | 'q4'`). When unset,
|
|
50
|
+
* the runtime picks the model's recommended default.
|
|
51
|
+
*/
|
|
52
|
+
readonly dtype?: string;
|
|
53
|
+
/** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */
|
|
54
|
+
readonly device?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Override the underlying `pipeline` factory - used by the test
|
|
57
|
+
* suite to inject a stub. Production callers should leave this
|
|
58
|
+
* unset so the package lazily loads `@huggingface/transformers`.
|
|
59
|
+
*/
|
|
60
|
+
readonly pipelineFactory?: PipelineFactory;
|
|
61
|
+
/**
|
|
62
|
+
* Optional dimensionality hint. When the caller knows the output
|
|
63
|
+
* dimension up-front, it is included in the canonical id without
|
|
64
|
+
* waiting for the first `embed()` call.
|
|
65
|
+
*/
|
|
66
|
+
readonly dim?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Disable the automatic E5 `query:` / `passage:` prefixing (PS-10). The
|
|
69
|
+
* prefixes are applied by default for E5-family models (the multilingual-e5
|
|
70
|
+
* default and any model whose id carries an `e5` token), because the E5 model
|
|
71
|
+
* card requires them and omitting them measurably degrades retrieval. Set
|
|
72
|
+
* this to `true` only if your inputs are already prefixed or you use a
|
|
73
|
+
* non-standard E5 export. Toggling it changes the canonical `configHash`
|
|
74
|
+
* (and thus the embedder id), which triggers a re-embedding migration.
|
|
75
|
+
*/
|
|
76
|
+
readonly disableTaskPrefix?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Tiny structural shape of `@huggingface/transformers`' feature-
|
|
81
|
+
* extraction pipeline used by this package. Declared inline so the
|
|
82
|
+
* embedder does not import the heavy peer at build time.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
export type FeatureExtractor = (
|
|
87
|
+
texts: string | readonly string[],
|
|
88
|
+
options?: { pooling?: Pooling; normalize?: boolean; signal?: AbortSignal },
|
|
89
|
+
) => Promise<{
|
|
90
|
+
data: Float32Array;
|
|
91
|
+
dims: readonly number[];
|
|
92
|
+
tolist?(): unknown;
|
|
93
|
+
}>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Pipeline-factory shape used for dependency injection in tests.
|
|
97
|
+
*
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
export type PipelineFactory = (
|
|
101
|
+
task: 'feature-extraction',
|
|
102
|
+
model: string,
|
|
103
|
+
opts: { revision?: string; cache_dir?: string; dtype?: string; device?: string },
|
|
104
|
+
) => Promise<FeatureExtractor>;
|
|
105
|
+
|
|
106
|
+
const DEFAULT_MODEL = 'Xenova/multilingual-e5-base';
|
|
107
|
+
const DEFAULT_DIM = 768;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Raised when the underlying transformer model cannot be loaded
|
|
111
|
+
* (offline / corporate firewall / wrong cache dir).
|
|
112
|
+
*
|
|
113
|
+
* @stable
|
|
114
|
+
*/
|
|
115
|
+
export class EmbedderModelLoadError extends Error {
|
|
116
|
+
override readonly name = 'EmbedderModelLoadError';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a `TransformersJsEmbedder` instance. Lazy: the underlying
|
|
121
|
+
* pipeline is constructed on the first `embed()` call so packaging
|
|
122
|
+
* the embedder does not pay the model-load cost.
|
|
123
|
+
*
|
|
124
|
+
* @stable
|
|
125
|
+
*/
|
|
126
|
+
export function createTransformersJsEmbedder(
|
|
127
|
+
options: TransformersJsEmbedderOptions = {},
|
|
128
|
+
): TransformersJsEmbedder {
|
|
129
|
+
return new TransformersJsEmbedder(options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `EmbedderProvider` implementation backed by `@huggingface/transformers`.
|
|
134
|
+
*
|
|
135
|
+
* @stable
|
|
136
|
+
*/
|
|
137
|
+
export class TransformersJsEmbedder implements EmbedderProvider {
|
|
138
|
+
readonly #model: string;
|
|
139
|
+
readonly #pooling: Pooling;
|
|
140
|
+
readonly #normalize: boolean;
|
|
141
|
+
readonly #revision: string | undefined;
|
|
142
|
+
readonly #cacheDir: string | undefined;
|
|
143
|
+
readonly #dtype: string | undefined;
|
|
144
|
+
readonly #device: string | undefined;
|
|
145
|
+
readonly #pipelineFactory: PipelineFactory | undefined;
|
|
146
|
+
/** Whether to apply E5 `query:` / `passage:` prefixes (PS-10). */
|
|
147
|
+
readonly #taskPrefix: boolean;
|
|
148
|
+
#extractor: FeatureExtractor | null = null;
|
|
149
|
+
#loading: Promise<FeatureExtractor> | null = null;
|
|
150
|
+
#resolvedDim: number | null;
|
|
151
|
+
|
|
152
|
+
constructor(options: TransformersJsEmbedderOptions) {
|
|
153
|
+
this.#model = options.model ?? DEFAULT_MODEL;
|
|
154
|
+
this.#pooling = options.pooling ?? 'mean';
|
|
155
|
+
this.#normalize = options.normalize ?? true;
|
|
156
|
+
this.#revision = options.revision;
|
|
157
|
+
this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;
|
|
158
|
+
this.#dtype = options.dtype;
|
|
159
|
+
this.#device = options.device;
|
|
160
|
+
this.#pipelineFactory = options.pipelineFactory;
|
|
161
|
+
this.#resolvedDim = options.dim ?? guessDefaultDim(this.#model);
|
|
162
|
+
this.#taskPrefix = isE5Model(this.#model) && options.disableTaskPrefix !== true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
id(): string {
|
|
166
|
+
return `transformersjs:${this.#model}@${this.dim()}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Output dimension - the explicit `dim` option, a known-model
|
|
171
|
+
* default, or the width resolved from the first `embed()`.
|
|
172
|
+
* periphery-05 (the PS-11 fix ported from the Ollama embedder):
|
|
173
|
+
* throws for an unknown model with no `dim` hint instead of silently
|
|
174
|
+
* assuming 768 - a wrong assumed width bakes a wrong-width id AND a
|
|
175
|
+
* wrong-width vec0 table, and the id then CHANGES after the first
|
|
176
|
+
* `embed()` resolves the truth, which `lock-on-first` reads as an
|
|
177
|
+
* embedder swap.
|
|
178
|
+
*/
|
|
179
|
+
dim(): number {
|
|
180
|
+
if (this.#resolvedDim !== null) return this.#resolvedDim;
|
|
181
|
+
throw new Error(
|
|
182
|
+
`[graphorin/embedder-transformersjs] Unknown embedding width for model '${this.#model}'. ` +
|
|
183
|
+
`Pass { dim: <width> } to createTransformersJsEmbedder(...) (or call embed() once before ` +
|
|
184
|
+
`registration so the width is resolved from the model output).`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
configHash(): string {
|
|
189
|
+
return canonicalConfigHash({
|
|
190
|
+
adapter: 'transformersjs',
|
|
191
|
+
model: this.#model,
|
|
192
|
+
pooling: this.#pooling,
|
|
193
|
+
normalize: this.#normalize,
|
|
194
|
+
revision: this.#revision ?? null,
|
|
195
|
+
dtype: this.#dtype ?? null,
|
|
196
|
+
device: this.#device ?? null,
|
|
197
|
+
// PS-10: the prefix policy changes the embeddings, so it must change the
|
|
198
|
+
// id. Only added when active (E5 + not disabled) so non-E5 ids - and the
|
|
199
|
+
// historical hash of an E5 model with prefixing turned off - are stable.
|
|
200
|
+
...(this.#taskPrefix ? { taskPrefix: 'e5' as const } : {}),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async embed(
|
|
205
|
+
texts: ReadonlyArray<string>,
|
|
206
|
+
opts: EmbedOptions = {},
|
|
207
|
+
): Promise<ReadonlyArray<Float32Array>> {
|
|
208
|
+
if (texts.length === 0) return [];
|
|
209
|
+
const extractor = await this.#getExtractor();
|
|
210
|
+
// PS-10: E5 models require an asymmetric `query:` / `passage:` prefix.
|
|
211
|
+
// Default to `passage` (the indexing role) when the caller doesn't specify.
|
|
212
|
+
const inputs = this.#taskPrefix
|
|
213
|
+
? texts.map((t) => `${opts.taskType ?? 'passage'}: ${t}`)
|
|
214
|
+
: [...texts];
|
|
215
|
+
const result = await extractor(inputs, {
|
|
216
|
+
pooling: this.#pooling,
|
|
217
|
+
normalize: this.#normalize,
|
|
218
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
219
|
+
});
|
|
220
|
+
const lastDim = result.dims[result.dims.length - 1] ?? this.#resolvedDim ?? DEFAULT_DIM;
|
|
221
|
+
// periphery-05: a width already published (via `dim` option or a
|
|
222
|
+
// prior embed) must not silently drift - the vec0 table and the
|
|
223
|
+
// canonical id were derived from it.
|
|
224
|
+
if (this.#resolvedDim !== null && lastDim !== this.#resolvedDim) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`[graphorin/embedder-transformersjs] Model '${this.#model}' produced ${lastDim}-dim ` +
|
|
227
|
+
`vectors but the embedder is bound to ${this.#resolvedDim} - check the 'dim' option.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (this.#resolvedDim === null) {
|
|
231
|
+
this.#resolvedDim = lastDim;
|
|
232
|
+
}
|
|
233
|
+
const dim = lastDim;
|
|
234
|
+
const out: Float32Array[] = [];
|
|
235
|
+
for (let i = 0; i < texts.length; i++) {
|
|
236
|
+
const start = i * dim;
|
|
237
|
+
out.push(result.data.slice(start, start + dim));
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async #getExtractor(): Promise<FeatureExtractor> {
|
|
243
|
+
if (this.#extractor !== null) return this.#extractor;
|
|
244
|
+
if (this.#loading !== null) return this.#loading;
|
|
245
|
+
const loader = this.#pipelineFactory ?? (await loadDefaultPipelineFactory());
|
|
246
|
+
this.#loading = loader('feature-extraction', this.#model, {
|
|
247
|
+
...(this.#revision !== undefined ? { revision: this.#revision } : {}),
|
|
248
|
+
...(this.#cacheDir !== undefined ? { cache_dir: this.#cacheDir } : {}),
|
|
249
|
+
...(this.#dtype !== undefined ? { dtype: this.#dtype } : {}),
|
|
250
|
+
...(this.#device !== undefined ? { device: this.#device } : {}),
|
|
251
|
+
})
|
|
252
|
+
.then((pipe) => {
|
|
253
|
+
this.#extractor = pipe;
|
|
254
|
+
this.#loading = null;
|
|
255
|
+
return pipe;
|
|
256
|
+
})
|
|
257
|
+
.catch((err) => {
|
|
258
|
+
this.#loading = null;
|
|
259
|
+
throw new EmbedderModelLoadError(
|
|
260
|
+
`[graphorin/embedder-transformersjs] failed to load model '${this.#model}'. ` +
|
|
261
|
+
'See the offline-install guide for instructions on pre-downloading the model.',
|
|
262
|
+
{ cause: err },
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
return this.#loading;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** @internal */
|
|
270
|
+
let CACHED_PIPELINE_FACTORY: PipelineFactory | null = null;
|
|
271
|
+
|
|
272
|
+
async function loadDefaultPipelineFactory(): Promise<PipelineFactory> {
|
|
273
|
+
if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;
|
|
274
|
+
try {
|
|
275
|
+
const mod = (await import('@huggingface/transformers')) as unknown as {
|
|
276
|
+
pipeline: PipelineFactory;
|
|
277
|
+
};
|
|
278
|
+
CACHED_PIPELINE_FACTORY = mod.pipeline;
|
|
279
|
+
return CACHED_PIPELINE_FACTORY;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
throw new EmbedderModelLoadError(
|
|
282
|
+
"[graphorin/embedder-transformersjs] required peer '@huggingface/transformers' is not installed.",
|
|
283
|
+
{ cause: err },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Test-only cache reset. */
|
|
289
|
+
export function _resetPipelineFactoryCacheForTesting(): void {
|
|
290
|
+
CACHED_PIPELINE_FACTORY = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const KNOWN_DIMS: ReadonlyMap<string, number> = new Map([
|
|
294
|
+
['Xenova/multilingual-e5-small', 384],
|
|
295
|
+
['Xenova/multilingual-e5-base', 768],
|
|
296
|
+
['Xenova/multilingual-e5-large', 1024],
|
|
297
|
+
['Xenova/bge-m3', 1024],
|
|
298
|
+
['onnx-community/all-MiniLM-L6-v2-ONNX', 384],
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
function guessDefaultDim(model: string): number | null {
|
|
302
|
+
return KNOWN_DIMS.get(model) ?? null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* True when a model id belongs to the E5 family, which requires asymmetric
|
|
307
|
+
* `query:` / `passage:` prefixes (PS-10). Matches an `e5` token bounded by a
|
|
308
|
+
* path / dash / underscore so it covers `multilingual-e5-base`, `e5-large`,
|
|
309
|
+
* `intfloat/e5-mistral`, etc. without false-matching unrelated names.
|
|
310
|
+
*/
|
|
311
|
+
export function isE5Model(model: string): boolean {
|
|
312
|
+
return /(?:^|[/_-])e5(?:[/_-]|$)/i.test(model);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Canonical-JSON deterministic hash of an embedder configuration.
|
|
317
|
+
* Object keys are sorted lexicographically; primitives flow through as
|
|
318
|
+
* `JSON.stringify` would render them. Used by the multi-table per-
|
|
319
|
+
* embedder vec0 layout to tell drift apart from a true model swap.
|
|
320
|
+
*
|
|
321
|
+
* @stable
|
|
322
|
+
*/
|
|
323
|
+
export function canonicalConfigHash(config: unknown): string {
|
|
324
|
+
const canonical = canonicalize(config);
|
|
325
|
+
return createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function canonicalize(value: unknown): string {
|
|
329
|
+
if (value === null) return 'null';
|
|
330
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
331
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value.toString() : 'null';
|
|
332
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
333
|
+
if (Array.isArray(value)) {
|
|
334
|
+
return `[${value.map(canonicalize).join(',')}]`;
|
|
335
|
+
}
|
|
336
|
+
if (typeof value === 'object') {
|
|
337
|
+
const obj = value as Record<string, unknown>;
|
|
338
|
+
const keys = Object.keys(obj).sort();
|
|
339
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`;
|
|
340
|
+
}
|
|
341
|
+
return 'null';
|
|
342
|
+
}
|