@atlaskit/editor-plugin-autocomplete 3.1.0 → 3.3.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.
@@ -1,28 +1,40 @@
1
- import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
1
  import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
3
- import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
4
3
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
4
+ import _typeof from "@babel/runtime/helpers/typeof";
5
+ import _createClass from "@babel/runtime/helpers/createClass";
6
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
7
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
8
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
9
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
10
+ import _regeneratorRuntime from "@babel/runtime/regenerator";
5
11
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
6
12
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
7
13
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
8
- import _regeneratorRuntime from "@babel/runtime/regenerator";
9
- function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
10
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
11
14
  /**
12
15
  * Local Slow Lane Client: On-device inference via @mlc-ai/web-llm.
13
16
  *
14
- * Drop-in replacement for the network-based slow-lane-client. Instead of
15
- * calling a backend API, this client uses MLC WebLLM to run a small language
16
- * model (SmolLM 135M) directly in the browser via WebGPU.
17
+ * Drop-in replacement for the network-based slow-lane-client. Instead of calling
18
+ * a backend API, this client runs two models in the browser via WebGPU, in a
19
+ * single MLCEngine, to reproduce the BE encoder's outputs on-device:
20
+ *
21
+ * - Causal LM (SmolLM2-135M-Instruct): one decode step per word boundary. A
22
+ * registered LogitProcessor captures the raw next-token logits, which
23
+ * `computeBePayload` turns into a whole-word `lm_logits` payload — a faithful
24
+ * port of the BE `CausalLMEncoder._get_top_k_probs` (masked softmax over the
25
+ * vocab's first-tokens, prefix expansion, L2 reservation, log-space pooling).
26
+ * - Semantic embedder (Snowflake Arctic Embed S): produces the real 384-d
27
+ * `semantic_vector`. Inputs are wrapped as passages (see `wrapForArctic`) so
28
+ * the runtime vector lands in the same space as the precomputed word bin.
17
29
  *
18
30
  * ── Why main thread (no Web Worker)? ─────────────────────────────────────
19
- * SmolLM 135M is small enough (~270 MB weights, 350-400 MB VRAM) that
20
- * WebGPU inference on the main thread is production-viable:
31
+ * The models are small enough (~640 MB combined VRAM) that WebGPU inference on
32
+ * the main thread is viable:
21
33
  *
22
34
  * - WebGPU GPU compute is inherently async (doesn't block the main thread)
23
- * - CPU overhead (tokenization + post-processing) is only 5-10 ms
24
- * - Single forward pass latency is 50-150 ms — well within autocomplete
25
- * expectations (~250 ms between word boundaries)
35
+ * - CPU overhead (BE-parity post-processing) is a few ms
36
+ * - Per-inference latency is well within autocomplete expectations
37
+ * (~250 ms between word boundaries)
26
38
  *
27
39
  * This avoids all the complexity of Web Workers:
28
40
  * - No CSP workarounds (blob URLs, inline scripts)
@@ -46,17 +58,522 @@ import { isWordBoundary } from './slow-lane-client';
46
58
  // ─── Constants ───────────────────────────────────────────────────────────────
47
59
 
48
60
  var DEFAULT_DEBOUNCE_MS = 300;
49
- export var LOCAL_MLC_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
61
+ export var LOCAL_MLC_CAUSAL_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
62
+
63
+ /**
64
+ * MLC ID for the semantic embedder (Snowflake Arctic Embed S, batch=4 variant).
65
+ *
66
+ * The `-b4` suffix selects the prebuilt variant compiled for a max batch size of
67
+ * 4 (≈239 MB VRAM) rather than `-b32` (≈1023 MB VRAM). Autocomplete embeds one
68
+ * context at a time, so `-b4` is the right fit. This model IS in
69
+ * `prebuiltAppConfig.model_list` of web-llm 0.2.82 — no `customModelConfig` needed.
70
+ */
71
+ export var LOCAL_MLC_EMBEDDING_MODEL_ID = 'snowflake-arctic-embed-s-q0f32-MLC-b4';
72
+
73
+ /**
74
+ * Wrap raw context text with BERT special tokens before embedding.
75
+ *
76
+ * web-llm's `EmbeddingPipeline` does NOT auto-prepend `[CLS]` / append `[SEP]`
77
+ * (the official MLC embeddings example wraps manually). The Python
78
+ * `sentence_transformers` side that generated the word-vector bin adds these
79
+ * inside `model.encode()`, so we must mirror it here for the runtime context
80
+ * vector to land in the same region of Arctic's embedding space as the bin.
81
+ *
82
+ * No query prefix is applied: the semantic step is sentence-to-sentence (`s2s`)
83
+ * similarity ("which words are conceptually similar to this context?"), not
84
+ * sentence-to-passage (`s2p`) retrieval. Arctic's query prefix would misframe
85
+ * the relationship. Encode both sides as passages. See implementation.md §4.3.
86
+ */
87
+ export var wrapForArctic = function wrapForArctic(text) {
88
+ return "[CLS] ".concat(text, " [SEP]");
89
+ };
50
90
 
