@glassly/engine 0.3.3-dev.670 → 0.3.4-dev.674

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 (115) hide show
  1. package/build/engine.d.ts +38 -14
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +3 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/ai.d.ts +32 -0
  6. package/build/facades/ai.d.ts.map +1 -0
  7. package/build/facades/ai.js +38 -0
  8. package/build/facades/ai.js.map +1 -0
  9. package/build/facades/glasses.d.ts +30 -24
  10. package/build/facades/glasses.d.ts.map +1 -1
  11. package/build/facades/glasses.js +49 -30
  12. package/build/facades/glasses.js.map +1 -1
  13. package/build/facades/glassesSettings.d.ts.map +1 -1
  14. package/build/facades/glassesSettings.js +1 -0
  15. package/build/facades/glassesSettings.js.map +1 -1
  16. package/build/facades/pairing.d.ts +6 -5
  17. package/build/facades/pairing.d.ts.map +1 -1
  18. package/build/facades/pairing.js +14 -14
  19. package/build/facades/pairing.js.map +1 -1
  20. package/build/generated/releaseMetadata.js +4 -4
  21. package/build/generated/releaseMetadata.js.map +1 -1
  22. package/build/index.d.ts +5 -2
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +2 -0
  25. package/build/index.js.map +1 -1
  26. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  27. package/build/services/DeviceEventRouter.js +9 -1
  28. package/build/services/DeviceEventRouter.js.map +1 -1
  29. package/build/services/GestureDotsService.d.ts +4 -3
  30. package/build/services/GestureDotsService.d.ts.map +1 -1
  31. package/build/services/GestureDotsService.js +17 -12
  32. package/build/services/GestureDotsService.js.map +1 -1
  33. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  34. package/build/services/GlassesTouchInput.js +11 -9
  35. package/build/services/GlassesTouchInput.js.map +1 -1
  36. package/build/services/LlmRouting.d.ts +35 -0
  37. package/build/services/LlmRouting.d.ts.map +1 -0
  38. package/build/services/LlmRouting.js +25 -0
  39. package/build/services/LlmRouting.js.map +1 -0
  40. package/build/services/LocalLlmModelManager.d.ts +79 -0
  41. package/build/services/LocalLlmModelManager.d.ts.map +1 -0
  42. package/build/services/LocalLlmModelManager.js +278 -0
  43. package/build/services/LocalLlmModelManager.js.map +1 -0
  44. package/build/services/LocalLlmPrompt.d.ts +90 -0
  45. package/build/services/LocalLlmPrompt.d.ts.map +1 -0
  46. package/build/services/LocalLlmPrompt.js +189 -0
  47. package/build/services/LocalLlmPrompt.js.map +1 -0
  48. package/build/services/LocalLlmProvider.d.ts +35 -0
  49. package/build/services/LocalLlmProvider.d.ts.map +1 -0
  50. package/build/services/LocalLlmProvider.js +217 -0
  51. package/build/services/LocalLlmProvider.js.map +1 -0
  52. package/build/services/LocalMiniappRuntime.d.ts +9 -1
  53. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  54. package/build/services/LocalMiniappRuntime.js +91 -17
  55. package/build/services/LocalMiniappRuntime.js.map +1 -1
  56. package/build/services/STTModelManager.d.ts +25 -2
  57. package/build/services/STTModelManager.d.ts.map +1 -1
  58. package/build/services/STTModelManager.js +137 -6
  59. package/build/services/STTModelManager.js.map +1 -1
  60. package/build/services/controllers.d.ts +21 -0
  61. package/build/services/controllers.d.ts.map +1 -0
  62. package/build/services/controllers.js +62 -0
  63. package/build/services/controllers.js.map +1 -0
  64. package/build/services/effectiveCapabilities.d.ts.map +1 -1
  65. package/build/services/effectiveCapabilities.js +18 -24
  66. package/build/services/effectiveCapabilities.js.map +1 -1
  67. package/build/services/neuralBand/NeuralBandInput.d.ts +17 -6
  68. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
  69. package/build/services/neuralBand/NeuralBandInput.js +40 -23
  70. package/build/services/neuralBand/NeuralBandInput.js.map +1 -1
  71. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
  72. package/build/services/neuralBand/NeuralBandSettingsSync.js +23 -26
  73. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
  74. package/build/services/settingsBridge.d.ts +26 -18
  75. package/build/services/settingsBridge.d.ts.map +1 -1
  76. package/build/services/settingsBridge.js +36 -15
  77. package/build/services/settingsBridge.js.map +1 -1
  78. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  79. package/build/stores/bluetoothSettingKeys.js +3 -1
  80. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  81. package/build/stores/core.d.ts.map +1 -1
  82. package/build/stores/core.js +0 -1
  83. package/build/stores/core.js.map +1 -1
  84. package/build/stores/glasses.d.ts.map +1 -1
  85. package/build/stores/glasses.js +2 -6
  86. package/build/stores/glasses.js.map +1 -1
  87. package/build/stores/settings.d.ts.map +1 -1
  88. package/build/stores/settings.js +28 -2
  89. package/build/stores/settings.js.map +1 -1
  90. package/package.json +12 -8
  91. package/src/engine.ts +3 -0
  92. package/src/facades/ai.ts +43 -0
  93. package/src/facades/glasses.ts +60 -31
  94. package/src/facades/glassesSettings.ts +1 -0
  95. package/src/facades/pairing.ts +18 -14
  96. package/src/generated/releaseMetadata.ts +4 -4
  97. package/src/index.ts +16 -2
  98. package/src/services/DeviceEventRouter.ts +8 -1
  99. package/src/services/GestureDotsService.ts +20 -14
  100. package/src/services/GlassesTouchInput.ts +11 -9
  101. package/src/services/LlmRouting.ts +49 -0
  102. package/src/services/LocalLlmModelManager.ts +327 -0
  103. package/src/services/LocalLlmPrompt.ts +246 -0
  104. package/src/services/LocalLlmProvider.ts +289 -0
  105. package/src/services/LocalMiniappRuntime.ts +137 -21
  106. package/src/services/STTModelManager.ts +140 -8
  107. package/src/services/controllers.ts +77 -0
  108. package/src/services/effectiveCapabilities.ts +16 -32
  109. package/src/services/neuralBand/NeuralBandInput.ts +50 -26
  110. package/src/services/neuralBand/NeuralBandSettingsSync.ts +28 -26
  111. package/src/services/settingsBridge.ts +60 -22
  112. package/src/stores/bluetoothSettingKeys.ts +3 -1
  113. package/src/stores/core.ts +0 -1
  114. package/src/stores/glasses.ts +2 -6
  115. package/src/stores/settings.ts +28 -2
