@tryhamster/gerbil 1.0.0-rc.23 → 1.0.0-rc.24

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.
Files changed (55) hide show
  1. package/dist/browser/index.d.ts +146 -2
  2. package/dist/browser/index.d.ts.map +1 -1
  3. package/dist/browser/index.js +472 -21
  4. package/dist/browser/index.js.map +1 -1
  5. package/dist/cli.mjs +7 -7
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/frameworks/express.d.mts +1 -3
  8. package/dist/frameworks/express.d.mts.map +1 -1
  9. package/dist/frameworks/express.mjs +3 -3
  10. package/dist/frameworks/express.mjs.map +1 -1
  11. package/dist/frameworks/fastify.d.mts +1 -1
  12. package/dist/frameworks/fastify.mjs +1 -1
  13. package/dist/frameworks/hono.d.mts +1 -1
  14. package/dist/frameworks/hono.mjs +1 -1
  15. package/dist/frameworks/next.d.mts +2 -2
  16. package/dist/frameworks/next.mjs +1 -1
  17. package/dist/frameworks/react.d.mts +1 -1
  18. package/dist/frameworks/react.d.mts.map +1 -1
  19. package/dist/frameworks/trpc.d.mts +1 -1
  20. package/dist/frameworks/trpc.mjs +1 -1
  21. package/dist/{gerbil-DJygY0sJ.d.mts → gerbil-CbnV_cG5.d.mts} +9 -2
  22. package/dist/gerbil-CbnV_cG5.d.mts.map +1 -0
  23. package/dist/{gerbil-PzPtcdeM.mjs → gerbil-DODVGr-u.mjs} +1 -1
  24. package/dist/{gerbil-DzZ-L6n8.mjs → gerbil-jO9anIh_.mjs} +90 -3
  25. package/dist/gerbil-jO9anIh_.mjs.map +1 -0
  26. package/dist/index.d.mts +3 -3
  27. package/dist/index.d.mts.map +1 -1
  28. package/dist/index.mjs +2 -2
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/integrations/ai-sdk.d.mts +1 -1
  31. package/dist/integrations/ai-sdk.mjs +1 -1
  32. package/dist/integrations/langchain.d.mts +1 -1
  33. package/dist/integrations/langchain.mjs +1 -1
  34. package/dist/integrations/llamaindex.d.mts +1 -1
  35. package/dist/integrations/llamaindex.mjs +1 -1
  36. package/dist/integrations/mcp.d.mts +2 -2
  37. package/dist/integrations/mcp.mjs +4 -4
  38. package/dist/{mcp-D161vL_C.mjs → mcp-tavZtFY1.mjs} +3 -3
  39. package/dist/{mcp-D161vL_C.mjs.map → mcp-tavZtFY1.mjs.map} +1 -1
  40. package/dist/{one-liner-C-pRqDK2.mjs → one-liner-Ba58M_6j.mjs} +2 -2
  41. package/dist/{one-liner-C-pRqDK2.mjs.map → one-liner-Ba58M_6j.mjs.map} +1 -1
  42. package/dist/{repl-D9x3TnQc.mjs → repl-BGly-o_e.mjs} +3 -3
  43. package/dist/skills/index.d.mts +3 -3
  44. package/dist/skills/index.d.mts.map +1 -1
  45. package/dist/skills/index.mjs +3 -3
  46. package/dist/{skills-D14RwyUN.mjs → skills-BKxP2pex.mjs} +2 -2
  47. package/dist/{skills-D14RwyUN.mjs.map → skills-BKxP2pex.mjs.map} +1 -1
  48. package/dist/{types-evP8RShr.d.mts → types-6uG8lC7u.d.mts} +65 -2
  49. package/dist/types-6uG8lC7u.d.mts.map +1 -0
  50. package/docs/architecture/overview.md +2 -0
  51. package/docs/observability.md +230 -0
  52. package/package.json +5 -4
  53. package/dist/gerbil-DJygY0sJ.d.mts.map +0 -1
  54. package/dist/gerbil-DzZ-L6n8.mjs.map +0 -1
  55. package/dist/types-evP8RShr.d.mts.map +0 -1
@@ -766,6 +766,13 @@ function useChat(options = {}) {
766
766
  setIsLoading(true);
767
767
  setShouldLoad(true);
768
768
  }, [isLoading]);