51
- /** HF root for the default weights (includes `tensor-cache.json` for WebLLM 0.2+). */
52
- export var LOCAL_MLC_HF_MODEL_REPO = 'https://huggingface.co/mlc-ai/SmolLM2-135M-Instruct-q0f16-MLC';
53
- export var LOCAL_MLC_MODEL_LIB_WASM_NAME = 'SmolLM2-135M-Instruct-q0f16-ctx4k_cs1k-webgpu.wasm';
91
+ /**
92
+ * BE-parity constants — must match `CausalLMEncoder` defaults in the Python
93
+ * sidecar (`cc-smarts/python-sidecar/src/causal_lm_encoder.py`) and
94
+ * `SlowLaneEngine` (`typeahead_context_encoding.py`) so local payloads behave
95
+ * identically to the server-client setup.
96
+ */
97
+ export var BE_PARITY = {
98
+ /** Final payload size cap (BE: `top_k_words`). */
99
+ TOP_K_WORDS: 2000,
100
+ /** L2 (domain) words admitted unconditionally before pooling (BE: `reserved_l2_slots`). */
101
+ RESERVED_L2_SLOTS: 500,
102
+ /** Log-space additive bias favouring L2 over L3 in the pool (BE: `l2_bias`). */
103
+ L2_BIAS: 1.0,
104
+ /** Drop words below this probability from the final payload (BE: `> 0.00001`). */
105
+ MIN_PROB: 0.00001,
106
+ /**
107
+ * Word-level approximation of the BE causal LM token limit.
108
+ *
109
+ * BE: `CausalLMEncoder.max_context_tokens = 100` (BPE tokens, left-truncated).
110
+ * FE: no tokenizer available, so we approximate with word count. English text
111
+ * averages ~1.3–1.5 BPE tokens/word, meaning 100 words ≈ 130–150 tokens.
112
+ * Using 100 words keeps the approximation simple and errs on the side of
113
+ * sending slightly more context than the BE sees — acceptable for a PoC.
114
+ */
115
+ MAX_CONTEXT_TOKENS: 100,
116
+ /**
117
+ * Word-level rolling window for the semantic embedder.
118
+ *
119
+ * BE: `SlowLaneEngine.max_context_words = 100` (applied in
120
+ * `typeahead_context_encoding.py` before calling `SemanticEncoder.encode`).
121
+ * Truncated identically here so the runtime Arctic vector lands in the same
122
+ * region of the embedding space as the precomputed word-vector bin.
123
+ */
124
+ MAX_CONTEXT_WORDS: 100
125
+ };
126
+ var splitOnWhitespace = function splitOnWhitespace(text) {
127
+ var trimmed = text.trim();
128
+ if (trimmed === '') {
129
+ return [];
130
+ }
131
+ var words = [];
132
+ var wordStart = -1;
133
+ for (var i = 0; i < trimmed.length; i++) {
134
+ if (trimmed[i].trim() === '') {
135
+ if (wordStart !== -1) {
136
+ words.push(trimmed.slice(wordStart, i));
137
+ wordStart = -1;
138
+ }
139
+ continue;
140
+ }
141
+ if (wordStart === -1) {
142
+ wordStart = i;
143
+ }
144
+ }
145
+ if (wordStart !== -1) {
146
+ words.push(trimmed.slice(wordStart));
147
+ }
148
+ return words;
149
+ };
150
+
151
+ /**
152
+ * Return the last `n` whitespace-separated words of `text`, joined by spaces.
153
+ * Mirrors the BE rolling-window truncation applied before both encoders.
154
+ */
155
+ var truncateToLastNWords = function truncateToLastNWords(text, n) {
156
+ var words = splitOnWhitespace(text);
157
+ return words.length <= n ? text : words.slice(-n).join(' ');
158
+ };
159
+
160
+ // ─── Logit capture ─────────────────────────────────────────────────────────
54
161
 
55
162
  /**
56
- * Original target repo (add-basics fine-tune). **Not compatible with WebLLM 0.2.x** (no `tensor-cache.json`).
57
- * @see module doc above
163
+ * A LogitProcessor that captures the raw next-token logits and passes them
164
+ * through unmodified.
165
+ *
166
+ * web-llm invokes `processLogits` on the CPU after the model's forward pass and
167
+ * before sampling, handing us the full `Float32Array(vocab_size)` at the current
168
+ * decode position. We copy it off web-llm's shared buffer (which it may reuse
169
+ * across calls) and return the original untouched so sampling is unaffected.
170
+ *
171
+ * This is the raw-logit access the BE-parity algorithm needs (masked softmax +
172
+ * prefix expansion, consumed in a later step). Registered for the causal LM
173
+ * only — the embedder never decodes tokens, so it produces no logits.
174
+ */
175
+ var CapturingLogitProcessor = /*#__PURE__*/_createClass(function CapturingLogitProcessor() {
176
+ var _this = this;
177
+ _classCallCheck(this, CapturingLogitProcessor);
178
+ _defineProperty(this, "captured", null);
179
+ _defineProperty(this, "processLogits", function (logits) {
180
+ // Copy off web-llm's shared buffer — it may reuse `logits` across calls.
181
+ _this.captured = new Float32Array(logits);
182
+ return logits;
183
+ });
184
+ _defineProperty(this, "processSampledToken", function () {
185
+ // No-op — we don't track sampled tokens.
186
+ });
187
+ _defineProperty(this, "resetState", function () {
188
+ _this.captured = null;
189
+ });
190
+ }); // ─── BE-parity data + algorithm ──────────────────────────────────────────────
191
+ /**
192
+ * Prefix-expansion map: first-token id → words whose space-prefixed SmolLM2
193
+ * encoding starts with that token. Generated offline by
194
+ * `scripts/gen_first_token_to_words.py`, which mirrors the BE's in-memory map
195
+ * (`CausalLMEncoder._ensure_loaded`).
196
+ *
197
+ * Populated lazily by `loadBePayloadData()` from a dynamically-imported JSON so
198
+ * the (large) payload is only fetched when the local client is actually
199
+ * initialised — keeping it out of the editor's main chunk for the vast majority
200
+ * of users (who run with `useLocalModel` off).
58
201
  */
