@atlaskit/editor-plugin-autocomplete 3.5.0 → 3.6.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.
@@ -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
+ /** GPU architecture reported by the adapter (e.g. "metal-3", "rdna2"). */
118
+ architecture?: string;
119
+ /** Whether `navigator.gpu.requestAdapter()` returned a usable adapter. */
120
+ adapterAvailable?: boolean;
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,142 @@ 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
+ /\boom\b/u.test(lower) ||
777
+ lower.includes('allocation') ||
778
+ lower.includes('exceeds') ||
779
+ lower.includes('buffer size') ||
780
+ lower.includes('not enough memory')
781
+ ) {
782
+ return 'insufficient_memory';
783
+ }
784
+ // Pre-flight already returns missing_shader_f16 when the feature is absent,
785
+ // so only match the exact feature token here — not bare 'shader' (compile
786
+ // errors) or bare 'f16' (present in model ids like q0f16-MLC).
787
+ if (lower.includes('shader-f16') || lower.includes('shader_f16')) {
788
+ return 'missing_shader_f16';
789
+ }
790
+ if (
791
+ lower.includes('fetch') ||
792
+ lower.includes('network') ||
793
+ lower.includes('download') ||
794
+ lower.includes('http') ||
795
+ lower.includes('cache')
796
+ ) {
797
+ return 'model_download_failed';
798
+ }
799
+ return 'init_failed';
800
+ };
801
+
802
+ // Canonical, controlled failure descriptions. We never emit the raw engine
803
+ // error into analytics — it can embed customer-context URLs/paths (HOT-120175)
804
+ // — so the analytics `message` is always one of these fixed strings.
805
+ const LOAD_FAILURE_MESSAGE: Record<LocalSlowLaneLoadErrorReason, string> = {
806
+ webgpu_unavailable: 'WebGPU is not available in this browser',
807
+ webgpu_no_adapter: 'No compatible WebGPU adapter found',
808
+ missing_shader_f16: 'GPU does not support the shader-f16 feature',
809
+ insufficient_memory: 'Insufficient GPU memory to load the model',
810
+ model_download_failed: 'Failed to download model assets',
811
+ module_load_failed: 'Failed to load the web-llm runtime module',
812
+ init_failed: 'Model engine failed to initialise',
813
+ };
814
+
815
+ const handleLoadFailure = (
816
+ reason: LocalSlowLaneLoadErrorReason,
817
+ capabilities: WebGpuCapabilities,
818
+ // Raw engine error — local debug logging only, never sent to analytics.
819
+ debugDetail?: string,
820
+ ): void => {
821
+ ready = false;
822
+ const message = LOAD_FAILURE_MESSAGE[reason];
823
+ if (isAutocompleteDebugEnabled()) {
824
+ // eslint-disable-next-line no-console
825
+ console.log(
826
+ `[LocalSlowLane] Engine initialisation failed (${reason}): ${debugDetail ?? message}`,
827
+ );
828
+ }
829
+ onStatus?.(`Engine initialisation failed: ${message}`);
830
+ onLoadError?.({
831
+ reason,
832
+ message,
833
+ modelId,
834
+ embeddingModelId: LOCAL_MLC_EMBEDDING_MODEL_ID,
835
+ capabilities,
836
+ });
837
+ engineInitPromise = null;
838
+ initFailed = true;
839
+ };
840
+
644
841
  const initEngine = async (): Promise<void> => {
842
+ const capabilities = await probeWebGpuCapabilities();
843
+
844
+ // ── Pre-flight: machine limitations short-circuit before the expensive load ──
845
+ if (!capabilities.available) {
846
+ handleLoadFailure('webgpu_unavailable', capabilities);
847
+ return;
848
+ }
849
+ if (capabilities.adapterAvailable === false) {
850
+ handleLoadFailure('webgpu_no_adapter', capabilities);
851
+ return;
852
+ }
853
+ if (capabilities.shaderF16Supported === false) {
854
+ handleLoadFailure('missing_shader_f16', capabilities);
855
+ return;
856
+ }
857
+
858
+ const startTime = performance.now();
859
+
645
860
  try {
646
861
  if (isAutocompleteDebugEnabled()) {
647
862
  // eslint-disable-next-line no-console
@@ -653,10 +868,6 @@ export const createLocalSlowLaneClient = (
653
868
  }
654
869
  onStatus?.(`Initialising models: ${modelId} + ${LOCAL_MLC_EMBEDDING_MODEL_ID}…`);
655
870
 
656
- if (!('gpu' in navigator)) {
657
- throw new Error('WebGPU not supported');
658
- }
659
-
660
871
  // Fetch the web-llm runtime and the BE-parity lookup tables in parallel;
661
872
  // both are dynamically imported so they stay out of the main editor chunk.
662
873
  const [{ MLCEngine: MLCEngineCtor, prebuiltAppConfig }] = await Promise.all([
@@ -714,6 +925,7 @@ export const createLocalSlowLaneClient = (
714
925
 
715
926
  engine = newEngine;
716
927
  ready = true;
928
+ const loadDurationMs = Math.round(performance.now() - startTime);
717
929
 
718
930
  if (isAutocompleteDebugEnabled()) {
719
931
  // eslint-disable-next-line no-console
@@ -740,16 +952,15 @@ export const createLocalSlowLaneClient = (
740
952
  );
741
953
  }
742
954
  onStatus?.('Model loaded and ready.');
955
+ onLoadSuccess?.({
956
+ modelId,
957
+ embeddingModelId: LOCAL_MLC_EMBEDDING_MODEL_ID,
958
+ loadDurationMs,
959
+ capabilities,
960
+ });
743
961
  } catch (err) {
744
962
  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;
963
+ handleLoadFailure(classifyEngineError(errorMsg), capabilities, errorMsg);
753
964
  }
754
965
  };
755
966