@@ -0,0 +1,79 @@
1
+ export type LocalLlmModelId = "qwen3-0.6b" | "qwen3-1.7b" | "qwen3-4b";
2
+ export declare const DEFAULT_LOCAL_LLM_MODEL: LocalLlmModelId;
3
+ export interface LocalLlmModelConfig {
4
+ id: LocalLlmModelId;
5
+ /** Human label for the Settings dropdown. */
6
+ label: string;
7
+ /** Approximate download size in bytes (the .pte dominates). */
8
+ sizeBytes: number;
9
+ /** Rough resident memory while loaded; the Settings copy warns above ~half of device RAM. */
10
+ ramBytes: number;
11
+ urls: {
12
+ model: string;
13
+ tokenizer: string;
14
+ tokenizerConfig: string;
15
+ };
16
+ }
17
+ /** The three files every model directory holds; names are fixed so paths never depend on the URL. */
18
+ export interface LocalLlmModelPaths {
19
+ model: string;
20
+ tokenizer: string;
21
+ tokenizerConfig: string;
22
+ }
23
+ export type LocalLlmDownloadStage = "idle" | "downloading" | "complete" | "failed";
24
+ export interface LocalLlmDownloadStatus {
25
+ modelId: LocalLlmModelId;
26
+ stage: LocalLlmDownloadStage;
27
+ /** 0–100, meaningful while stage is "downloading". */
28
+ percent: number;
29
+ error?: string;
30
+ }
31
+ export declare function isLocalLlmModelId(value: unknown): value is LocalLlmModelId;
32
+ type StatusListener = (status: LocalLlmDownloadStatus | null) => void;
33
+ declare class LocalLlmModelManager {
34
+ private static instance;
35
+ private downloadJobId?;
36
+ private inFlight;
37
+ private cancelled;
38
+ private currentStatus;
39
+ private listeners;
40
+ private modelReadyListeners;
41
+ private constructor();
42
+ static getInstance(): LocalLlmModelManager;
43
+ models(): LocalLlmModelConfig[];
44
+ model(id: LocalLlmModelId): LocalLlmModelConfig;
45
+ /** The Settings > AI Models pick; garbage in the store resolves to the default. */
46
+ selectedModelId(): LocalLlmModelId;
47
+ select(id: LocalLlmModelId): Promise<void>;
48
+ getModelDirectory(): string;
49
+ paths(id?: LocalLlmModelId): LocalLlmModelPaths;
50
+ isModelAvailable(id?: LocalLlmModelId): Promise<boolean>;
51
+ downloadedModelIds(): Promise<LocalLlmModelId[]>;
52
+ /** Subscribe to download status changes. Fires immediately with the current status. */
53
+ subscribe(listener: StatusListener): () => void;
54
+ getStatus(): LocalLlmDownloadStatus | null;
55
+ /** Fired once a model's files are complete on disk. */
56
+ onModelReady(listener: (modelId: LocalLlmModelId) => void): () => void;
57
+ /**
58
+ * True when the selected model is on disk. Otherwise starts (or joins) its
59
+ * download in the background and returns false — the caller reports
60
+ * "downloading" rather than waiting out a multi-GB transfer.
61
+ */
62
+ ensureSelectedModel(): Promise<boolean>;
63
+ /**
64
+ * Download one model (three files, model last). Concurrent calls for the
65
+ * same model share the transfer; a call for a different model waits for
66
+ * the running one — the RNFS job slot is singular.
67
+ */
68
+ downloadModel(id?: LocalLlmModelId): Promise<void>;
69
+ private downloadInner;
70
+ private fetchFile;
71
+ cancelDownload(): Promise<void>;
72
+ deleteModel(id?: LocalLlmModelId): Promise<void>;
73
+ private setStatus;
74
+ static formatBytes(bytes: number): string;
75
+ }
76
+ declare const instance: LocalLlmModelManager;
77
+ export { LocalLlmModelManager };
78
+ export default instance;
79
+ //# sourceMappingURL=LocalLlmModelManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLlmModelManager.d.ts","sourceRoot":"","sources":["../../src/services/LocalLlmModelManager.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,YAAY,GAAG,UAAU,CAAA;AAEtE,eAAO,MAAM,uBAAuB,EAAE,eAA8B,CAAA;AAEpE,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,eAAe,CAAA;IACnB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAA;IACjB,6FAA6F;IAC7F,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,eAAe,EAAE,MAAM,CAAA;KACxB,CAAA;CACF;AAED,qGAAqG;AACrG,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,QAAQ,CAAA;AAElF,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,eAAe,CAAA;IACxB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA6CD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAE1E;AAED,KAAK,cAAc,GAAG,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI,KAAK,IAAI,CAAA;AAErE,cAAM,oBAAoB;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAsB;IAE7C,OAAO,CAAC,aAAa,CAAC,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAkE;IAClF,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,aAAa,CAAsC;IAC3D,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,mBAAmB,CAAgD;IAE3E,OAAO;IAEP,MAAM,CAAC,WAAW,IAAI,oBAAoB;IAO1C,MAAM,IAAI,mBAAmB,EAAE;IAI/B,KAAK,CAAC,EAAE,EAAE,eAAe,GAAG,mBAAmB;IAI/C,mFAAmF;IACnF,eAAe,IAAI,eAAe;IAK5B,MAAM,CAAC,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD,iBAAiB,IAAI,MAAM;IAI3B,KAAK,CAAC,EAAE,GAAE,eAAwC,GAAG,kBAAkB;IASjE,gBAAgB,CAAC,EAAE,GAAE,eAAwC,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBhF,kBAAkB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAQtD,uFAAuF;IACvF,SAAS,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI;IAQ/C,SAAS,IAAI,sBAAsB,GAAG,IAAI;IAI1C,uDAAuD;IACvD,YAAY,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAKtE;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC;IAS7C;;;;OAIG;IACG,aAAa,CAAC,EAAE,GAAE,eAAwC,GAAG,OAAO,CAAC,IAAI,CAAC;YAYlE,aAAa;YAuCb,SAAS;IA2BjB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,WAAW,CAAC,EAAE,GAAE,eAAwC,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9E,OAAO,CAAC,SAAS;IAWjB,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CAI1C;AAED,QAAA,MAAM,QAAQ,sBAAqC,CAAA;AACnD,OAAO,EAAC,oBAAoB,EAAC,CAAA;AAC7B,eAAe,QAAQ,CAAA"}
@@ -0,0 +1,278 @@
1
+ /**
2
+ * LocalLlmModelManager — the on-device LLM model files: a catalog of
3
+ * ExecuTorch exports (Software Mansion's Qwen3 .pte builds on Hugging Face),
4
+ * their download into Documents/llm_models/<id>/, availability checks, and a
5
+ * status stream the Settings > AI Models screen renders. Modelled on
6
+ * STTModelManager; the download itself is RNFS so the Settings screen can
7
+ * list, cancel, and delete models with the same calls the STT rows use.
8
+ *
9
+ * Which model is "the" on-device model is the `local_llm_model` setting
10
+ * (Settings > AI Models). Downloads are on demand: the first request that
11
+ * routes on-device without a model present starts one (ensureSelectedModel),
12
+ * and the Settings screen can start one explicitly.
13
+ */
14
+ import { Platform } from "react-native";
15
+ import * as RNFS from "@dr.pogodin/react-native-fs";
16
+ import { SETTINGS, useSettingsStore } from "../stores/settings";
17
+ export const DEFAULT_LOCAL_LLM_MODEL = "qwen3-1.7b";
18
+ // XNNPACK 8da4w exports from software-mansion/react-native-executorch-qwen-3,
19
+ // pinned to the v0.10.0 release tag that react-native-executorch 0.10.x
20
+ // expects. Sizes are the .pte file sizes on the Hub (tokenizer adds ~11 MB).
21
+ const QWEN3_BASE = "https://huggingface.co/software-mansion/react-native-executorch-qwen-3/resolve/v0.10.0";
22
+ const MB = 1024 * 1024;
23
+ const GB = 1024 * MB;
24
+ const MODELS = {
25
+ "qwen3-0.6b": {
26
+ id: "qwen3-0.6b",
27
+ label: "Qwen3 0.6B",
28
+ sizeBytes: 517 * MB,
29
+ ramBytes: 0.9 * GB,
30
+ urls: {
31
+ model: `${QWEN3_BASE}/0_6b/xnnpack/qwen_3_0_6b_xnnpack_8da4w.pte`,
32
+ tokenizer: `${QWEN3_BASE}/tokenizer.json`,
33
+ tokenizerConfig: `${QWEN3_BASE}/tokenizer_config.json`,
34
+ },
35
+ },
36
+ "qwen3-1.7b": {
37
+ id: "qwen3-1.7b",
38
+ label: "Qwen3 1.7B",
39
+ sizeBytes: 1315 * MB,
40
+ ramBytes: 1.8 * GB,
41
+ urls: {
42
+ model: `${QWEN3_BASE}/1_7b/xnnpack/qwen_3_1_7b_xnnpack_8da4w.pte`,
43
+ tokenizer: `${QWEN3_BASE}/tokenizer.json`,
44
+ tokenizerConfig: `${QWEN3_BASE}/tokenizer_config.json`,
45
+ },
46
+ },
47
+ "qwen3-4b": {
48
+ id: "qwen3-4b",
49
+ label: "Qwen3 4B",
50
+ sizeBytes: 2693 * MB,
51
+ ramBytes: 3.4 * GB,
52
+ urls: {
53
+ model: `${QWEN3_BASE}/4b/xnnpack/qwen_3_4b_xnnpack_8da4w.pte`,
54
+ tokenizer: `${QWEN3_BASE}/tokenizer.json`,
55
+ tokenizerConfig: `${QWEN3_BASE}/tokenizer_config.json`,
56
+ },
57
+ },
58
+ };
59
+ export function isLocalLlmModelId(value) {
60
+ return typeof value === "string" && value in MODELS;
61
+ }
62
+ class LocalLlmModelManager {
63
+ static instance;
64
+ downloadJobId;
65
+ inFlight = null;
66
+ cancelled = false;
67
+ currentStatus = null;
68
+ listeners = new Set();
69
+ modelReadyListeners = new Set();
70
+ constructor() { }
71
+ static getInstance() {
72
+ if (!LocalLlmModelManager.instance) {
73
+ LocalLlmModelManager.instance = new LocalLlmModelManager();
74
+ }
75
+ return LocalLlmModelManager.instance;
76
+ }
77
+ models() {
78
+ return Object.values(MODELS);
79
+ }
80
+ model(id) {
81
+ return MODELS[id];
82
+ }
83
+ /** The Settings > AI Models pick; garbage in the store resolves to the default. */
84
+ selectedModelId() {
85
+ const raw = useSettingsStore.getState().getSetting(SETTINGS.local_llm_model.key);
86
+ return isLocalLlmModelId(raw) ? raw : DEFAULT_LOCAL_LLM_MODEL;
87
+ }
88
+ async select(id) {
89
+ await useSettingsStore.getState().setSetting(SETTINGS.local_llm_model.key, id);
90
+ }
91
+ getModelDirectory() {
92
+ return `${RNFS.DocumentDirectoryPath}/llm_models`;
93
+ }
94
+ paths(id = this.selectedModelId()) {
95
+ const dir = `${this.getModelDirectory()}/${id}`;
96
+ return {
97
+ model: `${dir}/model.pte`,
98
+ tokenizer: `${dir}/tokenizer.json`,
99
+ tokenizerConfig: `${dir}/tokenizer_config.json`,
100
+ };
101
+ }
102
+ async isModelAvailable(id = this.selectedModelId()) {
103
+ const paths = this.paths(id);
104
+ try {
105
+ for (const path of [paths.tokenizerConfig, paths.tokenizer, paths.model]) {
106
+ if (!(await RNFS.exists(path)))
107
+ return false;
108
+ }
109
+ // A truncated .pte loads as garbage or crashes the runtime: insist on
110
+ // most of the published size. Downloads land in a .part file and are
111
+ // renamed only once whole, so this only catches external tampering.
112
+ const stat = await RNFS.stat(paths.model);
113
+ return Number(stat.size) >= MODELS[id].sizeBytes * 0.95;
114
+ }
115
+ catch (error) {
116
+ console.warn("LOCAL_LLM: availability check failed:", error);
117
+ return false;
118
+ }
119
+ }
120
+ async downloadedModelIds() {
121
+ const ids = [];
122
+ for (const id of Object.keys(MODELS)) {
123
+ if (await this.isModelAvailable(id))
124
+ ids.push(id);
125
+ }
126
+ return ids;
127
+ }
128
+ /** Subscribe to download status changes. Fires immediately with the current status. */
129
+ subscribe(listener) {
130
+ this.listeners.add(listener);
131
+ listener(this.currentStatus);
132
+ return () => {
133
+ this.listeners.delete(listener);
134
+ };
135
+ }
136
+ getStatus() {
137
+ return this.currentStatus;
138
+ }
139
+ /** Fired once a model's files are complete on disk. */
140
+ onModelReady(listener) {
141
+ this.modelReadyListeners.add(listener);
142
+ return () => this.modelReadyListeners.delete(listener);
143
+ }
144
+ /**
145
+ * True when the selected model is on disk. Otherwise starts (or joins) its
146
+ * download in the background and returns false — the caller reports
147
+ * "downloading" rather than waiting out a multi-GB transfer.
148
+ */
149
+ async ensureSelectedModel() {
150
+ const id = this.selectedModelId();
151
+ if (await this.isModelAvailable(id))
152
+ return true;
153
+ void this.downloadModel(id).catch((error) => {
154
+ console.warn(`LOCAL_LLM: background download of ${id} failed:`, error);
155
+ });
156
+ return false;
157
+ }
158
+ /**
159
+ * Download one model (three files, model last). Concurrent calls for the
160
+ * same model share the transfer; a call for a different model waits for
161
+ * the running one — the RNFS job slot is singular.
162
+ */
163
+ async downloadModel(id = this.selectedModelId()) {
164
+ if (this.inFlight) {
165
+ if (this.inFlight.modelId === id)
166
+ return this.inFlight.promise;
167
+ await this.inFlight.promise.catch(() => undefined);
168
+ }
169
+ const promise = this.downloadInner(id).finally(() => {
170
+ this.inFlight = null;
171
+ });
172
+ this.inFlight = { modelId: id, promise };
173
+ return promise;
174
+ }
175
+ async downloadInner(id) {
176
+ const config = MODELS[id];
177
+ const paths = this.paths(id);
178
+ const dir = `${this.getModelDirectory()}/${id}`;
179
+ this.cancelled = false;
180
+ this.setStatus({ modelId: id, stage: "downloading", percent: 0 });
181
+ try {
182
+ await RNFS.mkdir(dir, { NSURLIsExcludedFromBackupKey: true });
183
+ // Tokenizer files first (small), the weights last: a model directory
184
+ // is only "available" once model.pte exists, so an interrupted run
185
+ // never leaves a complete-looking directory behind.
186
+ await this.fetchFile(config.urls.tokenizerConfig, paths.tokenizerConfig);
187
+ await this.fetchFile(config.urls.tokenizer, paths.tokenizer);
188
+ await this.fetchFile(config.urls.model, paths.model, (percent) => this.setStatus({ modelId: id, stage: "downloading", percent }));
189
+ this.setStatus({ modelId: id, stage: "complete", percent: 100 });
190
+ for (const listener of this.modelReadyListeners) {
191
+ try {
192
+ listener(id);
193
+ }
194
+ catch (error) {
195
+ console.warn("LOCAL_LLM: model-ready listener failed:", error);
196
+ }
197
+ }
198
+ }
199
+ catch (error) {
200
+ const message = this.cancelled ? "download cancelled" : error instanceof Error ? error.message : String(error);
201
+ this.setStatus({ modelId: id, stage: this.cancelled ? "idle" : "failed", percent: 0, error: message });
202
+ try {
203
+ if (await RNFS.exists(`${paths.model}.part`))
204
+ await RNFS.unlink(`${paths.model}.part`);
205
+ }
206
+ catch (cleanupError) {
207
+ console.warn("LOCAL_LLM: partial cleanup failed:", cleanupError);
208
+ }
209
+ throw error;
210
+ }
211
+ finally {
212
+ this.downloadJobId = undefined;
213
+ }
214
+ }
215
+ async fetchFile(url, dest, onPercent) {
216
+ if (this.cancelled)
217
+ throw new Error("download cancelled");
218
+ const partial = `${dest}.part`;
219
+ const download = RNFS.downloadFile({
220
+ fromUrl: url,
221
+ toFile: partial,
222
+ // iOS keeps a background URLSession transfer going while the app is
223
+ // suspended; the files here are large enough to need it.
224
+ background: Platform.OS === "ios",
225
+ discretionary: false,
226
+ progressDivider: 1,
227
+ connectionTimeout: 30_000,
228
+ readTimeout: 60_000,
229
+ progress: (res) => {
230
+ if (!onPercent || res.contentLength <= 0)
231
+ return;
232
+ onPercent(Math.min(99, Math.floor((res.bytesWritten / res.contentLength) * 100)));
233
+ },
234
+ });
235
+ this.downloadJobId = download.jobId;
236
+ const result = await download.promise;
237
+ if (result.statusCode !== 200) {
238
+ throw new Error(`download failed with status ${result.statusCode}`);
239
+ }
240
+ if (await RNFS.exists(dest))
241
+ await RNFS.unlink(dest);
242
+ await RNFS.moveFile(partial, dest);
243
+ }
244
+ async cancelDownload() {
245
+ if (this.downloadJobId === undefined)
246
+ return;
247
+ this.cancelled = true;
248
+ await RNFS.stopDownload(this.downloadJobId);
249
+ this.downloadJobId = undefined;
250
+ }
251
+ async deleteModel(id = this.selectedModelId()) {
252
+ const dir = `${this.getModelDirectory()}/${id}`;
253
+ if (await RNFS.exists(dir))
254
+ await RNFS.unlink(dir);
255
+ if (this.currentStatus?.modelId === id)
256
+ this.setStatus(null);
257
+ }
258
+ setStatus(status) {
259
+ this.currentStatus = status;
260
+ for (const listener of this.listeners) {
261
+ try {
262
+ listener(status);
263
+ }
264
+ catch (error) {
265
+ console.warn("LOCAL_LLM: status listener threw:", error);
266
+ }
267
+ }
268
+ }
269
+ static formatBytes(bytes) {
270
+ if (bytes >= GB)
271
+ return `${(bytes / GB).toFixed(1)} GB`;
272
+ return `${Math.round(bytes / MB)} MB`;
273
+ }
274
+ }
275
+ const instance = LocalLlmModelManager.getInstance();
276
+ export { LocalLlmModelManager };
277
+ export default instance;
278
+ //# sourceMappingURL=LocalLlmModelManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLlmModelManager.js","sourceRoot":"","sources":["../../src/services/LocalLlmModelManager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * LocalLlmModelManager — the on-device LLM model files: a catalog of\n * ExecuTorch exports (Software Mansion's Qwen3 .pte builds on Hugging Face),\n * their download into Documents/llm_models/<id>/, availability checks, and a\n * status stream the Settings > AI Models screen renders. Modelled on\n * STTModelManager; the download itself is RNFS so the Settings screen can\n * list, cancel, and delete models with the same calls the STT rows use.\n *\n * Which model is \"the\" on-device model is the `local_llm_model` setting\n * (Settings > AI Models). Downloads are on demand: the first request that\n * routes on-device without a model present starts one (ensureSelectedModel),\n * and the Settings screen can start one explicitly.\n */\nimport {Platform} from \"react-native\"\nimport * as RNFS from \"@dr.pogodin/react-native-fs\"\n\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\n\nexport type LocalLlmModelId = \"qwen3-0.6b\" | \"qwen3-1.7b\" | \"qwen3-4b\"\n\nexport const DEFAULT_LOCAL_LLM_MODEL: LocalLlmModelId = \"qwen3-1.7b\"\n\nexport interface LocalLlmModelConfig {\n id: LocalLlmModelId\n /** Human label for the Settings dropdown. */\n label: string\n /** Approximate download size in bytes (the .pte dominates). */\n sizeBytes: number\n /** Rough resident memory while loaded; the Settings copy warns above ~half of device RAM. */\n ramBytes: number\n urls: {\n model: string\n tokenizer: string\n tokenizerConfig: string\n }\n}\n\n/** The three files every model directory holds; names are fixed so paths never depend on the URL. */\nexport interface LocalLlmModelPaths {\n model: string\n tokenizer: string\n tokenizerConfig: string\n}\n\nexport type LocalLlmDownloadStage = \"idle\" | \"downloading\" | \"complete\" | \"failed\"\n\nexport interface LocalLlmDownloadStatus {\n modelId: LocalLlmModelId\n stage: LocalLlmDownloadStage\n /** 0–100, meaningful while stage is \"downloading\". */\n percent: number\n error?: string\n}\n\n// XNNPACK 8da4w exports from software-mansion/react-native-executorch-qwen-3,\n// pinned to the v0.10.0 release tag that react-native-executorch 0.10.x\n// expects. Sizes are the .pte file sizes on the Hub (tokenizer adds ~11 MB).\nconst QWEN3_BASE = \"https://huggingface.co/software-mansion/react-native-executorch-qwen-3/resolve/v0.10.0\"\nconst MB = 1024 * 1024\nconst GB = 1024 * MB\n\nconst MODELS: Record<LocalLlmModelId, LocalLlmModelConfig> = {\n \"qwen3-0.6b\": {\n id: \"qwen3-0.6b\",\n label: \"Qwen3 0.6B\",\n sizeBytes: 517 * MB,\n ramBytes: 0.9 * GB,\n urls: {\n model: `${QWEN3_BASE}/0_6b/xnnpack/qwen_3_0_6b_xnnpack_8da4w.pte`,\n tokenizer: `${QWEN3_BASE}/tokenizer.json`,\n tokenizerConfig: `${QWEN3_BASE}/tokenizer_config.json`,\n },\n },\n \"qwen3-1.7b\": {\n id: \"qwen3-1.7b\",\n label: \"Qwen3 1.7B\",\n sizeBytes: 1315 * MB,\n ramBytes: 1.8 * GB,\n urls: {\n model: `${QWEN3_BASE}/1_7b/xnnpack/qwen_3_1_7b_xnnpack_8da4w.pte`,\n tokenizer: `${QWEN3_BASE}/tokenizer.json`,\n tokenizerConfig: `${QWEN3_BASE}/tokenizer_config.json`,\n },\n },\n \"qwen3-4b\": {\n id: \"qwen3-4b\",\n label: \"Qwen3 4B\",\n sizeBytes: 2693 * MB,\n ramBytes: 3.4 * GB,\n urls: {\n model: `${QWEN3_BASE}/4b/xnnpack/qwen_3_4b_xnnpack_8da4w.pte`,\n tokenizer: `${QWEN3_BASE}/tokenizer.json`,\n tokenizerConfig: `${QWEN3_BASE}/tokenizer_config.json`,\n },\n },\n}\n\nexport function isLocalLlmModelId(value: unknown): value is LocalLlmModelId {\n return typeof value === \"string\" && value in MODELS\n}\n\ntype StatusListener = (status: LocalLlmDownloadStatus | null) => void\n\nclass LocalLlmModelManager {\n private static instance: LocalLlmModelManager\n\n private downloadJobId?: number\n private inFlight: {modelId: LocalLlmModelId; promise: Promise<void>} | null = null\n private cancelled = false\n private currentStatus: LocalLlmDownloadStatus | null = null\n private listeners = new Set<StatusListener>()\n private modelReadyListeners = new Set<(modelId: LocalLlmModelId) => void>()\n\n private constructor() {}\n\n static getInstance(): LocalLlmModelManager {\n if (!LocalLlmModelManager.instance) {\n LocalLlmModelManager.instance = new LocalLlmModelManager()\n }\n return LocalLlmModelManager.instance\n }\n\n models(): LocalLlmModelConfig[] {\n return Object.values(MODELS)\n }\n\n model(id: LocalLlmModelId): LocalLlmModelConfig {\n return MODELS[id]\n }\n\n /** The Settings > AI Models pick; garbage in the store resolves to the default. */\n selectedModelId(): LocalLlmModelId {\n const raw = useSettingsStore.getState().getSetting(SETTINGS.local_llm_model.key)\n return isLocalLlmModelId(raw) ? raw : DEFAULT_LOCAL_LLM_MODEL\n }\n\n async select(id: LocalLlmModelId): Promise<void> {\n await useSettingsStore.getState().setSetting(SETTINGS.local_llm_model.key, id)\n }\n\n getModelDirectory(): string {\n return `${RNFS.DocumentDirectoryPath}/llm_models`\n }\n\n paths(id: LocalLlmModelId = this.selectedModelId()): LocalLlmModelPaths {\n const dir = `${this.getModelDirectory()}/${id}`\n return {\n model: `${dir}/model.pte`,\n tokenizer: `${dir}/tokenizer.json`,\n tokenizerConfig: `${dir}/tokenizer_config.json`,\n }\n }\n\n async isModelAvailable(id: LocalLlmModelId = this.selectedModelId()): Promise<boolean> {\n const paths = this.paths(id)\n try {\n for (const path of [paths.tokenizerConfig, paths.tokenizer, paths.model]) {\n if (!(await RNFS.exists(path))) return false\n }\n // A truncated .pte loads as garbage or crashes the runtime: insist on\n // most of the published size. Downloads land in a .part file and are\n // renamed only once whole, so this only catches external tampering.\n const stat = await RNFS.stat(paths.model)\n return Number(stat.size) >= MODELS[id].sizeBytes * 0.95\n } catch (error) {\n console.warn(\"LOCAL_LLM: availability check failed:\", error)\n return false\n }\n }\n\n async downloadedModelIds(): Promise<LocalLlmModelId[]> {\n const ids: LocalLlmModelId[] = []\n for (const id of Object.keys(MODELS) as LocalLlmModelId[]) {\n if (await this.isModelAvailable(id)) ids.push(id)\n }\n return ids\n }\n\n /** Subscribe to download status changes. Fires immediately with the current status. */\n subscribe(listener: StatusListener): () => void {\n this.listeners.add(listener)\n listener(this.currentStatus)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n getStatus(): LocalLlmDownloadStatus | null {\n return this.currentStatus\n }\n\n /** Fired once a model's files are complete on disk. */\n onModelReady(listener: (modelId: LocalLlmModelId) => void): () => void {\n this.modelReadyListeners.add(listener)\n return () => this.modelReadyListeners.delete(listener)\n }\n\n /**\n * True when the selected model is on disk. Otherwise starts (or joins) its\n * download in the background and returns false — the caller reports\n * \"downloading\" rather than waiting out a multi-GB transfer.\n */\n async ensureSelectedModel(): Promise<boolean> {\n const id = this.selectedModelId()\n if (await this.isModelAvailable(id)) return true\n void this.downloadModel(id).catch((error) => {\n console.warn(`LOCAL_LLM: background download of ${id} failed:`, error)\n })\n return false\n }\n\n /**\n * Download one model (three files, model last). Concurrent calls for the\n * same model share the transfer; a call for a different model waits for\n * the running one — the RNFS job slot is singular.\n */\n async downloadModel(id: LocalLlmModelId = this.selectedModelId()): Promise<void> {\n if (this.inFlight) {\n if (this.inFlight.modelId === id) return this.inFlight.promise\n await this.inFlight.promise.catch(() => undefined)\n }\n const promise = this.downloadInner(id).finally(() => {\n this.inFlight = null\n })\n this.inFlight = {modelId: id, promise}\n return promise\n }\n\n private async downloadInner(id: LocalLlmModelId): Promise<void> {\n const config = MODELS[id]\n const paths = this.paths(id)\n const dir = `${this.getModelDirectory()}/${id}`\n this.cancelled = false\n this.setStatus({modelId: id, stage: \"downloading\", percent: 0})\n\n try {\n await RNFS.mkdir(dir, {NSURLIsExcludedFromBackupKey: true})\n // Tokenizer files first (small), the weights last: a model directory\n // is only \"available\" once model.pte exists, so an interrupted run\n // never leaves a complete-looking directory behind.\n await this.fetchFile(config.urls.tokenizerConfig, paths.tokenizerConfig)\n await this.fetchFile(config.urls.tokenizer, paths.tokenizer)\n await this.fetchFile(config.urls.model, paths.model, (percent) =>\n this.setStatus({modelId: id, stage: \"downloading\", percent}),\n )\n this.setStatus({modelId: id, stage: \"complete\", percent: 100})\n for (const listener of this.modelReadyListeners) {\n try {\n listener(id)\n } catch (error) {\n console.warn(\"LOCAL_LLM: model-ready listener failed:\", error)\n }\n }\n } catch (error) {\n const message = this.cancelled ? \"download cancelled\" : error instanceof Error ? error.message : String(error)\n this.setStatus({modelId: id, stage: this.cancelled ? \"idle\" : \"failed\", percent: 0, error: message})\n try {\n if (await RNFS.exists(`${paths.model}.part`)) await RNFS.unlink(`${paths.model}.part`)\n } catch (cleanupError) {\n console.warn(\"LOCAL_LLM: partial cleanup failed:\", cleanupError)\n }\n throw error\n } finally {\n this.downloadJobId = undefined\n }\n }\n\n private async fetchFile(url: string, dest: string, onPercent?: (percent: number) => void): Promise<void> {\n if (this.cancelled) throw new Error(\"download cancelled\")\n const partial = `${dest}.part`\n const download = RNFS.downloadFile({\n fromUrl: url,\n toFile: partial,\n // iOS keeps a background URLSession transfer going while the app is\n // suspended; the files here are large enough to need it.\n background: Platform.OS === \"ios\",\n discretionary: false,\n progressDivider: 1,\n connectionTimeout: 30_000,\n readTimeout: 60_000,\n progress: (res) => {\n if (!onPercent || res.contentLength <= 0) return\n onPercent(Math.min(99, Math.floor((res.bytesWritten / res.contentLength) * 100)))\n },\n })\n this.downloadJobId = download.jobId\n const result = await download.promise\n if (result.statusCode !== 200) {\n throw new Error(`download failed with status ${result.statusCode}`)\n }\n if (await RNFS.exists(dest)) await RNFS.unlink(dest)\n await RNFS.moveFile(partial, dest)\n }\n\n async cancelDownload(): Promise<void> {\n if (this.downloadJobId === undefined) return\n this.cancelled = true\n await RNFS.stopDownload(this.downloadJobId)\n this.downloadJobId = undefined\n }\n\n async deleteModel(id: LocalLlmModelId = this.selectedModelId()): Promise<void> {\n const dir = `${this.getModelDirectory()}/${id}`\n if (await RNFS.exists(dir)) await RNFS.unlink(dir)\n if (this.currentStatus?.modelId === id) this.setStatus(null)\n }\n\n private setStatus(status: LocalLlmDownloadStatus | null): void {\n this.currentStatus = status\n for (const listener of this.listeners) {\n try {\n listener(status)\n } catch (error) {\n console.warn(\"LOCAL_LLM: status listener threw:\", error)\n }\n }\n }\n\n static formatBytes(bytes: number): string {\n if (bytes >= GB) return `${(bytes / GB).toFixed(1)} GB`\n return `${Math.round(bytes / MB)} MB`\n }\n}\n\nconst instance = LocalLlmModelManager.getInstance()\nexport {LocalLlmModelManager}\nexport default instance\n"]}
@@ -0,0 +1,90 @@
1
+ import type { LlmMessage, LlmStopReason, LlmTool, LlmToolCall } from "@glassly/cloud-protocol/llm";
2
+ export interface TemplateToolCall {
3
+ id: string;
4
+ type: "function";
5
+ function: {
6
+ name: string;
7
+ arguments: Record<string, unknown>;
8
+ };
9
+ }
10
+ export type TemplateMessage = {
11
+ role: "system" | "user";
12
+ content: string;
13
+ } | {
14
+ role: "assistant";
15
+ content: string;
16
+ tool_calls?: TemplateToolCall[];
17
+ } | {
18
+ role: "tool";
19
+ content: string;
20
+ name?: string;
21
+ tool_call_id?: string;
22
+ };
23
+ export interface TemplateTool {
24
+ type: "function";
25
+ function: {
26
+ name: string;
27
+ description: string;
28
+ parameters: Record<string, unknown>;
29
+ };
30
+ }
31
+ /** Appended to the system prompt under `jsonMode`; small models need it said twice. */
32
+ export declare const JSON_MODE_INSTRUCTION = "Respond with exactly one JSON object and nothing else: no prose before or after it, no markdown fences.";
33
+ /**
34
+ * Neutral messages → template messages. A user turn carrying toolResults
35
+ * becomes one `tool` message per result (the template groups consecutive
36
+ * tool messages into a single user block), followed by the user text if any.
37
+ */
38
+ export declare function buildTemplateMessages(request: {
39
+ messages: LlmMessage[];
40
+ system?: string;
41
+ jsonMode?: boolean;
42
+ }): TemplateMessage[];
43
+ export declare function toTemplateTools(tools?: LlmTool[]): TemplateTool[] | undefined;
44
+ export declare function estimateTokens(text: string): number;
45
+ export declare function estimateMessageTokens(message: TemplateMessage): number;
46
+ /**
47
+ * Fit the conversation into `budgetTokens`. History is grouped into turns
48
+ * (a user message plus everything up to the next user message, so a tool
49
+ * round stays with the question it answers); the oldest turns go first and
50
+ * the last turn is never dropped — the model must at least see the question.
51
+ * When that last turn alone still does not fit, tool outputs are cut to
52
+ * their head.
53
+ */
54
+ export declare function fitToBudget(messages: TemplateMessage[], budgetTokens: number, toolTokens?: number): TemplateMessage[];
55
+ /** Remove a leading `<think>…</think>` block (or an unterminated one). */
56
+ export declare function stripThinking(text: string): string;
57
+ /** Pull `<tool_call>` blocks out of the reply. Unparseable blocks are dropped, not surfaced. */
58
+ export declare function parseToolCalls(text: string, makeId: (index: number) => string): {
59
+ toolCalls: LlmToolCall[];
60
+ text: string;
61
+ };
62
+ /**
63
+ * The first JSON object in the reply, repaired if the model left it slightly
64
+ * broken (trailing commas, single quotes, a dropped brace). Null when there
65
+ * is nothing object-shaped: the caller keeps the raw text and the miniapp's
66
+ * own parser decides.
67
+ */
68
+ export declare function extractJsonObject(text: string): string | null;
69
+ export interface LocalGeneration {
70
+ text: string;
71
+ promptTokens: number;
72
+ generatedTokens: number;
73
+ maxNewTokens: number;
74
+ }
75
+ export interface LocalCompletion {
76
+ text: string;
77
+ stopReason: LlmStopReason;
78
+ toolCalls?: LlmToolCall[];
79
+ usage: {
80
+ inputTokens: number;
81
+ outputTokens: number;
82
+ };
83
+ }
84
+ /** Raw generation → neutral completion. */
85
+ export declare function finalizeLocalResult(generation: LocalGeneration, opts: {
86
+ jsonMode: boolean;
87
+ toolsOffered: boolean;
88
+ makeToolCallId: (index: number) => string;
89
+ }): LocalCompletion;
90
+ //# sourceMappingURL=LocalLlmPrompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalLlmPrompt.d.ts","sourceRoot":"","sources":["../../src/services/LocalLlmPrompt.ts"],"names":[],"mappings":"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"}