59
- export var HUGGINGFACE_TB_SMOLLM_ADD_BASICS_REPO = 'https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC';
202
+ var firstTokenToWords = new Map();
203
+
204
+ /**
205
+ * L2 (Atlassian-domain) word set, derived from the keys of `vocabulary_10k.json`.
206
+ * Used by `computeBePayload` for tier-aware ranking: any word in the prefix map
207
+ * that is not in this set is treated as L3 (general English), matching the BE.
208
+ * Populated lazily alongside `firstTokenToWords` — see `loadBePayloadData()`.
209
+ */
210
+ var l2Words = new Set();
211
+
212
+ /**
213
+ * Array of token IDs that appear as a first token for at least one vocabulary
214
+ * word. Derived from `firstTokenToWords` when the data loads so `computeBePayload`
215
+ * does not re-allocate this array on every word-boundary call.
216
+ */
217
+ var prefixMapTokenIds = [];
218
+
219
+ /** De-dupes concurrent loads and lets repeated calls await the same payload. */
220
+ var bePayloadDataPromise;
221
+
222
+ /**
223
+ * Unwrap a dynamically imported JSON module to the parsed JSON value, working
224
+ * across the two interop modes AFM's bundler chain emits:
225
+ *
226
+ * 1. **`.default`-wrapped namespace** — classic webpack (and Jest) hang the
227
+ * JSON value under the `default` export.
228
+ * 2. **Named-exports namespace** — webpack 5 / atlaspack with JSON
229
+ * named-exports (or native ESM JSON modules) expose each top-level key as
230
+ * a named export and shadow `default`, so `mod.default` can be `undefined`
231
+ * (or some unrelated value) even though `mod` itself holds the data.
232
+ *
233
+ * The caller MUST declare the underlying JSON shape via `shape` because, in
234
+ * named-exports mode, a dense array `["a","b"]` and a sparse numeric-keyed
235
+ * object `{"5":"a","12":"b"}` are emitted identically (`{"0":..}` / `{"5":..}`);
236
+ * no runtime heuristic can tell them apart, so only the caller knows which:
237
+ *
238
+ * - `'object'` — the JSON is a `{...}` (including sparse maps keyed by integer
239
+ * IDs). The named exports are rebuilt into a plain object so `Object.entries`
240
+ * yields the real keys, not synthetic array indices.
241
+ * - `'array'` — the JSON is a `[...]`, reconstructed from the `0..n-1` indices.
242
+ *
243
+ * :param mod: The raw module object returned by `await import('./*.json')`.
244
+ * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
245
+ * :returns: The parsed JSON value, or `null` if neither interop mode applies.
246
+ */
247
+ var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
248
+ if (mod == null || _typeof(mod) !== 'object') {
249
+ return null;
250
+ }
251
+ var namespace = mod;
252
+
253
+ // Compute the named-export own-keys (strip synthetic markers).
254
+ var ownKeys = Object.keys(namespace).filter(function (k) {
255
+ return k !== 'default' && k !== '__esModule';
256
+ });
257
+
258
+ // PREFER named exports when present — they always reflect the JSON's real
259
+ // top-level keys / indices, regardless of what `default` happens to be.
260
+ // Under JSON named-exports mode `default` is not necessarily the parsed
261
+ // value (e.g. for `{"service": 0, ...}` it can be the number `0`, with the
262
+ // real data in the named exports), so taking `default` first would corrupt it.
263
+ if (ownKeys.length > 0) {
264
+ if (shape === 'array') {
265
+ // JSON arrays are dense; reconstruct from `0..length-1` indices.
266
+ var len = ownKeys.length;
267
+ var arr = new Array(len);
268
+ for (var i = 0; i < len; i++) {
269
+ arr[i] = namespace[String(i)];
270
+ }
271
+ return arr;
272
+ }
273
+ // shape === 'object'. Rebuild a plain object from the (stripped) own
274
+ // keys so callers can `Object.entries()` it without iterating over
275
+ // `default` / `__esModule`, and to detach from the module-namespace
276
+ // object (which is sealed/non-extensible on some bundler outputs).
277
+ var obj = {};
278
+ var _iterator = _createForOfIteratorHelper(ownKeys),
279
+ _step;
280
+ try {
281
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
282
+ var k = _step.value;
283
+ obj[k] = namespace[k];
284
+ }
285
+ } catch (err) {
286
+ _iterator.e(err);
287
+ } finally {
288
+ _iterator.f();
289
+ }
290
+ return obj;
291
+ }
292
+
293
+ // Fallback: no named exports — classic webpack JSON-module interop where
294
+ // the whole parsed JSON value is hung under `default`. Trust it.
295
+ if ('default' in namespace && namespace.default != null) {
296
+ return namespace.default;
297
+ }
298
+ return null;
299
+ };
300
+
301
+ /**
302
+ * Lazily load and build the BE-parity lookup tables from their JSON payloads.
303
+ * The dynamic imports are split into their own async chunks so neither file is
304
+ * bundled into the editor's main chunk unless local inference is initialised.
305
+ *
306
+ * :returns:
307
+ * A promise that resolves once `firstTokenToWords`, `l2Words` and
308
+ * `prefixMapTokenIds` are populated.
309
+ */
310
+ var loadBePayloadData = function loadBePayloadData() {
311
+ if (!bePayloadDataPromise) {
312
+ bePayloadDataPromise = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
313
+ var _yield$Promise$all, _yield$Promise$all2, firstTokenToWordsModule, vocabularyModule, firstTokenToWordsData, vocabularyData;
314
+ return _regeneratorRuntime.wrap(function (_context) {
315
+ while (1) switch (_context.prev = _context.next) {
316
+ case 0:
317
+ _context.next = 1;
318
+ return Promise.all([import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-first-token-to-words" */'./data/first_token_to_words.json'), import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */'./data/vocabulary_10k.json')]);
319
+ case 1:
320
+ _yield$Promise$all = _context.sent;
321
+ _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2);
322
+ firstTokenToWordsModule = _yield$Promise$all2[0];
323
+ vocabularyModule = _yield$Promise$all2[1];
324
+ firstTokenToWordsData = unwrapJsonModule(firstTokenToWordsModule, 'object');
325
+ vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
326
+ if (!(firstTokenToWordsData == null || (vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null)) {
327
+ _context.next = 2;
328
+ break;
329
+ }
330
+ throw new Error("[LocalSlowLane] JSON module could not be unwrapped \u2014 " + "firstTokenToWordsData=".concat(firstTokenToWordsData == null ? 'null/undefined' : 'defined', ", ") + "vocabularyData=".concat(vocabularyData == null ? 'null/undefined' : vocabularyData.words == null ? 'defined but missing .words' : 'defined'));
331
+ case 2:
332
+ firstTokenToWords = new Map(Object.entries(firstTokenToWordsData).map(function (_ref2) {
333
+ var _ref3 = _slicedToArray(_ref2, 2),
334
+ tokenId = _ref3[0],
335
+ words = _ref3[1];
336
+ return [Number(tokenId), words];
337
+ }));
338
+ l2Words = new Set(Object.keys(vocabularyData.words));
339
+ prefixMapTokenIds = Array.from(firstTokenToWords.keys());
340
+ if (isAutocompleteDebugEnabled()) {
341
+ // eslint-disable-next-line no-console
342
+ console.log('%c[LocalSlowLane] %c✅ BE-parity payload data loaded:', 'color: #9c27b0; font-weight: bold;', 'color: #4caf50; font-weight: bold;', {
343
+ firstTokenToWordsEntries: firstTokenToWords.size,
344
+ l2WordsCount: l2Words.size,
345
+ prefixMapTokenIdsLength: prefixMapTokenIds.length
346
+ });
347
+ }
348
+ case 3:
349
+ case "end":
350
+ return _context.stop();
351
+ }
352
+ }, _callee);
353
+ }))().catch(function (e) {
354
+ // Don't cache a rejected promise — a transient import failure would
355
+ // otherwise prevent the local model from ever initialising again this
356
+ // session. Reset so the next init attempt retries.
357
+ bePayloadDataPromise = undefined;
358
+ throw e;
359
+ });
360
+ }
361
+ return bePayloadDataPromise;
362
+ };
363
+
364
+ /**
365
+ * Convert a raw next-token logit vector into a whole-word probability payload,
366
+ * faithfully porting the BE `CausalLMEncoder._get_top_k_probs`
367
+ * (`cc-smarts/python-sidecar/src/causal_lm_encoder.py`).
368
+ *
369
+ * Steps: (1) numerically-stable masked softmax over only the token ids present
370
+ * in the prefix-expansion map; (2) spread each token's probability to every
371
+ * whole word sharing that first token, taking the max; (3) reserve the top L2
372
+ * words unconditionally; (4) rank the remainder in a log-space pool with an
373
+ * additive L2 bias; (5) emit raw probabilities for the survivors, lowercased
374
+ * and trimmed at `MIN_PROB`.
375
+ *
376
+ * :params:
377
+ * rawLogits: Full-vocabulary logits from the LM's single decode step
378
+ * prefixMap: Map of first-token id to the words starting with that token
379
+ * domainWords: Set of L2 (domain) words, for tier-aware ranking
380
+ * :returns:
381
+ * A record of lowercase word to probability — the BE `lm_logits` payload
382
+ */
383
+ export var computeBePayload = function computeBePayload(rawLogits, prefixMap, domainWords) {
384
+ var validTokenIds = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : prefixMapTokenIds;
385
+ // 1. Numerically-stable masked softmax over validTokenIds only.
386
+ var maxLogit = -Infinity;
387
+ var _iterator2 = _createForOfIteratorHelper(validTokenIds),
388
+ _step2;
389
+ try {
390
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
391
+ var id = _step2.value;
392
+ var v = rawLogits[id];
393
+ if (v > maxLogit) {
394
+ maxLogit = v;
395
+ }
396
+ }
397
+ } catch (err) {
398
+ _iterator2.e(err);
399
+ } finally {
400
+ _iterator2.f();
401
+ }
402
+ var sumExp = 0;
403
+ var expByToken = new Map();
404
+ var _iterator3 = _createForOfIteratorHelper(validTokenIds),
405
+ _step3;
406
+ try {
407
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
408
+ var _id = _step3.value;
409
+ var e = Math.exp(rawLogits[_id] - maxLogit);
410
+ expByToken.set(_id, e);
411
+ sumExp += e;
412
+ }
413
+
414
+ // 2. Prefix expansion with max aggregation (probabilities sum to 1 over the
415
+ // masked subset, so divide each token's exp by sumExp on the fly).
416
+ } catch (err) {
417
+ _iterator3.e(err);
418
+ } finally {
419
+ _iterator3.f();
420
+ }
421
+ var wordProbs = new Map();
422
+ var _iterator4 = _createForOfIteratorHelper(prefixMap),
423
+ _step4;
424
+ try {
425
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
426
+ var _expByToken$get;
427
+ var _step4$value = _slicedToArray(_step4.value, 2),
428
+ _id2 = _step4$value[0],
429
+ words = _step4$value[1];
430
+ var _p = sumExp > 0 ? ((_expByToken$get = expByToken.get(_id2)) !== null && _expByToken$get !== void 0 ? _expByToken$get : 0) / sumExp : 0;
431
+ var _iterator9 = _createForOfIteratorHelper(words),
432
+ _step9;
433
+ try {
434
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
435
+ var _wordProbs$get2;
436
+ var _w = _step9.value;
437
+ var prev = (_wordProbs$get2 = wordProbs.get(_w)) !== null && _wordProbs$get2 !== void 0 ? _wordProbs$get2 : 0;
438
+ if (_p > prev) {
439
+ wordProbs.set(_w, _p);
440
+ }
441
+ }
442
+ } catch (err) {
443
+ _iterator9.e(err);
444
+ } finally {
445
+ _iterator9.f();
446
+ }
447
+ }
448
+
449
+ // 3. Split into L2 / L3 and reserve the top L2 slots unconditionally.
450
+ } catch (err) {
451
+ _iterator4.e(err);
452
+ } finally {
453
+ _iterator4.f();
454
+ }
455
+ var l2Matches = [];
456
+ var l3Matches = [];
457
+ var _iterator5 = _createForOfIteratorHelper(wordProbs),
458
+ _step5;
459
+ try {
460
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
461
+ var _step5$value = _slicedToArray(_step5.value, 2),
462
+ _w2 = _step5$value[0],
463
+ _p2 = _step5$value[1];
464
+ if (domainWords.has(_w2)) {
465
+ l2Matches.push([_w2, _p2]);
466
+ } else {
467
+ l3Matches.push([_w2, _p2]);
468
+ }
469
+ }
470
+ } catch (err) {
471
+ _iterator5.e(err);
472
+ } finally {
473
+ _iterator5.f();
474
+ }
475
+ l2Matches.sort(function (a, b) {
476
+ return b[1] - a[1];
477
+ });
478
+ var reserved = l2Matches.slice(0, BE_PARITY.RESERVED_L2_SLOTS);
479
+
480
+ // 4. Pool the leftovers in log space; the L2 bias only affects ranking here.
481
+ // Words in l2Matches are unique and the array is sorted descending, so the
482
+ // non-reserved entries are exactly the tail after the reserved prefix — slice
483
+ // it directly rather than allocating a Set and scanning every entry on this
484
+ // hot path (runs ~every word boundary while typing).
485
+ var pool = [];
486
+ var _iterator6 = _createForOfIteratorHelper(l2Matches.slice(BE_PARITY.RESERVED_L2_SLOTS)),
487
+ _step6;
488
+ try {
489
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
490
+ var _step6$value = _slicedToArray(_step6.value, 2),
491
+ _w3 = _step6$value[0],
492
+ _p3 = _step6$value[1];
493
+ pool.push([_w3, Math.log(Math.max(_p3, 1e-10)) + BE_PARITY.L2_BIAS]);
494
+ }
495
+ } catch (err) {
496
+ _iterator6.e(err);
497
+ } finally {
498
+ _iterator6.f();
499
+ }
500
+ for (var _i = 0, _l3Matches = l3Matches; _i < _l3Matches.length; _i++) {
501
+ var _l3Matches$_i = _slicedToArray(_l3Matches[_i], 2),
502
+ w = _l3Matches$_i[0],
503
+ p = _l3Matches$_i[1];
504
+ pool.push([w, Math.log(Math.max(p, 1e-10))]);
505
+ }
506
+ pool.sort(function (a, b) {
507
+ return b[1] - a[1];
508
+ });
509
+ var remainingSlots = Math.max(0, BE_PARITY.TOP_K_WORDS - reserved.length);
510
+ var poolWinners = pool.slice(0, remainingSlots);
511
+
512
+ // 5. Assemble payload: store RAW probabilities (the bias was ranking-only),
513
+ // lowercase keys, trimmed at MIN_PROB. Reserved first, then pool winners.
514
+ // Reserved entries are written first; pool-winner writes must NOT clobber a
515
+ // reserved entry whose normalised key collides (two source words can
516
+ // `.trim().toLowerCase()` to the same key — e.g. "Function" vs "function ").
517
+ // Without the existence guard, a low-probability pool winner would silently
518
+ // overwrite the (higher-probability) reserved entry, degrading top-K
519
+ // quality in a way that's invisible from the debug summary.
520
+ var result = {};
521
+ var addEntry = function addEntry(word, prob, allowOverwrite) {
522
+ if (prob <= BE_PARITY.MIN_PROB) {
523
+ return;
524
+ }
525
+ var key = word.trim().toLowerCase();
526
+ if (!allowOverwrite && key in result) {
527
+ return;
528
+ }
529
+ result[key] = prob;
530
+ };
531
+ var _iterator7 = _createForOfIteratorHelper(reserved),
532
+ _step7;
533
+ try {
534
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
535
+ var _step7$value = _slicedToArray(_step7.value, 2),
536
+ _w4 = _step7$value[0],
537
+ _p4 = _step7$value[1];
538
+ addEntry(_w4, _p4, true);
539
+ }
540
+ } catch (err) {
541
+ _iterator7.e(err);
542
+ } finally {
543
+ _iterator7.f();
544
+ }
545
+ var _iterator8 = _createForOfIteratorHelper(poolWinners),
546
+ _step8;
547
+ try {
548
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
549
+ var _wordProbs$get3;
550
+ var _step8$value = _slicedToArray(_step8.value, 1),
551
+ _w5 = _step8$value[0];
552
+ addEntry(_w5, (_wordProbs$get3 = wordProbs.get(_w5)) !== null && _wordProbs$get3 !== void 0 ? _wordProbs$get3 : 0, false);
553
+ }
554
+ } catch (err) {
555
+ _iterator8.e(err);
556
+ } finally {
557
+ _iterator8.f();
558
+ }
559
+ if (isAutocompleteDebugEnabled()) {
560
+ var topReserved = reserved.slice(0, 5).map(function (_ref4) {
561
+ var _ref5 = _slicedToArray(_ref4, 2),
562
+ w = _ref5[0],
563
+ p = _ref5[1];
564
+ return "".concat(w, ":").concat((p * 100).toFixed(2), "%");
565
+ }).join(', ');
566
+ var topPool = poolWinners.slice(0, 5).map(function (_ref6) {
567
+ var _wordProbs$get;
568
+ var _ref7 = _slicedToArray(_ref6, 1),
569
+ w = _ref7[0];
570
+ return "".concat(w, ":").concat((((_wordProbs$get = wordProbs.get(w)) !== null && _wordProbs$get !== void 0 ? _wordProbs$get : 0) * 100).toFixed(2), "%");
571
+ }).join(', ');
572
+ // eslint-disable-next-line no-console
573
+ console.log('%c[computeBePayload] %c%d valid tokens → %d words expanded | L2: %d / L3: %d | reserved: %d | pool winners: %d | final: %d words\n maxLogit(masked): %s | sumExp: %s\n top reserved L2: %s\n top pool: %s', 'color: #9c27b0; font-weight: bold;', 'color: inherit;', validTokenIds.length, wordProbs.size, l2Matches.length, l3Matches.length, reserved.length, poolWinners.length, Object.keys(result).length, maxLogit.toFixed(3), sumExp.toFixed(1), topReserved || '(none)', topPool || '(none)');
574
+ }
575
+ return result;
576
+ };
60
577
 
