@graphorin/embedder-transformersjs 0.5.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 ADDED
@@ -0,0 +1,9 @@
1
+ # @graphorin/embedder-transformersjs
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial release. Default in-process embedder for the Graphorin
8
+ framework — wraps `@huggingface/transformers` for the multilingual
9
+ `Xenova/multilingual-e5-base` model.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksiy Stepurenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @graphorin/embedder-transformersjs
2
+
3
+ > Default in-process embedder for the Graphorin framework.
4
+
5
+ `@graphorin/embedder-transformersjs` wraps `@huggingface/transformers@^4.1.0`
6
+ to produce dense embeddings inside the Graphorin process — no external
7
+ service, no network call after the first model download. The default
8
+ model is `Xenova/multilingual-e5-base` (768-dim, multilingual). Other
9
+ multilingual variants from the same family (`multilingual-e5-small`,
10
+ `multilingual-e5-large`, `bge-m3`) are first-class through the same
11
+ factory.
12
+
13
+ The package implements the `EmbedderProvider` contract from
14
+ `@graphorin/core/contracts`:
15
+
16
+ - `id()` — canonical id, e.g.
17
+ `'transformersjs:Xenova/multilingual-e5-base@768'`.
18
+ - `dim()` — output vector dimensionality (resolved at first `embed`).
19
+ - `configHash()` — deterministic hex hash over canonical-JSON config.
20
+ - `embed(texts)` — batched, lazy. Returns one `Float32Array` per text.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pnpm add @graphorin/embedder-transformersjs @huggingface/transformers
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```ts
31
+ import { createTransformersJsEmbedder } from '@graphorin/embedder-transformersjs';
32
+
33
+ const embedder = createTransformersJsEmbedder({
34
+ model: 'Xenova/multilingual-e5-base',
35
+ pooling: 'mean',
36
+ normalize: true,
37
+ });
38
+
39
+ const [vec] = await embedder.embed(['Loves espresso.']);
40
+ console.log(embedder.id(), embedder.dim(), vec.length);
41
+ ```
42
+
43
+ ## E5 asymmetric prefixes
44
+
45
+ E5-family models (the default, and any model id carrying an `e5` token) require
46
+ asymmetric `query:` / `passage:` prefixes — omitting them measurably degrades
47
+ retrieval. The embedder applies them automatically: pass `taskType: 'query'`
48
+ when embedding a search query and `taskType: 'passage'` (the default) for stored
49
+ content. The Graphorin memory tiers thread this for you (`query` on search,
50
+ `passage` on `remember`).
51
+
52
+ > **Migration:** enabling the prefixes (the new default for E5) changes the
53
+ > embedder's `configHash`, and therefore its `id`. Under the default
54
+ > `lock-on-first` policy an existing index built before this change reports an
55
+ > embedder mismatch — run `graphorin memory migrate` to re-embed it, or pass
56
+ > `disableTaskPrefix: true` to keep the old (unprefixed) behaviour and id.
57
+
58
+ ## Cache directory
59
+
60
+ The Hugging Face model cache lives under `os.homedir()/.cache/...` by
61
+ default. Override with the standard env var or pass it explicitly:
62
+
63
+ ```bash
64
+ GRAPHORIN_CACHE_DIR=/var/lib/graphorin/models pnpm start
65
+ ```
66
+
67
+ ## Network behaviour
68
+
69
+ The embedder is **local-first**: the only outbound call is the first
70
+ model download (cached after success). The
71
+ `scripts/check-no-network.mjs` static guard explicitly allow-lists
72
+ `packages/embedder-*/` for this reason.
73
+
74
+ If the model cannot be downloaded (offline / corporate firewall), the
75
+ embedder throws an `EmbedderModelLoadError` with an actionable message:
76
+ the Phase 17 docs cover the offline-install procedure.
77
+
78
+ ## License
79
+
80
+ MIT © 2026 Oleksiy Stepurenko.
81
+
82
+ ---
83
+
84
+ **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
@@ -0,0 +1,141 @@
1
+ import { EmbedOptions, EmbedderProvider } from "@graphorin/core/contracts";
2
+
3
+ //#region src/index.d.ts
4
+
5
+ /** Canonical version constant. Mirrors the `package.json` version. */
6
+ declare const VERSION = "0.5.0";
7
+ /**
8
+ * Pooling strategy. Defaults to `'mean'` per the multilingual-e5 model
9
+ * card.
10
+ *
11
+ * @stable
12
+ */
13
+ type Pooling = 'mean' | 'cls' | 'first_token' | 'eos' | 'last_token' | 'none';
14
+ /**
15
+ * Configuration accepted by {@link createTransformersJsEmbedder}.
16
+ *
17
+ * @stable
18
+ */
19
+ interface TransformersJsEmbedderOptions {
20
+ /** Default `'Xenova/multilingual-e5-base'` (768-dim). */
21
+ readonly model?: string;
22
+ /** Default `'mean'`. */
23
+ readonly pooling?: Pooling;
24
+ /** Default `true`. */
25
+ readonly normalize?: boolean;
26
+ /** Optional model revision pin (`'main'` if unset). */
27
+ readonly revision?: string;
28
+ /**
29
+ * Optional cache directory. When unset, the embedder honours
30
+ * `process.env.GRAPHORIN_CACHE_DIR`, otherwise falls back to the
31
+ * Hugging Face default (`os.homedir()/.cache/huggingface/hub`).
32
+ */
33
+ readonly cacheDir?: string;
34
+ /**
35
+ * Optional dtype hint (`'fp32' | 'fp16' | 'q8' | 'q4'`). When unset,
36
+ * the runtime picks the model's recommended default.
37
+ */
38
+ readonly dtype?: string;
39
+ /** Override device (`'cpu'`, `'webgpu'`, …). Default `'cpu'`. */
40
+ readonly device?: string;
41
+ /**
42
+ * Override the underlying `pipeline` factory — used by the test
43
+ * suite to inject a stub. Production callers should leave this
44
+ * unset so the package lazily loads `@huggingface/transformers`.
45
+ */
46
+ readonly pipelineFactory?: PipelineFactory;
47
+ /**
48
+ * Optional dimensionality hint. When the caller knows the output
49
+ * dimension up-front, it is included in the canonical id without
50
+ * waiting for the first `embed()` call.
51
+ */
52
+ readonly dim?: number;
53
+ /**
54
+ * Disable the automatic E5 `query:` / `passage:` prefixing (PS-10). The
55
+ * prefixes are applied by default for E5-family models (the multilingual-e5
56
+ * default and any model whose id carries an `e5` token), because the E5 model
57
+ * card requires them and omitting them measurably degrades retrieval. Set
58
+ * this to `true` only if your inputs are already prefixed or you use a
59
+ * non-standard E5 export. Toggling it changes the canonical `configHash`
60
+ * (and thus the embedder id), which triggers a re-embedding migration.
61
+ */
62
+ readonly disableTaskPrefix?: boolean;
63
+ }
64
+ /**
65
+ * Tiny structural shape of `@huggingface/transformers`' feature-
66
+ * extraction pipeline used by this package. Declared inline so the
67
+ * embedder does not import the heavy peer at build time.
68
+ *
69
+ * @internal
70
+ */
71
+ type FeatureExtractor = (texts: string | readonly string[], options?: {
72
+ pooling?: Pooling;
73
+ normalize?: boolean;
74
+ signal?: AbortSignal;
75
+ }) => Promise<{
76
+ data: Float32Array;
77
+ dims: readonly number[];
78
+ tolist?(): unknown;
79
+ }>;
80
+ /**
81
+ * Pipeline-factory shape used for dependency injection in tests.
82
+ *
83
+ * @internal
84
+ */
85
+ type PipelineFactory = (task: 'feature-extraction', model: string, opts: {
86
+ revision?: string;
87
+ cache_dir?: string;
88
+ dtype?: string;
89
+ device?: string;
90
+ }) => Promise<FeatureExtractor>;
91
+ /**
92
+ * Raised when the underlying transformer model cannot be loaded
93
+ * (offline / corporate firewall / wrong cache dir).
94
+ *
95
+ * @stable
96
+ */
97
+ declare class EmbedderModelLoadError extends Error {
98
+ readonly name = "EmbedderModelLoadError";
99
+ }
100
+ /**
101
+ * Build a `TransformersJsEmbedder` instance. Lazy: the underlying
102
+ * pipeline is constructed on the first `embed()` call so packaging
103
+ * the embedder does not pay the model-load cost.
104
+ *
105
+ * @stable
106
+ */
107
+ declare function createTransformersJsEmbedder(options?: TransformersJsEmbedderOptions): TransformersJsEmbedder;
108
+ /**
109
+ * `EmbedderProvider` implementation backed by `@huggingface/transformers`.
110
+ *
111
+ * @stable
112
+ */
113
+ declare class TransformersJsEmbedder implements EmbedderProvider {
114
+ #private;
115
+ constructor(options: TransformersJsEmbedderOptions);
116
+ id(): string;
117
+ dim(): number;
118
+ configHash(): string;
119
+ embed(texts: ReadonlyArray<string>, opts?: EmbedOptions): Promise<ReadonlyArray<Float32Array>>;
120
+ }
121
+ /** Test-only cache reset. */
122
+ declare function _resetPipelineFactoryCacheForTesting(): void;
123
+ /**
124
+ * True when a model id belongs to the E5 family, which requires asymmetric
125
+ * `query:` / `passage:` prefixes (PS-10). Matches an `e5` token bounded by a
126
+ * path / dash / underscore so it covers `multilingual-e5-base`, `e5-large`,
127
+ * `intfloat/e5-mistral`, etc. without false-matching unrelated names.
128
+ */
129
+ declare function isE5Model(model: string): boolean;
130
+ /**
131
+ * Canonical-JSON deterministic hash of an embedder configuration.
132
+ * Object keys are sorted lexicographically; primitives flow through as
133
+ * `JSON.stringify` would render them. Used by the multi-table per-
134
+ * embedder vec0 layout to tell drift apart from a true model swap.
135
+ *
136
+ * @stable
137
+ */
138
+ declare function canonicalConfigHash(config: unknown): string;
139
+ //#endregion
140
+ export { EmbedderModelLoadError, FeatureExtractor, PipelineFactory, Pooling, TransformersJsEmbedder, TransformersJsEmbedderOptions, VERSION, _resetPipelineFactoryCacheForTesting, canonicalConfigHash, createTransformersJsEmbedder, isE5Model };
141
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;AAiGY,cAlFC,OAAA,GAkFc,OAId;AAWb;AAWA;AAWA;;;;AAyD2B,KAxKf,OAAA,GAwKe,MAAA,GAAA,KAAA,GAAA,aAAA,GAAA,KAAA,GAAA,YAAA,GAAA,MAAA;;;;;AAyE3B;AAsBgB,UAhQC,6BAAA,CAgQQ;EAYT;;;qBAxQK;;;;;;;;;;;;;;;;;;;;;;;6BAuBQ;;;;;;;;;;;;;;;;;;;;;;;;;KA0BjB,gBAAA;YAEY;;WAAuC;MAC1D;QACG;;;;;;;;;KAUI,eAAA;;;;;MAIP,QAAQ;;;;;;;cAWA,sBAAA,SAA+B,KAAA;;;;;;;;;;iBAW5B,4BAAA,WACL,gCACR;;;;;;cASU,sBAAA,YAAkC;;uBAexB;;;;eAwCZ,8BACD,eACL,QAAQ,cAAc;;;iBAyEX,oCAAA,CAAA;;;;;;;iBAsBA,SAAA;;;;;;;;;iBAYA,mBAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,185 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ //#region src/index.ts
4
+ /**
5
+ * @graphorin/embedder-transformersjs — default in-process embedder.
6
+ *
7
+ * Wraps `@huggingface/transformers@^4.1.0` to produce dense embeddings
8
+ * inside the Graphorin process. Default model
9
+ * `Xenova/multilingual-e5-base` (768-dim, multilingual; DEC-130 /
10
+ * ADR-025).
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ /** Canonical version constant. Mirrors the `package.json` version. */
15
+ const VERSION = "0.5.0";
16
+ const DEFAULT_MODEL = "Xenova/multilingual-e5-base";
17
+ const DEFAULT_DIM = 768;
18
+ /**
19
+ * Raised when the underlying transformer model cannot be loaded
20
+ * (offline / corporate firewall / wrong cache dir).
21
+ *
22
+ * @stable
23
+ */
24
+ var EmbedderModelLoadError = class extends Error {
25
+ name = "EmbedderModelLoadError";
26
+ };
27
+ /**
28
+ * Build a `TransformersJsEmbedder` instance. Lazy: the underlying
29
+ * pipeline is constructed on the first `embed()` call so packaging
30
+ * the embedder does not pay the model-load cost.
31
+ *
32
+ * @stable
33
+ */
34
+ function createTransformersJsEmbedder(options = {}) {
35
+ return new TransformersJsEmbedder(options);
36
+ }
37
+ /**
38
+ * `EmbedderProvider` implementation backed by `@huggingface/transformers`.
39
+ *
40
+ * @stable
41
+ */
42
+ var TransformersJsEmbedder = class {
43
+ #model;
44
+ #pooling;
45
+ #normalize;
46
+ #revision;
47
+ #cacheDir;
48
+ #dtype;
49
+ #device;
50
+ #pipelineFactory;
51
+ /** Whether to apply E5 `query:` / `passage:` prefixes (PS-10). */
52
+ #taskPrefix;
53
+ #extractor = null;
54
+ #loading = null;
55
+ #resolvedDim;
56
+ constructor(options) {
57
+ this.#model = options.model ?? DEFAULT_MODEL;
58
+ this.#pooling = options.pooling ?? "mean";
59
+ this.#normalize = options.normalize ?? true;
60
+ this.#revision = options.revision;
61
+ this.#cacheDir = options.cacheDir ?? process.env.GRAPHORIN_CACHE_DIR;
62
+ this.#dtype = options.dtype;
63
+ this.#device = options.device;
64
+ this.#pipelineFactory = options.pipelineFactory;
65
+ this.#resolvedDim = options.dim ?? guessDefaultDim(this.#model);
66
+ this.#taskPrefix = isE5Model(this.#model) && options.disableTaskPrefix !== true;
67
+ }
68
+ id() {
69
+ const dim = this.#resolvedDim ?? DEFAULT_DIM;
70
+ return `transformersjs:${this.#model}@${dim}`;
71
+ }
72
+ dim() {
73
+ if (this.#resolvedDim !== null) return this.#resolvedDim;
74
+ return DEFAULT_DIM;
75
+ }
76
+ configHash() {
77
+ return canonicalConfigHash({
78
+ adapter: "transformersjs",
79
+ model: this.#model,
80
+ pooling: this.#pooling,
81
+ normalize: this.#normalize,
82
+ revision: this.#revision ?? null,
83
+ dtype: this.#dtype ?? null,
84
+ device: this.#device ?? null,
85
+ ...this.#taskPrefix ? { taskPrefix: "e5" } : {}
86
+ });
87
+ }
88
+ async embed(texts, opts = {}) {
89
+ if (texts.length === 0) return [];
90
+ const result = await (await this.#getExtractor())(this.#taskPrefix ? texts.map((t) => `${opts.taskType ?? "passage"}: ${t}`) : [...texts], {
91
+ pooling: this.#pooling,
92
+ normalize: this.#normalize,
93
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {}
94
+ });
95
+ const lastDim = result.dims[result.dims.length - 1] ?? this.#resolvedDim ?? DEFAULT_DIM;
96
+ if (this.#resolvedDim === null) this.#resolvedDim = lastDim;
97
+ const dim = lastDim;
98
+ const out = [];
99
+ for (let i = 0; i < texts.length; i++) {
100
+ const start = i * dim;
101
+ out.push(result.data.slice(start, start + dim));
102
+ }
103
+ return out;
104
+ }
105
+ async #getExtractor() {
106
+ if (this.#extractor !== null) return this.#extractor;
107
+ if (this.#loading !== null) return this.#loading;
108
+ this.#loading = (this.#pipelineFactory ?? await loadDefaultPipelineFactory())("feature-extraction", this.#model, {
109
+ ...this.#revision !== void 0 ? { revision: this.#revision } : {},
110
+ ...this.#cacheDir !== void 0 ? { cache_dir: this.#cacheDir } : {},
111
+ ...this.#dtype !== void 0 ? { dtype: this.#dtype } : {},
112
+ ...this.#device !== void 0 ? { device: this.#device } : {}
113
+ }).then((pipe) => {
114
+ this.#extractor = pipe;
115
+ this.#loading = null;
116
+ return pipe;
117
+ }).catch((err) => {
118
+ this.#loading = null;
119
+ throw new EmbedderModelLoadError(`[graphorin/embedder-transformersjs] failed to load model '${this.#model}'. See the offline-install guide for instructions on pre-downloading the model.`, { cause: err });
120
+ });
121
+ return this.#loading;
122
+ }
123
+ };
124
+ /** @internal */
125
+ let CACHED_PIPELINE_FACTORY = null;
126
+ async function loadDefaultPipelineFactory() {
127
+ if (CACHED_PIPELINE_FACTORY !== null) return CACHED_PIPELINE_FACTORY;
128
+ try {
129
+ CACHED_PIPELINE_FACTORY = (await import("@huggingface/transformers")).pipeline;
130
+ return CACHED_PIPELINE_FACTORY;
131
+ } catch (err) {
132
+ throw new EmbedderModelLoadError("[graphorin/embedder-transformersjs] required peer '@huggingface/transformers' is not installed.", { cause: err });
133
+ }
134
+ }
135
+ /** Test-only cache reset. */
136
+ function _resetPipelineFactoryCacheForTesting() {
137
+ CACHED_PIPELINE_FACTORY = null;
138
+ }
139
+ const KNOWN_DIMS = new Map([
140
+ ["Xenova/multilingual-e5-small", 384],
141
+ ["Xenova/multilingual-e5-base", 768],
142
+ ["Xenova/multilingual-e5-large", 1024],
143
+ ["Xenova/bge-m3", 1024],
144
+ ["onnx-community/all-MiniLM-L6-v2-ONNX", 384]
145
+ ]);
146
+ function guessDefaultDim(model) {
147
+ return KNOWN_DIMS.get(model) ?? null;
148
+ }
149
+ /**
150
+ * True when a model id belongs to the E5 family, which requires asymmetric
151
+ * `query:` / `passage:` prefixes (PS-10). Matches an `e5` token bounded by a
152
+ * path / dash / underscore so it covers `multilingual-e5-base`, `e5-large`,
153
+ * `intfloat/e5-mistral`, etc. without false-matching unrelated names.
154
+ */
155
+ function isE5Model(model) {
156
+ return /(?:^|[/_-])e5(?:[/_-]|$)/i.test(model);
157
+ }
158
+ /**
159
+ * Canonical-JSON deterministic hash of an embedder configuration.
160
+ * Object keys are sorted lexicographically; primitives flow through as
161
+ * `JSON.stringify` would render them. Used by the multi-table per-
162
+ * embedder vec0 layout to tell drift apart from a true model swap.
163
+ *
164
+ * @stable
165
+ */
166
+ function canonicalConfigHash(config) {
167
+ const canonical = canonicalize(config);
168
+ return createHash("sha256").update(canonical, "utf8").digest("hex");
169
+ }
170
+ function canonicalize(value) {
171
+ if (value === null) return "null";
172
+ if (typeof value === "boolean") return value ? "true" : "false";
173
+ if (typeof value === "number") return Number.isFinite(value) ? value.toString() : "null";
174
+ if (typeof value === "string") return JSON.stringify(value);
175
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
176
+ if (typeof value === "object") {
177
+ const obj = value;
178
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(",")}}`;
179
+ }
180
+ return "null";
181
+ }
182
+
183
+ //#endregion
184
+ export { EmbedderModelLoadError, TransformersJsEmbedder, VERSION, _resetPipelineFactoryCacheForTesting, canonicalConfigHash, createTransformersJsEmbedder, isE5Model };
185
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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.5.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 const dim = this.#resolvedDim ?? DEFAULT_DIM;\n return `transformersjs:${this.#model}@${dim}`;\n }\n\n dim(): number {\n if (this.#resolvedDim !== null) return this.#resolvedDim;\n return DEFAULT_DIM;\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 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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"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@graphorin/embedder-transformersjs",
3
+ "version": "0.5.0",
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
+ "license": "MIT",
6
+ "author": "Oleksiy Stepurenko",
7
+ "homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/embedder-transformersjs",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/o-stepper/graphorin.git",
11
+ "directory": "packages/embedder-transformersjs"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/o-stepper/graphorin/issues"
15
+ },
16
+ "keywords": [
17
+ "graphorin",
18
+ "ai",
19
+ "agents",
20
+ "framework",
21
+ "embeddings",
22
+ "transformersjs",
23
+ "huggingface",
24
+ "multilingual"
25
+ ],
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=22.0.0"
29
+ },
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE"
45
+ ],
46
+ "dependencies": {
47
+ "@graphorin/core": "0.5.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@huggingface/transformers": "^4.1.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "@huggingface/transformers": {
54
+ "optional": false
55
+ }
56
+ },
57
+ "publishConfig": {
58
+ "access": "public",
59
+ "provenance": true
60
+ },
61
+ "devDependencies": {
62
+ "@huggingface/transformers": "^4.1.0"
63
+ },
64
+ "scripts": {
65
+ "build": "tsdown",
66
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
67
+ "test": "vitest run",
68
+ "lint": "biome check .",
69
+ "clean": "rimraf dist .turbo *.tsbuildinfo"
70
+ }
71
+ }