769
+ useEffect(() => {
770
+ const crash = detectMemoryCrash();
771
+ if (crash.crashed) {
772
+ setError(crash.recommendation || "Previous model load failed due to device memory limits.");
773
+ onError?.(crash.recommendation || "Previous model load failed");
774
+ }
775
+ }, []);
769
776
  useEffect(() => {
770
777
  if (!shouldLoad) return;
771
778
  if (!isWebGPUSupported()) {
@@ -775,13 +782,27 @@ function useChat(options = {}) {
775
782
  onError?.(gpuError);
776
783
  return;
777
784
  }
785
+ const safetyCheck = isModelSafeForDevice(model);
786
+ if (!safetyCheck.safe) {
787
+ setError(safetyCheck.reason);
788
+ setIsLoading(false);
789
+ onError?.(safetyCheck.reason);
790
+ return;
791
+ }
778
792
  mountedRef.current = true;
793
+ setDownloadPhase("downloading", model);
779
794
  createGerbilWorker({
780
795
  modelId: model,
781
796
  onProgress: (p) => {
782
797
  if (!mountedRef.current) return;
783
798
  setLoadingProgress(p);
799
+ if (p.status === "downloading" && p.progress !== void 0) setDownloadPhase("downloading", model, {
800
+ bytesDownloaded: p.progress,
801
+ totalBytes: 100
802
+ });
803
+ else if (p.status === "loading") setDownloadPhase("initializing", model);
784
804
  if (p.status === "ready") {
805
+ clearDownloadPhase();
785
806
  setIsLoading(false);
786
807
  setIsReady(true);
787
808
  onReady?.();
@@ -799,6 +820,7 @@ function useChat(options = {}) {
799
820
  },
800
821
  onError: (err) => {
801
822
  if (!mountedRef.current) return;
823
+ setDownloadPhase("error", model);
802
824
  setError(err);
803
825
  setIsGenerating(false);
804
826
  onError?.(err);
@@ -808,6 +830,7 @@ function useChat(options = {}) {
808
830
  else worker.terminate();
809
831
  }).catch((err) => {
810
832
  if (mountedRef.current) {
833
+ setDownloadPhase("error", model);
811
834
  setError(err.message);
812
835
  setIsLoading(false);
813
836
  onError?.(err.message);
@@ -1035,6 +1058,13 @@ function useCompletion(options = {}) {
1035
1058
  setIsLoading(true);
1036
1059
  setShouldLoad(true);
1037
1060
  }, [isLoading]);
1061
+ useEffect(() => {
1062
+ const crash = detectMemoryCrash();
1063
+ if (crash.crashed) {
1064
+ setError(crash.recommendation || "Previous model load failed due to device memory limits.");
1065
+ onError?.(crash.recommendation || "Previous model load failed");
1066
+ }
1067
+ }, []);
1038
1068
  useEffect(() => {
1039
1069
  if (!shouldLoad) return;
1040
1070
  if (!isWebGPUSupported()) {
@@ -1044,13 +1074,27 @@ function useCompletion(options = {}) {
1044
1074
  onError?.(gpuError);
1045
1075
  return;
1046
1076
  }
1077
+ const safetyCheck = isModelSafeForDevice(model);
1078
+ if (!safetyCheck.safe) {
1079
+ setError(safetyCheck.reason);
1080
+ setIsLoading(false);
1081
+ onError?.(safetyCheck.reason);
1082
+ return;
1083
+ }
1047
1084
  mountedRef.current = true;
1085
+ setDownloadPhase("downloading", model);
1048
1086
  createGerbilWorker({
1049
1087
  modelId: model,
1050
1088
  onProgress: (p) => {
1051
1089
  if (!mountedRef.current) return;
1052
1090
  setLoadingProgress(p);
1091
+ if (p.status === "downloading" && p.progress !== void 0) setDownloadPhase("downloading", model, {
1092
+ bytesDownloaded: p.progress,
1093
+ totalBytes: 100
1094
+ });
1095
+ else if (p.status === "loading") setDownloadPhase("initializing", model);
1053
1096
  if (p.status === "ready") {
1097
+ clearDownloadPhase();
1054
1098
  setIsLoading(false);
1055
1099
  setIsReady(true);
1056
1100
  onReady?.();
@@ -1070,6 +1114,7 @@ function useCompletion(options = {}) {
1070
1114
  },
1071
1115
  onError: (err) => {
1072
1116
  if (!mountedRef.current) return;
1117
+ setDownloadPhase("error", model);
1073
1118
  setError(err);
1074
1119
  setIsGenerating(false);
1075
1120
  onError?.(err);
@@ -1079,6 +1124,7 @@ function useCompletion(options = {}) {
1079
1124
  else worker.terminate();
1080
1125
  }).catch((err) => {
1081
1126
  if (mountedRef.current) {
1127
+ setDownloadPhase("error", model);
1082
1128
  setError(err.message);
1083
1129
  setIsLoading(false);
1084
1130
  onError?.(err.message);
@@ -1445,12 +1491,12 @@ const TTS_WORKER_CODE = `
1445
1491
  // Load Supertonic using transformers.js pipeline with WASM fallback
1446
1492
  let device = "webgpu";
1447
1493
  try {
1448
- ttsInstance = await pipeline("text-to-speech", repo, {
1494
+ ttsInstance = await pipeline("text-to-speech", repo, {
1449
1495
  device,
1450
- progress_callback: (progress) => {
1451
- self.postMessage({ type: "progress", payload: progress });
1452
- },
1453
- });
1496
+ progress_callback: (progress) => {
1497
+ self.postMessage({ type: "progress", payload: progress });
1498
+ },
1499
+ });
1454
1500
  } catch (webgpuError) {
1455
1501
  console.warn("WebGPU failed for TTS, falling back to WASM:", webgpuError.message);
1456
1502
  self.postMessage({ type: "fallback", payload: { backend: "wasm", reason: webgpuError.message } });
@@ -1493,8 +1539,8 @@ const TTS_WORKER_CODE = `
1493
1539
 
1494
1540
  // Try WebGPU first, fallback to WASM
1495
1541
  try {
1496
- kokoroTTS = await KokoroTTS.from_pretrained(repo, {
1497
- dtype: "fp32",
1542
+ kokoroTTS = await KokoroTTS.from_pretrained(repo, {
1543
+ dtype: "fp32",
1498
1544
  device: "webgpu",
1499
1545
  progress_callback: (progress) => {
1500
1546
  self.postMessage({ type: "progress", payload: progress });
@@ -1506,10 +1552,10 @@ const TTS_WORKER_CODE = `
1506
1552
  kokoroTTS = await KokoroTTS.from_pretrained(repo, {
1507
1553
  dtype: "fp32",
1508
1554
  device: "wasm",
1509
- progress_callback: (progress) => {
1510
- self.postMessage({ type: "progress", payload: progress });
1511
- },
1512
- });
1555
+ progress_callback: (progress) => {
1556
+ self.postMessage({ type: "progress", payload: progress });
1557
+ },
1558
+ });
1513
1559
  }
1514
1560
  }
1515
1561
 
@@ -1629,10 +1675,18 @@ function useSpeech(options = {}) {
1629
1675
  setIsLoading(true);
1630
1676
  setShouldLoad(true);
1631
1677
  }, [isLoading]);
1678
+ useEffect(() => {
1679
+ const crash = detectMemoryCrash();
1680
+ if (crash.crashed) {
1681
+ setError(crash.recommendation || "Previous model load failed due to device memory limits.");
1682
+ onError?.(crash.recommendation || "Previous model load failed");
1683
+ }
1684
+ }, []);
1632
1685
  useEffect(() => {
1633
1686
  if (!shouldLoad) return;
1634
1687
  mountedRef.current = true;
1635
1688
  modelIdRef.current = modelId;
1689
+ setDownloadPhase("downloading", modelId);
1636
1690
  const config = TTS_MODELS[modelId];
1637
1691
  setLoadingProgress({
1638
1692
  status: "loading",
@@ -1649,6 +1703,7 @@ function useSpeech(options = {}) {
1649
1703
  progress: Math.round(payload.progress || 0)
1650
1704
  });
1651
1705
  if (type === "ready") {
1706
+ clearDownloadPhase();
1652
1707
  setIsLoading(false);
1653
1708
  setIsReady(true);
1654
1709
  setLoadingProgress({ status: "ready" });
@@ -1671,6 +1726,7 @@ function useSpeech(options = {}) {
1671
1726
  playAudioData(audio, sampleRate);
1672
1727
  }
1673
1728
  if (type === "error") {
1729
+ setDownloadPhase("error", modelId);
1674
1730
  const errorMsg = payload;
1675
1731
  setError(errorMsg);
1676
1732
  setIsLoading(false);
@@ -1684,6 +1740,7 @@ function useSpeech(options = {}) {
1684
1740
  };
1685
1741
  worker.onerror = (err) => {
1686
1742
  if (!mountedRef.current) return;
1743
+ setDownloadPhase("error", modelId);
1687
1744
  let errorMsg = err.message || "";
1688
1745
  if (!errorMsg || errorMsg === "Script error.") errorMsg = getWebGPUErrorMessage();
1689
1746
  setError(errorMsg);
@@ -1941,12 +1998,12 @@ const STT_WORKER_CODE = `
1941
1998
  // Load Whisper model with WASM fallback
1942
1999
  let device = "webgpu";
1943
2000
  try {
1944
- sttPipeline = await pipeline("automatic-speech-recognition", model, {
2001
+ sttPipeline = await pipeline("automatic-speech-recognition", model, {
1945
2002
  device,
1946
- progress_callback: (progress) => {
1947
- self.postMessage({ type: "progress", payload: progress });
1948
- },
1949
- });
2003
+ progress_callback: (progress) => {
2004
+ self.postMessage({ type: "progress", payload: progress });
2005
+ },
2006
+ });
1950
2007
  } catch (webgpuError) {
1951
2008
  console.warn("WebGPU failed for STT, falling back to WASM:", webgpuError.message);
1952
2009
  self.postMessage({ type: "fallback", payload: { backend: "wasm", reason: webgpuError.message } });
@@ -2065,9 +2122,17 @@ function useVoiceInput(options = {}) {
2065
2122
  "whisper-small.en": "onnx-community/whisper-small.en"
2066
2123
  }[modelId] || modelId;
2067
2124
  };
2125
+ useEffect(() => {
2126
+ const crash = detectMemoryCrash();
2127
+ if (crash.crashed) {
2128
+ setError(crash.recommendation || "Previous model load failed due to device memory limits.");
2129
+ onError?.(crash.recommendation || "Previous model load failed");
2130
+ }
2131
+ }, []);
2068
2132
  useEffect(() => {
2069
2133
  if (!shouldLoad || isReady) return;
2070
2134
  mountedRef.current = true;
2135
+ setDownloadPhase("downloading", model);
2071
2136
  setIsLoading(true);
2072
2137
  setLoadingProgress({
2073
2138
  status: "loading",
@@ -2093,6 +2158,7 @@ function useVoiceInput(options = {}) {
2093
2158
  onProgress?.(progress);
2094
2159
  }
2095
2160
  if (type === "ready") {
2161
+ clearDownloadPhase();
2096
2162
  setIsReady(true);
2097
2163
  setIsLoading(false);
2098
2164
  setLoadingProgress({ status: "ready" });
@@ -2109,6 +2175,7 @@ function useVoiceInput(options = {}) {
2109
2175
  }
2110
2176
  }
2111
2177
  if (type === "error") {
2178
+ setDownloadPhase("error", model);
2112
2179
  const errMsg = payload;
2113
2180
  setError(errMsg);
2114
2181
  setIsLoading(false);
@@ -2131,6 +2198,7 @@ function useVoiceInput(options = {}) {
2131
2198
  };
2132
2199
  worker.onerror = (err) => {
2133
2200
  if (!mountedRef.current) return;
2201
+ setDownloadPhase("error", model);
2134
2202
  let errMsg = err.message || "";
2135
2203
  if (!errMsg || errMsg === "Script error.") errMsg = getWebGPUErrorMessage();
2136
2204
  setError(errMsg);
@@ -3052,6 +3120,13 @@ function useEmbedding(options = {}) {
3052
3120
  if (magnitude === 0) return 0;
3053
3121
  return dotProduct / magnitude;
3054
3122
  }, []);
3123
+ useEffect(() => {
3124
+ const crash = detectMemoryCrash();
3125
+ if (crash.crashed) {
3126
+ setError(crash.recommendation || "Previous model load failed due to device memory limits.");
3127
+ onError?.(crash.recommendation || "Previous model load failed");
3128
+ }
3129
+ }, []);
3055
3130
  const load = useCallback(() => {
3056
3131
  if (isReady && workerRef.current) return Promise.resolve();
3057
3132
  if (loadRequestedRef.current && readyPromiseRef.current) return readyPromiseRef.current;
@@ -3061,6 +3136,7 @@ function useEmbedding(options = {}) {
3061
3136
  status: "loading",
3062
3137
  message: "Loading embedding model..."
3063
3138
  });
3139
+ setDownloadPhase("downloading", model);
3064
3140
  readyPromiseRef.current = new Promise((resolve) => {
3065
3141
  readyResolveRef.current = resolve;
3066
3142
  });
@@ -3075,18 +3151,21 @@ function useEmbedding(options = {}) {
3075
3151
  progress: Math.round(payload.loaded / payload.total * 100)
3076
3152
  });
3077
3153
  } else if (type === "ready") {
3154
+ clearDownloadPhase();
3078
3155
  setIsLoading(false);
3079
3156
  setIsReady(true);
3080
3157
  setLoadingProgress({ status: "ready" });
3081
3158
  readyResolveRef.current?.();
3082
3159
  onReady?.();
3083
3160
  } else if (type === "error") {
3161
+ setDownloadPhase("error", model);
3084
3162
  setIsLoading(false);
3085
3163
  setError(payload);
3086
3164
  onError?.(payload);
3087
3165
  }
3088
3166
  });
3089
3167
  worker.onerror = (err) => {
3168
+ setDownloadPhase("error", model);
3090
3169
  setIsLoading(false);
3091
3170
  let errMsg = err.message || "";
3092
3171
  if (!errMsg || errMsg === "Script error.") errMsg = getWebGPUErrorMessage();
@@ -3591,11 +3670,11 @@ async function getBrowserDiagnostics() {
3591
3670
  } catch {
3592
3671
  moduleWorkers = false;
3593
3672
  }
3594
- let indexedDB = false;
3673
+ let indexedDB$1 = false;
3595
3674
  try {
3596
- indexedDB = typeof window !== "undefined" && "indexedDB" in window;
3675
+ indexedDB$1 = typeof window !== "undefined" && "indexedDB" in window;
3597
3676
  } catch {
3598
- indexedDB = false;
3677
+ indexedDB$1 = false;
3599
3678
  }
3600
3679
  return {
3601
3680
  browser,
@@ -3608,7 +3687,7 @@ async function getBrowserDiagnostics() {
3608
3687
  webgpuExpected,
3609
3688
  webgpu,
3610
3689
  moduleWorkers,
3611
- indexedDB
3690
+ indexedDB: indexedDB$1
3612
3691
  };
3613
3692
  }
3614
3693
  /**
@@ -3646,6 +3725,370 @@ function getRecommendedModels() {
3646
3725
  };
3647
3726
  }
3648
3727
  /**
3728
+ * Maximum safe model sizes for iOS devices (in MB).
3729
+ * Based on WKWebView effective memory limit of ~200-400MB.
3730
+ */
3731
+ const IOS_MODEL_LIMITS = {
3732
+ safe: ["smollm2-135m", "smollm2-360m"],
3733
+ risky: ["qwen3-0.6b"],
3734
+ blocked: ["qwen3-1.7b", "qwen3-4b"],
3735
+ maxBudgetMB: 350
3736
+ };
3737
+ /**
3738
+ * Check if a model is safe to load on the current device.
3739
+ * Returns guidance specific to iOS memory constraints.
3740
+ */
3741
+ function isModelSafeForDevice(modelId) {
3742
+ const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
3743
+ const isIOS = /iPhone|iPad|iPod/.test(ua);
3744
+ const isIOSChrome = isIOS && /CriOS/.test(ua);
3745
+ const deviceMemory = typeof navigator !== "undefined" ? navigator.deviceMemory : null;
3746
+ const normalizedId = modelId.toLowerCase().replace(/[^a-z0-9]/g, "-");
3747
+ if (isIOS) {
3748
+ if (IOS_MODEL_LIMITS.blocked.some((m) => normalizedId.includes(m.toLowerCase().replace(/[^a-z0-9]/g, "-")))) return {
3749
+ safe: false,
3750
+ reason: `Model ${modelId} is too large for iOS devices${isIOSChrome ? " (iOS Chrome uses WKWebView, same limits as Safari)" : ""}. WKWebView memory limit (~300-400MB) will cause crashes.`,
3751
+ recommendation: "Use smollm2-360m or qwen3-0.6b on iOS. For larger models, use desktop.",
3752
+ maxSafeModel: "qwen3-0.6b"
3753
+ };
3754
+ if (IOS_MODEL_LIMITS.risky.some((m) => normalizedId.includes(m.toLowerCase().replace(/[^a-z0-9]/g, "-")))) {
3755
+ if (!(deviceMemory && deviceMemory >= 4)) return {
3756
+ safe: false,
3757
+ reason: `Model ${modelId} may crash on older iOS devices. Your device reports ${deviceMemory || "unknown"}GB RAM.`,
3758
+ recommendation: "Use smollm2-360m for reliable performance, or try on iPhone 14+ / iPad Pro.",
3759
+ maxSafeModel: "smollm2-360m"
3760
+ };
3761
+ return {
3762
+ safe: true,
3763
+ reason: `Model ${modelId} should work on your high-memory iOS device, but may be slow.`
3764
+ };
3765
+ }
3766
+ return {
3767
+ safe: true,
3768
+ reason: "Model is within iOS memory limits."
3769
+ };
3770
+ }
3771
+ if (/Android/.test(ua)) {
3772
+ if (normalizedId.includes("qwen3-4b") || normalizedId.includes("7b")) return {
3773
+ safe: false,
3774
+ reason: `Model ${modelId} is very large and may crash on Android devices.`,
3775
+ recommendation: "Use qwen3-1.7b or smaller on Android.",
3776
+ maxSafeModel: "qwen3-1.7b"
3777
+ };
3778
+ }
3779
+ return {
3780
+ safe: true,
3781
+ reason: "Desktop browser has sufficient memory."
3782
+ };
3783
+ }
3784
+ const SESSION_STORAGE_KEY = "gerbil_session_phase";
3785
+ /**
3786
+ * Generate a unique session ID for tracking across reloads.
3787
+ */
3788
+ function generateSessionId() {
3789
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
3790
+ }
3791
+ /**
3792
+ * Get or create the current session ID.
3793
+ */
3794
+ function getSessionId() {
3795
+ if (typeof localStorage === "undefined") return generateSessionId();
3796
+ let sessionId = sessionStorage.getItem("gerbil_session_id");
3797
+ if (!sessionId) {
3798
+ sessionId = generateSessionId();
3799
+ sessionStorage.setItem("gerbil_session_id", sessionId);
3800
+ }
3801
+ return sessionId;
3802
+ }
3803
+ /**
3804
+ * Set the current download/initialization phase.
3805
+ * Used to detect if a reload happened during a critical operation.
3806
+ */
3807
+ function setDownloadPhase(phase, modelId, progress) {
3808
+ if (typeof localStorage === "undefined") return;
3809
+ const state = {
3810
+ phase,
3811
+ modelId: modelId || null,
3812
+ sessionId: getSessionId(),
3813
+ timestamp: Date.now(),
3814
+ bytesDownloaded: progress?.bytesDownloaded,
3815
+ totalBytes: progress?.totalBytes
3816
+ };
3817
+ localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state));
3818
+ }
3819
+ /**
3820
+ * Get the last known download phase from storage.
3821
+ */
3822
+ function getDownloadPhase() {
3823
+ if (typeof localStorage === "undefined") return null;
3824
+ try {
3825
+ const raw = localStorage.getItem(SESSION_STORAGE_KEY);
3826
+ if (!raw) return null;
3827
+ return JSON.parse(raw);
3828
+ } catch {
3829
+ return null;
3830
+ }
3831
+ }
3832
+ /**
3833
+ * Detect if the page reloaded during a model download/initialization.
3834
+ * This typically indicates an iOS memory crash.
3835
+ *
3836
+ * @returns Detection result with recommended action
3837
+ */
3838
+ function detectMemoryCrash() {
3839
+ const lastState = getDownloadPhase();
3840
+ const currentSessionId = getSessionId();
3841
+ if (!lastState) return { crashed: false };
3842
+ const wasInCriticalPhase = [
3843
+ "downloading",
3844
+ "caching",
3845
+ "initializing"
3846
+ ].includes(lastState.phase);
3847
+ const sessionChanged = lastState.sessionId !== currentSessionId;
3848
+ const timeSinceCrash = Date.now() - lastState.timestamp;
3849
+ if (wasInCriticalPhase && sessionChanged && timeSinceCrash < 300 * 1e3) {
3850
+ localStorage.removeItem(SESSION_STORAGE_KEY);
3851
+ return {
3852
+ crashed: true,
3853
+ phase: lastState.phase,
3854
+ modelId: lastState.modelId || void 0,
3855
+ timeSinceCrash,
3856
+ recommendation: lastState.modelId?.includes("1.7b") ? "The model was too large for your device. Try smollm2-360m or qwen3-0.6b instead." : "Your device ran out of memory. Try a smaller model or use a desktop browser."
3857
+ };
3858
+ }
3859
+ return { crashed: false };
3860
+ }
3861
+ /**
3862
+ * Clear session phase (call when model loads successfully).
3863
+ */
3864
+ function clearDownloadPhase() {
3865
+ if (typeof localStorage === "undefined") return;
3866
+ localStorage.removeItem(SESSION_STORAGE_KEY);
3867
+ }
3868
+ /** Chunk size for downloads: 1.5MB (safe for iOS IndexedDB transactions) */
3869
+ const CHUNK_SIZE_BYTES = 1.5 * 1024 * 1024;
3870
+ /** IndexedDB database name for chunked downloads */
3871
+ const DOWNLOAD_DB_NAME = "gerbil-model-chunks";
3872
+ const DOWNLOAD_DB_VERSION = 1;
3873
+ /**
3874
+ * Open (or create) the IndexedDB for chunked downloads.
3875
+ */
3876
+ async function openDownloadDB() {
3877
+ return new Promise((resolve, reject) => {
3878
+ const request = indexedDB.open(DOWNLOAD_DB_NAME, DOWNLOAD_DB_VERSION);
3879
+ request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to open download DB: ${request.error?.message}`));
3880
+ request.onsuccess = () => resolve(request.result);
3881
+ request.onupgradeneeded = (event) => {
3882
+ const db = event.target.result;
3883
+ if (!db.objectStoreNames.contains("manifests")) db.createObjectStore("manifests", { keyPath: "modelId" });
3884
+ if (!db.objectStoreNames.contains("chunks")) db.createObjectStore("chunks");
3885
+ };
3886
+ });
3887
+ }
3888
+ /**
3889
+ * Get download manifest for a model.
3890
+ */
3891
+ async function getManifest(db, modelId) {
3892
+ return new Promise((resolve, reject) => {
3893
+ const request = db.transaction("manifests", "readonly").objectStore("manifests").get(modelId);
3894
+ request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to get manifest: ${request.error?.message}`));
3895
+ request.onsuccess = () => resolve(request.result || null);
3896
+ });
3897
+ }
3898
+ /**
3899
+ * Save download manifest.
3900
+ */
3901
+ async function saveManifest(db, manifest) {
3902
+ return new Promise((resolve, reject) => {
3903
+ const request = db.transaction("manifests", "readwrite").objectStore("manifests").put(manifest);
3904
+ request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to save manifest: ${request.error?.message}`));
3905
+ request.onsuccess = () => resolve();
3906
+ });
3907
+ }
3908
+ /**
3909
+ * Save a single chunk.
3910
+ */
3911
+ async function saveChunk(db, modelId, chunkIndex, data) {
3912
+ return new Promise((resolve, reject) => {
3913
+ const store = db.transaction("chunks", "readwrite").objectStore("chunks");
3914
+ const key = `${modelId}-${chunkIndex}`;
3915
+ const request = store.put(data, key);
3916
+ request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to save chunk ${chunkIndex}: ${request.error?.message}`));
3917
+ request.onsuccess = () => resolve();
3918
+ });
3919
+ }
3920
+ /**
3921
+ * Get a single chunk.
3922
+ */
3923
+ async function getChunk(db, modelId, chunkIndex) {
3924
+ return new Promise((resolve, reject) => {
3925
+ const store = db.transaction("chunks", "readonly").objectStore("chunks");
3926
+ const key = `${modelId}-${chunkIndex}`;
3927
+ const request = store.get(key);
3928
+ request.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to get chunk ${chunkIndex}: ${request.error?.message}`));
3929
+ request.onsuccess = () => resolve(request.result || null);
3930
+ });
3931
+ }
3932
+ /**
3933
+ * Delete all chunks and manifest for a model.
3934
+ */
3935
+ async function clearModelData(db, modelId) {
3936
+ const manifest = await getManifest(db, modelId);
3937
+ return new Promise((resolve, reject) => {
3938
+ const tx = db.transaction(["manifests", "chunks"], "readwrite");
3939
+ tx.objectStore("manifests").delete(modelId);
3940
+ if (manifest) {
3941
+ const totalChunks = Math.ceil(manifest.totalBytes / manifest.chunkSize);
3942
+ const chunkStore = tx.objectStore("chunks");
3943
+ for (let i = 0; i < totalChunks; i++) chunkStore.delete(`${modelId}-${i}`);
3944
+ }
3945
+ tx.oncomplete = () => resolve();
3946
+ tx.onerror = () => reject(/* @__PURE__ */ new Error(`Failed to clear model data: ${tx.error?.message}`));
3947
+ });
3948
+ }
3949
+ /**
3950
+ * Chunked resumable downloader for large model files.
3951
+ * Downloads in 1.5MB chunks to avoid iOS memory pressure.
3952
+ */
3953
+ async function downloadModelChunked(url, modelId, options = {}) {
3954
+ const { onProgress, signal } = options;
3955
+ setDownloadPhase("downloading", modelId);
3956
+ const db = await openDownloadDB();
3957
+ try {
3958
+ let manifest = await getManifest(db, modelId);
3959
+ const headResponse = await fetch(url, {
3960
+ method: "HEAD",
3961
+ signal
3962
+ });
3963
+ if (!headResponse.ok) throw new Error(`HEAD request failed: ${headResponse.status} ${headResponse.statusText}`);
3964
+ const contentLength = parseInt(headResponse.headers.get("content-length") || "0", 10);
3965
+ const etag = headResponse.headers.get("etag");
3966
+ const acceptRanges = headResponse.headers.get("accept-ranges");
3967
+ if (!contentLength) throw new Error("Server did not provide content-length");
3968
+ if (manifest && manifest.etag !== etag) {
3969
+ console.warn(`Model ${modelId} has been updated (etag mismatch). Clearing cached chunks.`);
3970
+ await clearModelData(db, modelId);
3971
+ manifest = null;
3972
+ }
3973
+ if (!(acceptRanges === "bytes")) {
3974
+ console.warn(`Server doesn't support range requests for ${modelId}. Using regular download.`);
3975
+ db.close();
3976
+ const response = await fetch(url, { signal });
3977
+ if (!response.ok) throw new Error(`Download failed: ${response.status}`);
3978
+ setDownloadPhase("caching", modelId);
3979
+ const buffer = await response.arrayBuffer();
3980
+ setDownloadPhase("ready", modelId);
3981
+ return buffer;
3982
+ }
3983
+ const totalChunks = Math.ceil(contentLength / CHUNK_SIZE_BYTES);
3984
+ if (!manifest) {
3985
+ manifest = {
3986
+ modelId,
3987
+ url,
3988
+ etag,
3989
+ totalBytes: contentLength,
3990
+ chunkSize: CHUNK_SIZE_BYTES,
3991
+ completedChunks: [],
3992
+ createdAt: Date.now(),
3993
+ updatedAt: Date.now()
3994
+ };
3995
+ await saveManifest(db, manifest);
3996
+ }
3997
+ for (let i = 0; i < totalChunks; i++) {
3998
+ if (signal?.aborted) throw new Error("Download aborted");
3999
+ if (manifest.completedChunks.includes(i)) {
4000
+ const bytesDownloaded$1 = manifest.completedChunks.length / totalChunks * contentLength;
4001
+ onProgress?.({
4002
+ phase: "resuming",
4003
+ bytesDownloaded: bytesDownloaded$1,
4004
+ totalBytes: contentLength,
4005
+ percent: Math.round(bytesDownloaded$1 / contentLength * 100)
4006
+ });
4007
+ continue;
4008
+ }
4009
+ const start = i * CHUNK_SIZE_BYTES;
4010
+ const end = Math.min(start + CHUNK_SIZE_BYTES - 1, contentLength - 1);
4011
+ const response = await fetch(url, {
4012
+ headers: { Range: `bytes=${start}-${end}` },
4013
+ signal
4014
+ });
4015
+ if (response.status !== 206) throw new Error(`Range request failed: ${response.status} (expected 206)`);
4016
+ const chunkData = await response.arrayBuffer();
4017
+ await saveChunk(db, modelId, i, chunkData);
4018
+ manifest.completedChunks.push(i);
4019
+ manifest.updatedAt = Date.now();
4020
+ await saveManifest(db, manifest);
4021
+ const bytesDownloaded = manifest.completedChunks.length * CHUNK_SIZE_BYTES;
4022
+ setDownloadPhase("downloading", modelId, {
4023
+ bytesDownloaded,
4024
+ totalBytes: contentLength
4025
+ });
4026
+ onProgress?.({
4027
+ phase: "downloading",
4028
+ bytesDownloaded: Math.min(bytesDownloaded, contentLength),
4029
+ totalBytes: contentLength,
4030
+ percent: Math.round(manifest.completedChunks.length / totalChunks * 100)
4031
+ });
4032
+ response.body = null;
4033
+ }
4034
+ setDownloadPhase("caching", modelId);
4035
+ onProgress?.({
4036
+ phase: "assembling",
4037
+ bytesDownloaded: contentLength,
4038
+ totalBytes: contentLength,
4039
+ percent: 100
4040
+ });
4041
+ const finalBuffer = new ArrayBuffer(contentLength);
4042
+ const finalView = new Uint8Array(finalBuffer);
4043
+ for (let i = 0; i < totalChunks; i++) {
4044
+ const chunk = await getChunk(db, modelId, i);
4045
+ if (!chunk) throw new Error(`Missing chunk ${i} during assembly`);
4046
+ const offset = i * CHUNK_SIZE_BYTES;
4047
+ finalView.set(new Uint8Array(chunk), offset);
4048
+ }
4049
+ await clearModelData(db, modelId);
4050
+ db.close();
4051
+ setDownloadPhase("ready", modelId);
4052
+ return finalBuffer;
4053
+ } catch (error) {
4054
+ setDownloadPhase("error", modelId);
4055
+ db.close();
4056
+ throw error;
4057
+ }
4058
+ }
4059
+ /**
4060
+ * Check if a model has an incomplete download.
4061
+ */
4062
+ async function hasIncompleteDownload(modelId) {
4063
+ try {
4064
+ const db = await openDownloadDB();
4065
+ const manifest = await getManifest(db, modelId);
4066
+ db.close();
4067
+ if (!manifest) return { incomplete: false };
4068
+ const totalChunks = Math.ceil(manifest.totalBytes / manifest.chunkSize);
4069
+ const completedChunks = manifest.completedChunks.length;
4070
+ if (completedChunks < totalChunks) return {
4071
+ incomplete: true,
4072
+ bytesDownloaded: completedChunks * manifest.chunkSize,
4073
+ totalBytes: manifest.totalBytes,
4074
+ percent: Math.round(completedChunks / totalChunks * 100)
4075
+ };
4076
+ return { incomplete: false };
4077
+ } catch {
4078
+ return { incomplete: false };
4079
+ }
4080
+ }
4081
+ /**
4082
+ * Clear incomplete download data for a model.
4083
+ */
4084
+ async function clearIncompleteDownload(modelId) {
4085
+ try {
4086
+ const db = await openDownloadDB();
4087
+ await clearModelData(db, modelId);
4088
+ db.close();
4089
+ } catch {}
4090
+ }
4091
+ /**
3649
4092
  * Check if there's enough storage quota for a model download.
3650
4093
  * Returns estimated available space and whether download should proceed.
3651
4094
  */
@@ -3693,6 +4136,14 @@ var browser_default = {
3693
4136
  getBrowserDiagnostics,
3694
4137
  getRecommendedModels,
3695
4138
  checkStorageQuota,
4139
+ isModelSafeForDevice,
4140
+ setDownloadPhase,
4141
+ getDownloadPhase,
4142
+ detectMemoryCrash,
4143
+ clearDownloadPhase,
4144
+ downloadModelChunked,
4145
+ hasIncompleteDownload,
4146
+ clearIncompleteDownload,
3696
4147
  createGerbilWorker,
3697
4148
  playAudio,
3698
4149
  createAudioPlayer,
@@ -3703,5 +4154,5 @@ var browser_default = {
3703
4154
  };
3704
4155
 
3705
4156
  //#endregion
3706
- export { BUILTIN_MODELS, checkStorageQuota, checkWebGPUCapabilities, checkWebGPUReady, createAudioPlayer, createGerbilWorker, browser_default as default, getBrowserDiagnostics, getRecommendedModels, getWebGPUInfo, isWebGPUSupported, playAudio, preloadChatModel, preloadEmbeddingModel, preloadSTTModel, preloadTTSModel, useChat, useCompletion, useEmbedding, useSpeech, useVoiceChat, useVoiceInput };
4157
+ export { BUILTIN_MODELS, checkStorageQuota, checkWebGPUCapabilities, checkWebGPUReady, clearDownloadPhase, clearIncompleteDownload, createAudioPlayer, createGerbilWorker, browser_default as default, detectMemoryCrash, downloadModelChunked, getBrowserDiagnostics, getDownloadPhase, getRecommendedModels, getWebGPUInfo, hasIncompleteDownload, isModelSafeForDevice, isWebGPUSupported, playAudio, preloadChatModel, preloadEmbeddingModel, preloadSTTModel, preloadTTSModel, setDownloadPhase, useChat, useCompletion, useEmbedding, useSpeech, useVoiceChat, useVoiceInput };
3707
4158
  //# sourceMappingURL=index.js.map