61
578
  // ─── Factory ─────────────────────────────────────────────────────────────────
62
579
 
@@ -85,7 +602,7 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
85
602
  onUpdate = config.onUpdate,
86
603
  onStatus = config.onStatus,
87
604
  _config$modelId = config.modelId,
88
- modelId = _config$modelId === void 0 ? LOCAL_MLC_MODEL_ID : _config$modelId,
605
+ modelId = _config$modelId === void 0 ? LOCAL_MLC_CAUSAL_MODEL_ID : _config$modelId,
89
606
  customModelConfig = config.customModelConfig;
90
607
 
91
608
  // ── State ──────────────────────────────────────────────────────────────
@@ -100,6 +617,9 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
100
617
  var initFailed = false;
101
618
  var engine = null;
102
619
  var engineInitPromise = null;
620
+ // Captures raw next-token logits from the LM's single decode step. Registered
621
+ // with the engine below; `lmLogitsCapture.captured` is consumed in a later step.
622
+ var lmLogitsCapture = new CapturingLogitProcessor();
103
623
  var unloadEngine = function unloadEngine(engineToUnload) {
104
624
  engineToUnload.unload().catch(function (error) {
105
625
  if (isAutocompleteDebugEnabled()) {
@@ -120,29 +640,31 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
120
640
  onStatus === null || onStatus === void 0 || onStatus(message);
121
641
  };
122
642
  var initEngine = /*#__PURE__*/function () {
123
- var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
124
- var _yield$import, CreateMLCEngine, prebuiltAppConfig, customModelRecord, appConfig, errorMsg, _t;
125
- return _regeneratorRuntime.wrap(function (_context) {
126
- while (1) switch (_context.prev = _context.next) {
643
+ var _ref8 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
644
+ var _yield$Promise$all3, _yield$Promise$all4, _yield$Promise$all4$, MLCEngineCtor, prebuiltAppConfig, customModelRecord, appConfig, newEngine, errorMsg, _t;
645
+ return _regeneratorRuntime.wrap(function (_context2) {
646
+ while (1) switch (_context2.prev = _context2.next) {
127
647
  case 0:
128
- _context.prev = 0;
648
+ _context2.prev = 0;
129
649
  if (isAutocompleteDebugEnabled()) {
130
650
  // eslint-disable-next-line no-console
131
- console.log("%c[LocalSlowLane] %c\uD83D\uDE80 Initialising MLC engine with model: ".concat(modelId), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
651
+ console.log("%c[LocalSlowLane] %c\uD83D\uDE80 Initialising MLC engine with models: ".concat(modelId, " (LM) + ").concat(LOCAL_MLC_EMBEDDING_MODEL_ID, " (embedder)"), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
132
652
  }
133
- onStatus === null || onStatus === void 0 || onStatus("Initialising model: ".concat(modelId, "\u2026"));
653
+ onStatus === null || onStatus === void 0 || onStatus("Initialising models: ".concat(modelId, " + ").concat(LOCAL_MLC_EMBEDDING_MODEL_ID, "\u2026"));
134
654
  if ('gpu' in navigator) {
135
- _context.next = 1;
655
+ _context2.next = 1;
136
656
  break;
137
657
  }
138
658
  throw new Error('WebGPU not supported');
139
659
  case 1:
140
- _context.next = 2;
141
- return import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-mlc-web-llm" */'@mlc-ai/web-llm');
660
+ _context2.next = 2;
661
+ return Promise.all([import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-mlc-web-llm" */'@mlc-ai/web-llm'), loadBePayloadData()]);
142
662
  case 2:
143
- _yield$import = _context.sent;
144
- CreateMLCEngine = _yield$import.CreateMLCEngine;
145
- prebuiltAppConfig = _yield$import.prebuiltAppConfig;
663
+ _yield$Promise$all3 = _context2.sent;
664
+ _yield$Promise$all4 = _slicedToArray(_yield$Promise$all3, 1);
665
+ _yield$Promise$all4$ = _yield$Promise$all4[0];
666
+ MLCEngineCtor = _yield$Promise$all4$.MLCEngine;
667
+ prebuiltAppConfig = _yield$Promise$all4$.prebuiltAppConfig;
146
668
  customModelRecord = customModelConfig ? _objectSpread(_objectSpread({
147
669
  model: customModelConfig.model,
148
670
  model_id: modelId,
@@ -158,34 +680,43 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
158
680
  } : {}) : undefined;
159
681
  appConfig = {
160
682
  model_list: [].concat(_toConsumableArray(prebuiltAppConfig.model_list), _toConsumableArray(customModelRecord ? [customModelRecord] : []))
161
- };
162
- _context.next = 3;
163
- return CreateMLCEngine(modelId, {
683
+ }; // Construct the engine with the logit-capture processor registered for
684
+ // the causal LM only (the embedder never decodes tokens), then load
685
+ // both the LM and the embedder into the same engine (multi-model).
686
+ newEngine = new MLCEngineCtor({
164
687
  appConfig: appConfig,
165
- initProgressCallback: initProgressCallback
688
+ initProgressCallback: initProgressCallback,
689
+ logitProcessorRegistry: new Map([[modelId, lmLogitsCapture]])
166
690
  });
691
+ _context2.next = 3;
692
+ return newEngine.reload([modelId, LOCAL_MLC_EMBEDDING_MODEL_ID]);
167
693
  case 3:
168
- engine = _context.sent;
169
694
  if (!destroyed) {
170
- _context.next = 4;
695
+ _context2.next = 4;
171
696
  break;
172
697
  }
173
698
  // destroy() was called while we were loading — clean up
174
- unloadEngine(engine);
175
- engine = null;
176
- return _context.abrupt("return");
699
+ unloadEngine(newEngine);
700
+ return _context2.abrupt("return");
177
701
  case 4:
702
+ engine = newEngine;
178
703
  ready = true;
179
704
  if (isAutocompleteDebugEnabled()) {
180
705
  // eslint-disable-next-line no-console
181
- console.log('%c[LocalSlowLane] %c✅ MLC engine loaded and ready', 'color: #9c27b0; font-weight: bold;', 'color: #4caf50;');
706
+ console.log('%c[LocalSlowLane] %c✅ Both models loaded and ready', 'color: #9c27b0; font-weight: bold;', 'color: #4caf50;');
707
+ // One-time identity summary so you can confirm which models are active
708
+ // without digging through the init-progress scroll.
709
+ // eslint-disable-next-line no-console
710
+ console.log('%c[LocalSlowLane] %c🧠 Causal LM →', 'color: #9c27b0; font-weight: bold;', 'color: #2196f3; font-weight: bold;', modelId);
711
+ // eslint-disable-next-line no-console
712
+ console.log('%c[LocalSlowLane] %c🔢 Embedder →', 'color: #9c27b0; font-weight: bold;', 'color: #009688; font-weight: bold;', LOCAL_MLC_EMBEDDING_MODEL_ID);
182
713
  }
183
714
  onStatus === null || onStatus === void 0 || onStatus('Model loaded and ready.');
184
- _context.next = 6;
715
+ _context2.next = 6;
185
716
  break;
186
717
  case 5:
187
- _context.prev = 5;
188
- _t = _context["catch"](0);
718
+ _context2.prev = 5;
719
+ _t = _context2["catch"](0);
189
720
  errorMsg = _t instanceof Error ? _t.message : String(_t);
190
721
  ready = false;
191
722
  if (isAutocompleteDebugEnabled()) {
@@ -197,12 +728,12 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
197
728
  initFailed = true;
198
729
  case 6:
199
730
  case "end":
200
- return _context.stop();
731
+ return _context2.stop();
201
732
  }
202
- }, _callee, null, [[0, 5]]);
733
+ }, _callee2, null, [[0, 5]]);
203
734
  }));
204
735
  return function initEngine() {
205
- return _ref.apply(this, arguments);
736
+ return _ref8.apply(this, arguments);
206
737
  };
207
738
  }();
208
739
  var ensureEngineInitialized = function ensureEngineInitialized() {
@@ -218,110 +749,131 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
218
749
  // ── Inference ──────────────────────────────────────────────────────────
219
750
 
220
751
  /**
221
- * Run a single forward pass to extract next-token logit probabilities.
752
+ * Run a single forward pass to produce the BE-parity slow-lane outputs.
222
753
  *
223
- * We use the chat completions API with `max_tokens: 1` and `logprobs: true`
224
- * to get the model's next-token distribution without generating text.
225
- * This is the cheapest possible inference call — a single forward pass.
754
+ * Two calls run in parallel on the shared engine:
755
+ * - `completions.create({ max_tokens: 1 })` runs the causal LM for exactly
756
+ * one decode step. We ignore the generated text; the LogitProcessor
757
+ * captures the raw next-token logits during that step, which we turn into
758
+ * a whole-word payload via `computeBePayload`.
759
+ * - `embeddings.create(...)` runs the Arctic embedder to produce the real
760
+ * 384-d semantic vector (passage-encoded; see `wrapForArctic`).
226
761
  */
227
762
  var runInference = /*#__PURE__*/function () {
228
- var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(text, requestId) {
229
- var _response$choices, response, lmLogits, logprobsContent, tokenLogprobs, token, _iterator, _step, alt, _token, logitValues, topTokens, errorMsg, _t2;
230
- return _regeneratorRuntime.wrap(function (_context2) {
231
- while (1) switch (_context2.prev = _context2.next) {
763
+ var _ref9 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee3(text, requestId) {
764
+ var lmText, semanticText, arcticInput, captureCompletionTime, _data, tStart, tLmDone, tEmbDone, _yield$Promise$all5, _yield$Promise$all6, embeddingResponse, rawLogits, payload, embedding, sumSq, i, topTokens, errorMsg, _t2;
765
+ return _regeneratorRuntime.wrap(function (_context3) {
766
+ while (1) switch (_context3.prev = _context3.next) {
232
767
  case 0:
233
768
  if (!(!engine || destroyed)) {
234
- _context2.next = 1;
769
+ _context3.next = 1;
235
770
  break;
236
771
  }
237
- return _context2.abrupt("return");
772
+ return _context3.abrupt("return");
238
773
  case 1:
239
- _context2.prev = 1;
240
- _context2.next = 2;
241
- return engine.chat.completions.create({
242
- messages: [{
243
- role: 'user',
244
- content: text
245
- }],
246
- max_tokens: 1,
247
- logprobs: true,
248
- top_logprobs: 5,
249
- temperature: 0
250
- });
251
- case 2:
252
- response = _context2.sent;
253
- if (!(requestId < latestRequestId || destroyed)) {
254
- _context2.next = 3;
255
- break;
774
+ // Clear the capture buffer so we read only this pass's logits. The engine
775
+ // serialises per-model requests and updateContext is debounced, so the
776
+ // latest request's decode step is the last to populate `captured` before
777
+ // we read it below; stale requests bail on the latestRequestId guard.
778
+ lmLogitsCapture.resetState();
779
+
780
+ // Apply BE-parity rolling-window truncation before both encoders.
781
+ // BE semantic: last max_context_words words (typeahead_context_encoding.py:36)
782
+ // BE causal LM: last max_context_tokens BPE tokens (causal_lm_encoder.py:194–198),
783
+ // approximated here with word count (no tokenizer available on FE).
784
+ lmText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_TOKENS);
785
+ semanticText = truncateToLastNWords(text, BE_PARITY.MAX_CONTEXT_WORDS);
786
+ arcticInput = wrapForArctic(semanticText);
787
+ captureCompletionTime = function captureCompletionTime(promise, onResolved) {
788
+ return promise.then(function (value) {
789
+ onResolved(performance.now());
790
+ return value;
791
+ });
792
+ };
793
+ if (isAutocompleteDebugEnabled()) {
794
+ // eslint-disable-next-line no-console
795
+ console.log("%c[LocalSlowLane] %c\uD83D\uDD22 Arctic input (".concat(arcticInput.length, " chars, ").concat(splitOnWhitespace(semanticText).length, " words): \"").concat(arcticInput.length > 100 ? "".concat(arcticInput.slice(0, 100), "\u2026") : arcticInput, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #009688;');
796
+ // eslint-disable-next-line no-console
797
+ console.log("%c[LocalSlowLane] %c\uD83E\uDDE0 LM input (".concat(lmText.length, " chars, ").concat(splitOnWhitespace(lmText).length, " words): \"").concat(lmText.length > 100 ? "".concat(lmText.slice(0, 100), "\u2026") : lmText, "\""), 'color: #9c27b0; font-weight: bold;', 'color: #2196f3;');
256
798
  }
257
- return _context2.abrupt("return");
799
+ _context3.prev = 2;
800
+ tStart = performance.now();
801
+ tLmDone = 0;
802
+ tEmbDone = 0;
803
+ _context3.next = 3;
804
+ return Promise.all([captureCompletionTime(engine.completions.create({
805
+ model: modelId,
806
+ prompt: lmText,
807
+ max_tokens: 1,
808
+ temperature: 0,
809
+ logprobs: false
810
+ }), function (resolvedAt) {
811
+ tLmDone = resolvedAt;
812
+ }), captureCompletionTime(engine.embeddings.create({
813
+ model: LOCAL_MLC_EMBEDDING_MODEL_ID,
814
+ input: arcticInput
815
+ }), function (resolvedAt) {
816
+ tEmbDone = resolvedAt;
817
+ })]);
258
818
  case 3:
259
- // ── Extract LM logits ───────────────────────────────────────
260
- lmLogits = {};
261
- logprobsContent = (_response$choices = response.choices) === null || _response$choices === void 0 || (_response$choices = _response$choices[0]) === null || _response$choices === void 0 || (_response$choices = _response$choices.logprobs) === null || _response$choices === void 0 ? void 0 : _response$choices.content;
262
- if (logprobsContent && logprobsContent.length > 0) {
263
- tokenLogprobs = logprobsContent[0]; // Add the top token
264
- if (tokenLogprobs.token) {
265
- token = tokenLogprobs.token.trim().toLowerCase(); // @ts-ignore TS1501: Unicode regex flag requires a newer TS target than the declaration build uses.
266
- if (token.length > 0 && /^[a-z\u017F\u212A]/i.test(token)) {
267
- lmLogits[token] = Math.exp(tokenLogprobs.logprob);
268
- }
269
- }
819
+ _yield$Promise$all5 = _context3.sent;
820
+ _yield$Promise$all6 = _slicedToArray(_yield$Promise$all5, 2);
821
+ embeddingResponse = _yield$Promise$all6[1];
822
+ if (isAutocompleteDebugEnabled()) {
823
+ // eslint-disable-next-line no-console
824
+ console.log("%c[LocalSlowLane] %c\u23F1 LM: ".concat((tLmDone - tStart).toFixed(0), "ms | Embedder: ").concat((tEmbDone - tStart).toFixed(0), "ms | Total: ").concat((Math.max(tLmDone, tEmbDone) - tStart).toFixed(0), "ms"), 'color: #9c27b0; font-weight: bold;', 'color: #ff9800;');
825
+ }
270
826
 
271
- // Add alternative tokens from top_logprobs
272
- if (tokenLogprobs.top_logprobs) {
273
- _iterator = _createForOfIteratorHelper(tokenLogprobs.top_logprobs);
274
- try {
275
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
276
- alt = _step.value;
277
- _token = alt.token.trim().toLowerCase(); // @ts-ignore TS1501: Unicode regex flag requires a newer TS target than the declaration build uses.
278
- if (_token.length > 0 && /^[a-z\u017F\u212A]/i.test(_token)) {
279
- lmLogits[_token] = Math.exp(alt.logprob);
280
- }
281
- }
282
- } catch (err) {
283
- _iterator.e(err);
284
- } finally {
285
- _iterator.f();
286
- }
287
- }
827
+ // Discard stale results
828
+ if (!(requestId < latestRequestId || destroyed)) {
829
+ _context3.next = 4;
830
+ break;
288
831
  }
289
- storedLmLogits = Object.keys(lmLogits).length > 0 ? lmLogits : null;
290
-
291
- // ── Semantic vector ─────────────────────────────────────────
292
- // SmolLM is a generative model, not an embedding model, so we
293
- // don't get a true semantic vector. We generate a lightweight
294
- // pseudo-embedding from the logit distribution for compatibility
295
- // with the existing scoring pipeline.
296
- //
297
- // For a production implementation, you would use a dedicated
298
- // embedding model (e.g. via web-llm's embeddings API with an
299
- // embedding-specific model).
300
- if (storedLmLogits) {
301
- logitValues = Object.values(storedLmLogits);
302
- storedContextVector = new Float32Array(logitValues);
832
+ return _context3.abrupt("return");
833
+ case 4:
834
+ // ── LM logits: whole-word BE-parity payload ──────────────────
835
+ rawLogits = lmLogitsCapture.captured;
836
+ if (rawLogits) {
837
+ payload = computeBePayload(rawLogits, firstTokenToWords, l2Words);
838
+ storedLmLogits = Object.keys(payload).length > 0 ? payload : null;
303
839
  } else {
304
- storedContextVector = null;
840
+ storedLmLogits = null;
305
841
  }
842
+
843
+ // ── Semantic vector: real 384-d Arctic embedding ─────────────
844
+ // Guard against base64-encoded responses (encoding_format: 'base64' would
845
+ // yield a string, and new Float32Array(string) silently produces an empty
846
+ // array, corrupting downstream cosine-similarity scoring).
847
+ embedding = (_data = embeddingResponse.data) === null || _data === void 0 || (_data = _data[0]) === null || _data === void 0 ? void 0 : _data.embedding;
848
+ storedContextVector = Array.isArray(embedding) && embedding.length > 0 ? new Float32Array(embedding) : null;
306
849
  if (isAutocompleteDebugEnabled()) {
307
850
  // eslint-disable-next-line no-console
308
851
  console.groupCollapsed("%c[LocalSlowLane] %c\uD83D\uDCE5 Inference result (request #".concat(requestId, ")"), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
852
+ if (storedContextVector) {
853
+ sumSq = 0;
854
+ for (i = 0; i < storedContextVector.length; i++) {
855
+ sumSq += storedContextVector[i] * storedContextVector[i];
856
+ }
857
+ // eslint-disable-next-line no-console
858
+ console.log("\u2705 semantic vector: ".concat(storedContextVector.length, " dims (L2 norm ").concat(Math.sqrt(sumSq).toFixed(3), ")"));
859
+ } else {
860
+ // eslint-disable-next-line no-console
861
+ console.log('❌ No vector');
862
+ }
309
863
  // eslint-disable-next-line no-console
310
- console.log(storedContextVector ? "\u2705 pseudo-vector: ".concat(storedContextVector.length, " dims") : '❌ No vector');
311
- // eslint-disable-next-line no-console
312
- console.log(storedLmLogits ? "\u2705 lm_logits: ".concat(Object.keys(storedLmLogits).length, " tokens") : '❌ No lm_logits');
864
+ console.log(storedLmLogits ? "\u2705 lm_logits: ".concat(Object.keys(storedLmLogits).length, " words") : '❌ No lm_logits');
313
865
  if (storedLmLogits) {
314
- topTokens = Object.entries(storedLmLogits).sort(function (_ref3, _ref4) {
315
- var _ref5 = _slicedToArray(_ref3, 2),
316
- a = _ref5[1];
317
- var _ref6 = _slicedToArray(_ref4, 2),
318
- b = _ref6[1];
866
+ topTokens = Object.entries(storedLmLogits).sort(function (_ref0, _ref1) {
867
+ var _ref10 = _slicedToArray(_ref0, 2),
868
+ a = _ref10[1];
869
+ var _ref11 = _slicedToArray(_ref1, 2),
870
+ b = _ref11[1];
319
871
  return b - a;
320
872
  }).slice(0, 10); // eslint-disable-next-line no-console
321
- console.log('Top 10 predictions:', topTokens.map(function (_ref7) {
322
- var _ref8 = _slicedToArray(_ref7, 2),
323
- t = _ref8[0],
324
- p = _ref8[1];
873
+ console.log('Top 10 predictions:', topTokens.map(function (_ref12) {
874
+ var _ref13 = _slicedToArray(_ref12, 2),
875
+ t = _ref13[0],
876
+ p = _ref13[1];
325
877
  return "".concat(t, ": ").concat((p * 100).toFixed(1), "%");
326
878
  }).join(', '));
327
879
  }
@@ -333,17 +885,17 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
333
885
  hasVector: storedContextVector !== null,
334
886
  hasLmLogits: storedLmLogits !== null
335
887
  });
336
- _context2.next = 6;
888
+ _context3.next = 7;
337
889
  break;
338
- case 4:
339
- _context2.prev = 4;
340
- _t2 = _context2["catch"](1);
890
+ case 5:
891
+ _context3.prev = 5;
892
+ _t2 = _context3["catch"](2);
341
893
  if (!(requestId < latestRequestId)) {
342
- _context2.next = 5;
894
+ _context3.next = 6;
343
895
  break;
344
896
  }
345
- return _context2.abrupt("return");
346
- case 5:
897
+ return _context3.abrupt("return");
898
+ case 6:
347
899
  storedContextVector = null;
348
900
  storedLmLogits = null;
349
901
  onUpdate === null || onUpdate === void 0 || onUpdate({
@@ -356,14 +908,14 @@ export var createLocalSlowLaneClient = function createLocalSlowLaneClient() {
356
908
  // eslint-disable-next-line no-console
357
909
  console.log("%c[LocalSlowLane] %c\u274C Inference error (request #".concat(requestId, "): ").concat(errorMsg), 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
358
910
  }
359
- case 6:
911
+ case 7:
360
912
  case "end":
361
- return _context2.stop();
913
+ return _context3.stop();
362
914
  }
363
- }, _callee2, null, [[1, 4]]);
915
+ }, _callee3, null, [[2, 5]]);
364
916
  }));
365
917
  return function runInference(_x, _x2) {
366
- return _ref2.apply(this, arguments);
918
+ return _ref9.apply(this, arguments);
367
919
  };
368
920
  }();
369
921