@atlaskit/editor-plugin-autocomplete 3.5.0 → 3.6.1

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.
@@ -60,6 +60,10 @@ export interface LocalSlowLaneClientConfig {
60
60
  * `customModelConfig` with the model URL and WASM library URL.
61
61
  */
62
62
  modelId?: string;
63
+ /** Callback fired when the engine fails to load/start. */
64
+ onLoadError?: (error: LocalSlowLaneLoadError) => void;
65
+ /** Callback fired when the engine successfully loads and is ready. */
66
+ onLoadSuccess?: (info: LocalSlowLaneLoadSuccess) => void;
63
67
  /** Callback fired with status messages (model loading progress, etc.). */
64
68
  onStatus?: (message: string) => void;
65
69
  /** Callback fired when inference returns new results. */
@@ -81,6 +85,54 @@ export interface LocalSlowLaneClient {
81
85
  setLmLogits: (logits: Record<string, number> | null) => void;
82
86
  updateContext: (text: string) => void;
83
87
  }
88
+ /**
89
+ * Why the local engine failed to load/start.
90
+ *
91
+ * The first three are user-machine limitations (WebGPU missing, no compatible
92
+ * GPU adapter, GPU lacks the `shader-f16` feature the model needs);
93
+ * `insufficient_memory` is hit when weights don't fit in VRAM. The rest cover
94
+ * delivery/runtime failures unrelated to hardware.
95
+ */
96
+ export type LocalSlowLaneLoadErrorReason = 'webgpu_unavailable' | 'webgpu_no_adapter' | 'missing_shader_f16' | 'insufficient_memory' | 'model_download_failed' | 'module_load_failed' | 'init_failed';
97
+ /** Snapshot of the machine's WebGPU support, used to explain hardware limits. */
98
+ export interface WebGpuCapabilities {
99
+ /** Whether `navigator.gpu.requestAdapter()` returned a usable adapter. */
100
+ adapterAvailable?: boolean;
101
+ /** GPU architecture reported by the adapter (e.g. "metal-3", "rdna2"). */
102
+ architecture?: string;
103
+ /** Whether `navigator.gpu` exists at all. */
104
+ available: boolean;
105
+ /** Largest single GPU buffer the adapter allows, in MB. */
106
+ maxBufferSizeMB?: number;
107
+ /** Largest storage-buffer binding the adapter allows, in MB. */
108
+ maxStorageBufferBindingSizeMB?: number;
109
+ /** Whether the adapter exposes the `shader-f16` feature the model requires. */
110
+ shaderF16Supported?: boolean;
111
+ /** GPU vendor reported by the adapter (e.g. "apple", "intel"). */
112
+ vendor?: string;
113
+ }
114
+ export interface LocalSlowLaneLoadError {
115
+ /** WebGPU support snapshot — explains hardware limitations behind the failure. */
116
+ capabilities: WebGpuCapabilities;
117
+ /** Semantic embedder loaded alongside the causal LM (loads atomically). */
118
+ embeddingModelId: string;
119
+ /** Canonical, controlled failure description (never raw error text). */
120
+ message: string;
121
+ /** Causal LM identifier that failed to load. */
122
+ modelId: string;
123
+ /** Coarse, privacy-safe failure category. */
124
+ reason: LocalSlowLaneLoadErrorReason;
125
+ }
126
+ export interface LocalSlowLaneLoadSuccess {
127
+ /** WebGPU support snapshot for the machine that loaded the model. */
128
+ capabilities: WebGpuCapabilities;
129
+ /** Semantic embedder loaded alongside the causal LM (loads atomically). */
130
+ embeddingModelId: string;
131
+ /** Model engine load time in ms (excludes the WebGPU capability probe). */
132
+ loadDurationMs: number;
133
+ /** Causal LM identifier that loaded. */
134
+ modelId: string;
135
+ }
84
136
  export declare const LOCAL_MLC_CAUSAL_MODEL_ID = "SmolLM2-135M-Instruct-q0f16-MLC";
85
137
  /**
86
138
  * MLC ID for the semantic embedder (Snowflake Arctic Embed S, batch=4 variant).
@@ -82,7 +82,10 @@ export declare const ingestDocumentPage: (pageContent: string | undefined) => vo
82
82
  export declare const predict: (textBefore: string) => string | null;
83
83
  export declare const loadVectorsAsync: (options?: {
84
84
  getBinaryUrl?: () => Promise<string>;
85
+ isLocalLLM?: boolean;
85
86
  }) => Promise<void>;
86
87
  export declare const initVectors: (store: VectorStore) => void;
87
- export declare const loadDefaultVocabulary: () => Promise<void>;
88
+ export declare const loadDefaultVocabulary: (options?: {
89
+ isLocalLLM?: boolean;
90
+ }) => Promise<void>;
88
91
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete",
3
- "version": "3.5.0",
3
+ "version": "3.6.1",
4
4
  "description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -35,7 +35,7 @@
35
35
  "wink-nlp": "^2.4.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@atlaskit/editor-common": "^115.8.0",
38
+ "@atlaskit/editor-common": "^115.15.0",
39
39
  "@atlaskit/editor-plugin-analytics": "^11.0.0",
40
40
  "react": "^18.2.0"
41
41
  },
@@ -16,7 +16,12 @@ import type { AutocompletePlugin } from '../autocompletePluginType';
16
16
 
17
17
  import { isAutocompleteDebugEnabled } from './debug-mode';
18
18
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
19
- import { createLocalSlowLaneClient, type LocalSlowLaneClient } from './local-slow-lane-client';
19
+ import {
20
+ createLocalSlowLaneClient,
21
+ type LocalSlowLaneClient,
22
+ type LocalSlowLaneLoadError,
23
+ type LocalSlowLaneLoadSuccess,
24
+ } from './local-slow-lane-client';
20
25
  import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
21
26
  import {
22
27
  predict,
@@ -298,6 +303,43 @@ export const createAutocompletePlugin = (
298
303
  });
299
304
  };
300
305
 
306
+ const fireLocalModelLoadedAnalytics = (info: LocalSlowLaneLoadSuccess): void => {
307
+ api?.analytics?.actions.fireAnalyticsEvent({
308
+ action: ACTION.LOCAL_MODEL_LOADED,
309
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
310
+ eventType: EVENT_TYPE.TRACK,
311
+ attributes: {
312
+ modelId: info.modelId,
313
+ embeddingModelId: info.embeddingModelId,
314
+ loadDurationMs: info.loadDurationMs,
315
+ gpuVendor: info.capabilities.vendor,
316
+ gpuArchitecture: info.capabilities.architecture,
317
+ },
318
+ });
319
+ };
320
+
321
+ const fireLocalModelLoadFailedAnalytics = (error: LocalSlowLaneLoadError): void => {
322
+ const { capabilities } = error;
323
+ api?.analytics?.actions.fireAnalyticsEvent({
324
+ action: ACTION.LOCAL_MODEL_LOAD_FAILED,
325
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
326
+ eventType: EVENT_TYPE.TRACK,
327
+ attributes: {
328
+ reason: error.reason,
329
+ message: error.message,
330
+ modelId: error.modelId,
331
+ embeddingModelId: error.embeddingModelId,
332
+ webgpuAvailable: capabilities.available,
333
+ adapterAvailable: capabilities.adapterAvailable,
334
+ shaderF16Supported: capabilities.shaderF16Supported,
335
+ maxBufferSizeMB: capabilities.maxBufferSizeMB,
336
+ maxStorageBufferBindingSizeMB: capabilities.maxStorageBufferBindingSizeMB,
337
+ gpuVendor: capabilities.vendor,
338
+ gpuArchitecture: capabilities.architecture,
339
+ },
340
+ });
341
+ };
342
+
301
343
  const fireSuggestionInsertedAnalytics = (ghostText: string): void => {
302
344
  const typedLength = lastSuggestionTypedLength;
303
345
  const suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
@@ -319,6 +361,8 @@ export const createAutocompletePlugin = (
319
361
  const slowLaneClient = options?.useLocalModel
320
362
  ? createLocalSlowLaneClient({
321
363
  debounceMs: LOCAL_SLOW_LANE_DEBOUNCE_MS,
364
+ onLoadSuccess: fireLocalModelLoadedAnalytics,
365
+ onLoadError: fireLocalModelLoadFailedAnalytics,
322
366
  })
323
367
  : createSlowLaneClient({
324
368
  baseUrl: '',
@@ -636,17 +680,20 @@ export const createAutocompletePlugin = (
636
680
  }
637
681
  return false;
638
682
  },
639
- focus: () => {
640
- loadDefaultVocabulary().catch((error) => {
641
- logException(error as Error, {
642
- location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
643
- });
683
+ focus: () => {
684
+ loadDefaultVocabulary({ isLocalLLM: options?.useLocalModel ?? false }).catch((error) => {
685
+ logException(error as Error, {
686
+ location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
644
687
  });
645
- loadVectorsAsync({ getBinaryUrl: options?.getVectorsBinaryUrl }).catch((error) => {
646
- logException(error as Error, {
647
- location: 'editor-plugin-autocomplete/loadVectorsAsync',
648
- });
688
+ });
689
+ loadVectorsAsync({
690
+ getBinaryUrl: options?.getVectorsBinaryUrl,
691
+ isLocalLLM: options?.useLocalModel ?? false,
692
+ }).catch((error) => {
693
+ logException(error as Error, {
694
+ location: 'editor-plugin-autocomplete/loadVectorsAsync',
649
695
  });
696
+ });
650
697
  if (!hasIngestedPage) {
651
698
  hasIngestedPage = true;
652
699
  refreshContext({
@@ -71,6 +71,10 @@ export interface LocalSlowLaneClientConfig {
71
71
  * `customModelConfig` with the model URL and WASM library URL.
72
72
  */
73
73
  modelId?: string;
74
+ /** Callback fired when the engine fails to load/start. */
75
+ onLoadError?: (error: LocalSlowLaneLoadError) => void;
76
+ /** Callback fired when the engine successfully loads and is ready. */
77
+ onLoadSuccess?: (info: LocalSlowLaneLoadSuccess) => void;
74
78
  /** Callback fired with status messages (model loading progress, etc.). */
75
79
  onStatus?: (message: string) => void;
76
80
  /** Callback fired when inference returns new results. */
@@ -91,6 +95,80 @@ export interface LocalSlowLaneClient {
91
95
  updateContext: (text: string) => void;
92
96
  }
93
97
 
98
+ /**
99
+ * Why the local engine failed to load/start.
100
+ *
101
+ * The first three are user-machine limitations (WebGPU missing, no compatible
102
+ * GPU adapter, GPU lacks the `shader-f16` feature the model needs);
103
+ * `insufficient_memory` is hit when weights don't fit in VRAM. The rest cover
104
+ * delivery/runtime failures unrelated to hardware.
105
+ */
106
+ export type LocalSlowLaneLoadErrorReason =
107
+ | 'webgpu_unavailable'
108
+ | 'webgpu_no_adapter'
109
+ | 'missing_shader_f16'
110
+ | 'insufficient_memory'
111
+ | 'model_download_failed'
112
+ | 'module_load_failed'
113
+ | 'init_failed';
114
+
115
+ /** Snapshot of the machine's WebGPU support, used to explain hardware limits. */
116
+ export interface WebGpuCapabilities {
117
+ /** Whether `navigator.gpu.requestAdapter()` returned a usable adapter. */
118
+ adapterAvailable?: boolean;
119
+ /** GPU architecture reported by the adapter (e.g. "metal-3", "rdna2"). */
120
+ architecture?: string;
121
+ /** Whether `navigator.gpu` exists at all. */
122
+ available: boolean;
123
+ /** Largest single GPU buffer the adapter allows, in MB. */
124
+ maxBufferSizeMB?: number;
125
+ /** Largest storage-buffer binding the adapter allows, in MB. */
126
+ maxStorageBufferBindingSizeMB?: number;
127
+ /** Whether the adapter exposes the `shader-f16` feature the model requires. */
128
+ shaderF16Supported?: boolean;
129
+ /** GPU vendor reported by the adapter (e.g. "apple", "intel"). */
130
+ vendor?: string;
131
+ }
132
+
133
+ export interface LocalSlowLaneLoadError {
134
+ /** WebGPU support snapshot — explains hardware limitations behind the failure. */
135
+ capabilities: WebGpuCapabilities;
136
+ /** Semantic embedder loaded alongside the causal LM (loads atomically). */
137
+ embeddingModelId: string;
138
+ /** Canonical, controlled failure description (never raw error text). */
139
+ message: string;
140
+ /** Causal LM identifier that failed to load. */
141
+ modelId: string;
142
+ /** Coarse, privacy-safe failure category. */
143
+ reason: LocalSlowLaneLoadErrorReason;
144
+ }
145
+
146
+ export interface LocalSlowLaneLoadSuccess {
147
+ /** WebGPU support snapshot for the machine that loaded the model. */
148
+ capabilities: WebGpuCapabilities;
149
+ /** Semantic embedder loaded alongside the causal LM (loads atomically). */
150
+ embeddingModelId: string;
151
+ /** Model engine load time in ms (excludes the WebGPU capability probe). */
152
+ loadDurationMs: number;
153
+ /** Causal LM identifier that loaded. */
154
+ modelId: string;
155
+ }
156
+
157
+ // Minimal WebGPU shape: lib.dom types aren't guaranteed in this build target.
158
+ interface MinimalGpuAdapterInfo {
159
+ architecture?: string;
160
+ vendor?: string;
161
+ }
162
+ interface MinimalGpuAdapter {
163
+ features: { has: (feature: string) => boolean };
164
+ info?: MinimalGpuAdapterInfo;
165
+ limits?: { maxBufferSize?: number; maxStorageBufferBindingSize?: number };
166
+ requestAdapterInfo?: () => Promise<MinimalGpuAdapterInfo>;
167
+ }
168
+ interface MinimalGpu {
169
+ requestAdapter: () => Promise<MinimalGpuAdapter | null>;
170
+ }
171
+
94
172
  // ─── Constants ───────────────────────────────────────────────────────────────
95
173
 
96
174
  const DEFAULT_DEBOUNCE_MS = 300;
@@ -584,6 +662,8 @@ export const createLocalSlowLaneClient = (
584
662
  debounceMs = DEFAULT_DEBOUNCE_MS,
585
663
  onUpdate,
586
664
  onStatus,
665
+ onLoadError,
666
+ onLoadSuccess,
587
667
  modelId = LOCAL_MLC_CAUSAL_MODEL_ID,
588
668
  customModelConfig,
589
669
  } = config;
@@ -641,7 +721,143 @@ export const createLocalSlowLaneClient = (
641
721
  onStatus?.(message);
642
722
  };
643
723
 
724
+ const bytesToMB = (bytes?: number): number | undefined =>
725
+ typeof bytes === 'number' ? Math.round(bytes / (1024 * 1024)) : undefined;
726
+
727
+ /**
728
+ * Inspect the machine's WebGPU support so a load failure can be attributed
729
+ * to a concrete hardware/browser limitation rather than a generic error.
730
+ */
731
+ const probeWebGpuCapabilities = async (): Promise<WebGpuCapabilities> => {
732
+ const gpu = (navigator as Navigator & { gpu?: MinimalGpu }).gpu;
733
+ if (!gpu) {
734
+ return { available: false };
735
+ }
736
+ try {
737
+ const adapter = await gpu.requestAdapter();
738
+ if (!adapter) {
739
+ return { available: true, adapterAvailable: false };
740
+ }
741
+ let vendor: string | undefined;
742
+ let architecture: string | undefined;
743
+ try {
744
+ const info = adapter.info ?? (await adapter.requestAdapterInfo?.());
745
+ vendor = info?.vendor || undefined;
746
+ architecture = info?.architecture || undefined;
747
+ } catch {
748
+ // adapter info is best-effort
749
+ }
750
+ return {
751
+ available: true,
752
+ adapterAvailable: true,
753
+ shaderF16Supported: adapter.features.has('shader-f16'),
754
+ maxBufferSizeMB: bytesToMB(adapter.limits?.maxBufferSize),
755
+ maxStorageBufferBindingSizeMB: bytesToMB(adapter.limits?.maxStorageBufferBindingSize),
756
+ vendor,
757
+ architecture,
758
+ };
759
+ } catch {
760
+ return { available: true, adapterAvailable: false };
761
+ }
762
+ };
763
+
764
+ /** Map an MLC/WebLLM engine-creation error message to a coarse reason. */
765
+ const classifyEngineError = (message: string): LocalSlowLaneLoadErrorReason => {
766
+ const lower = message.toLowerCase();
767
+ if (
768
+ lower.includes('loading chunk') ||
769
+ lower.includes('dynamically imported module') ||
770
+ lower.includes('dynamic import')
771
+ ) {
772
+ return 'module_load_failed';
773
+ }
774
+ if (
775
+ lower.includes('out of memory') ||
776
+ // eslint-disable-next-line require-unicode-regexp
777
+ /\boom\b/.test(lower) ||
778
+ lower.includes('allocation') ||
779
+ lower.includes('exceeds') ||
780
+ lower.includes('buffer size') ||
781
+ lower.includes('not enough memory')
782
+ ) {
783
+ return 'insufficient_memory';
784
+ }
785
+ // Pre-flight already returns missing_shader_f16 when the feature is absent,
786
+ // so only match the exact feature token here — not bare 'shader' (compile
787
+ // errors) or bare 'f16' (present in model ids like q0f16-MLC).
788
+ if (lower.includes('shader-f16') || lower.includes('shader_f16')) {
789
+ return 'missing_shader_f16';
790
+ }
791
+ if (
792
+ lower.includes('fetch') ||
793
+ lower.includes('network') ||
794
+ lower.includes('download') ||
795
+ lower.includes('http') ||
796
+ lower.includes('cache')
797
+ ) {
798
+ return 'model_download_failed';
799
+ }
800
+ return 'init_failed';
801
+ };
802
+
803
+ // Canonical, controlled failure descriptions. We never emit the raw engine
804
+ // error into analytics — it can embed customer-context URLs/paths (HOT-120175)
805
+ // — so the analytics `message` is always one of these fixed strings.
806
+ const LOAD_FAILURE_MESSAGE: Record<LocalSlowLaneLoadErrorReason, string> = {
807
+ webgpu_unavailable: 'WebGPU is not available in this browser',
808
+ webgpu_no_adapter: 'No compatible WebGPU adapter found',
809
+ missing_shader_f16: 'GPU does not support the shader-f16 feature',
810
+ insufficient_memory: 'Insufficient GPU memory to load the model',
811
+ model_download_failed: 'Failed to download model assets',
812
+ module_load_failed: 'Failed to load the web-llm runtime module',
813
+ init_failed: 'Model engine failed to initialise',
814
+ };
815
+
816
+ const handleLoadFailure = (
817
+ reason: LocalSlowLaneLoadErrorReason,
818
+ capabilities: WebGpuCapabilities,
819
+ // Raw engine error — local debug logging only, never sent to analytics.
820
+ debugDetail?: string,
821
+ ): void => {
822
+ ready = false;
823
+ const message = LOAD_FAILURE_MESSAGE[reason];
824
+ if (isAutocompleteDebugEnabled()) {
825
+ // eslint-disable-next-line no-console
826
+ console.log(
827
+ `[LocalSlowLane] Engine initialisation failed (${reason}): ${debugDetail ?? message}`,
828
+ );
829
+ }
830
+ onStatus?.(`Engine initialisation failed: ${message}`);
831
+ onLoadError?.({
832
+ reason,
833
+ message,
834
+ modelId,
835
+ embeddingModelId: LOCAL_MLC_EMBEDDING_MODEL_ID,
836
+ capabilities,
837
+ });
838
+ engineInitPromise = null;
839
+ initFailed = true;
840
+ };
841
+
644
842
  const initEngine = async (): Promise<void> => {
843
+ const capabilities = await probeWebGpuCapabilities();
844
+
845
+ // ── Pre-flight: machine limitations short-circuit before the expensive load ──
846
+ if (!capabilities.available) {
847
+ handleLoadFailure('webgpu_unavailable', capabilities);
848
+ return;
849
+ }
850
+ if (capabilities.adapterAvailable === false) {
851
+ handleLoadFailure('webgpu_no_adapter', capabilities);
852
+ return;
853
+ }
854
+ if (capabilities.shaderF16Supported === false) {
855
+ handleLoadFailure('missing_shader_f16', capabilities);
856
+ return;
857
+ }
858
+
859
+ const startTime = performance.now();
860
+
645
861
  try {
646
862
  if (isAutocompleteDebugEnabled()) {
647
863
  // eslint-disable-next-line no-console
@@ -653,10 +869,6 @@ export const createLocalSlowLaneClient = (
653
869
  }
654
870
  onStatus?.(`Initialising models: ${modelId} + ${LOCAL_MLC_EMBEDDING_MODEL_ID}…`);
655
871
 
656
- if (!('gpu' in navigator)) {
657
- throw new Error('WebGPU not supported');
658
- }
659
-
660
872
  // Fetch the web-llm runtime and the BE-parity lookup tables in parallel;
661
873
  // both are dynamically imported so they stay out of the main editor chunk.
662
874
  const [{ MLCEngine: MLCEngineCtor, prebuiltAppConfig }] = await Promise.all([
@@ -714,6 +926,7 @@ export const createLocalSlowLaneClient = (
714
926
 
715
927
  engine = newEngine;
716
928
  ready = true;
929
+ const loadDurationMs = Math.round(performance.now() - startTime);
717
930
 
718
931
  if (isAutocompleteDebugEnabled()) {
719
932
  // eslint-disable-next-line no-console
@@ -740,16 +953,15 @@ export const createLocalSlowLaneClient = (
740
953
  );
741
954
  }
742
955
  onStatus?.('Model loaded and ready.');
956
+ onLoadSuccess?.({
957
+ modelId,
958
+ embeddingModelId: LOCAL_MLC_EMBEDDING_MODEL_ID,
959
+ loadDurationMs,
960
+ capabilities,
961
+ });
743
962
  } catch (err) {
744
963
  const errorMsg = err instanceof Error ? err.message : String(err);
745
- ready = false;
746
- if (isAutocompleteDebugEnabled()) {
747
- // eslint-disable-next-line no-console
748
- console.log(`[LocalSlowLane] Engine initialisation failed: ${errorMsg}`);
749
- }
750
- onStatus?.(`Engine initialisation failed: ${errorMsg}`);
751
- engineInitPromise = null;
752
- initFailed = true;
964
+ handleLoadFailure(classifyEngineError(errorMsg), capabilities, errorMsg);
753
965
  }
754
966
  };
755
967
 
@@ -747,6 +747,7 @@ const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null
747
747
 
748
748
  export const loadVectorsAsync = async (options?: {
749
749
  getBinaryUrl?: () => Promise<string>;
750
+ isLocalLLM?: boolean;
750
751
  }): Promise<void> => {
751
752
  if (vectorStore || vectorsLoadStarted) {
752
753
  return;
@@ -758,15 +759,16 @@ export const loadVectorsAsync = async (options?: {
758
759
  );
759
760
  return;
760
761
  }
762
+ const isLocalLLM = options?.isLocalLLM ?? false;
761
763
  vectorsLoadStarted = true;
762
- startExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton');
764
+ startExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { isLocalLLM });
763
765
 
764
766
  let url: string;
765
767
  try {
766
768
  url = await options.getBinaryUrl();
767
769
  } catch (e) {
768
770
  vectorsLoadStarted = false;
769
- failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { errorType: 'resolve_url' });
771
+ failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { isLocalLLM, errorType: 'resolve_url' });
770
772
  // eslint-disable-next-line no-console
771
773
  console.warn('[text-predictor] Failed to resolve vectors URL:', e);
772
774
  return;
@@ -777,6 +779,7 @@ export const loadVectorsAsync = async (options?: {
777
779
  if (!res.ok) {
778
780
  vectorsLoadStarted = false;
779
781
  failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
782
+ isLocalLLM,
780
783
  status: res.status,
781
784
  errorType: 'http_error',
782
785
  });
@@ -809,6 +812,7 @@ export const loadVectorsAsync = async (options?: {
809
812
 
810
813
  vectorStore = { float32, wordIndex, dim };
811
814
  succeedExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
815
+ isLocalLLM,
812
816
  wordCount: nWords,
813
817
  dim,
814
818
  sizeBytes: float32.byteLength,
@@ -823,7 +827,7 @@ export const loadVectorsAsync = async (options?: {
823
827
  }
824
828
  } catch (e) {
825
829
  vectorsLoadStarted = false;
826
- failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { errorType: 'network' });
830
+ failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { isLocalLLM, errorType: 'network' });
827
831
  // eslint-disable-next-line no-console
828
832
  console.warn('[text-predictor] Failed to load vectors:', e);
829
833
  }
@@ -835,7 +839,7 @@ export const initVectors = (store: VectorStore): void => {
835
839
 
836
840
  let vocabularyLoadPromise: Promise<void> | undefined;
837
841
 
838
- export const loadDefaultVocabulary = (): Promise<void> => {
842
+ export const loadDefaultVocabulary = (options?: { isLocalLLM?: boolean }): Promise<void> => {
839
843
  if (isInitialized) {
840
844
  return Promise.resolve();
841
845
  }
@@ -843,8 +847,9 @@ export const loadDefaultVocabulary = (): Promise<void> => {
843
847
  return vocabularyLoadPromise;
844
848
  }
845
849
 
850
+ const isLocalLLM = options?.isLocalLLM ?? false;
846
851
  vocabularyLoadPromise = (async () => {
847
- startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
852
+ startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', { isLocalLLM });
848
853
 
849
854
  try {
850
855
  // The L2 vocabulary and L3 word list are code-split into their own async
@@ -879,11 +884,12 @@ export const loadDefaultVocabulary = (): Promise<void> => {
879
884
  initVocabulary({ terms });
880
885
 
881
886
  succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
887
+ isLocalLLM,
882
888
  l2WordCount: terms.length,
883
889
  l3WordCount: l3VocabularyData.length,
884
890
  });
885
891
  } catch (e) {
886
- failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', { errorType: 'parse_error' });
892
+ failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', { isLocalLLM, errorType: 'parse_error' });
887
893
  // Allow a later call to retry the load rather than caching the failure.
888
894
  vocabularyLoadPromise = undefined;
889
895
  throw e;