@agorapete/wllama 3.5.1-q2.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.
Files changed (86) hide show
  1. package/.gitmodules +3 -0
  2. package/.prettierignore +38 -0
  3. package/AGENTS.md +1 -0
  4. package/CMakeLists.txt +131 -0
  5. package/LICENCE +21 -0
  6. package/README-dev.md +178 -0
  7. package/README.md +225 -0
  8. package/README_banner.png +0 -0
  9. package/assets/screenshot_0.png +0 -0
  10. package/cpp/generate_glue_prototype.js +115 -0
  11. package/cpp/glue.hpp +664 -0
  12. package/cpp/test_glue.cpp +80 -0
  13. package/cpp/wllama-context.h +1172 -0
  14. package/cpp/wllama-fs.h +148 -0
  15. package/cpp/wllama.cpp +187 -0
  16. package/cpp/wllama.h +6 -0
  17. package/esm/cache-manager.d.ts +130 -0
  18. package/esm/debug.d.ts +28 -0
  19. package/esm/glue/glue.d.ts +22 -0
  20. package/esm/glue/messages.d.ts +146 -0
  21. package/esm/huggingface.d.ts +31 -0
  22. package/esm/index.cjs +3406 -0
  23. package/esm/index.d.ts +8 -0
  24. package/esm/index.js +3387 -0
  25. package/esm/index.min.js +1 -0
  26. package/esm/index.min.js.map +1 -0
  27. package/esm/model-manager.d.ts +136 -0
  28. package/esm/storage/cos.d.ts +36 -0
  29. package/esm/storage/index.d.ts +33 -0
  30. package/esm/storage/opfs.d.ts +12 -0
  31. package/esm/types/oai-compat.d.ts +278 -0
  32. package/esm/types/types.d.ts +112 -0
  33. package/esm/utils.d.ts +119 -0
  34. package/esm/wasm/source-map.d.ts +1 -0
  35. package/esm/wasm/wllama.wasm +0 -0
  36. package/esm/wasm-from-cdn.d.ts +8 -0
  37. package/esm/wllama.d.ts +397 -0
  38. package/esm/worker.d.ts +92 -0
  39. package/esm/workers-code/generated.d.ts +4 -0
  40. package/guides/intro-v2.md +132 -0
  41. package/guides/intro-v3.1.md +40 -0
  42. package/guides/intro-v3.md +230 -0
  43. package/index.ts +1 -0
  44. package/package.json +71 -0
  45. package/scripts/bisect_test.sh +33 -0
  46. package/scripts/build_hf_space.sh +26 -0
  47. package/scripts/build_source_map.js +269 -0
  48. package/scripts/build_wasm.sh +19 -0
  49. package/scripts/build_worker.sh +38 -0
  50. package/scripts/check_debug_build.js +30 -0
  51. package/scripts/check_package_size.js +25 -0
  52. package/scripts/docker-compose.yml +76 -0
  53. package/scripts/generate_wasm_from_cdn.js +24 -0
  54. package/scripts/http_server.js +44 -0
  55. package/scripts/post_build.sh +32 -0
  56. package/src/cache-manager.ts +358 -0
  57. package/src/debug.ts +111 -0
  58. package/src/glue/glue.ts +291 -0
  59. package/src/glue/messages.ts +773 -0
  60. package/src/huggingface.ts +151 -0
  61. package/src/index.ts +8 -0
  62. package/src/mjs.test.ts +44 -0
  63. package/src/model-manager.test.ts +200 -0
  64. package/src/model-manager.ts +359 -0
  65. package/src/storage/cos.test.ts +83 -0
  66. package/src/storage/cos.ts +171 -0
  67. package/src/storage/index.ts +40 -0
  68. package/src/storage/opfs.ts +119 -0
  69. package/src/types/oai-compat.ts +342 -0
  70. package/src/types/types.ts +133 -0
  71. package/src/utils.test.ts +231 -0
  72. package/src/utils.ts +403 -0
  73. package/src/wasm/source-map.ts +7 -0
  74. package/src/wasm/wllama.js +1 -0
  75. package/src/wasm/wllama.wasm +0 -0
  76. package/src/wasm-from-cdn.ts +13 -0
  77. package/src/wllama.test.ts +392 -0
  78. package/src/wllama.ts +1138 -0
  79. package/src/wllama.wgpu.test.ts +62 -0
  80. package/src/worker.ts +443 -0
  81. package/src/workers-code/generated.ts +11 -0
  82. package/src/workers-code/llama-cpp.js +511 -0
  83. package/src/workers-code/opfs-utils.js +150 -0
  84. package/tsconfig.build.json +34 -0
  85. package/tsup.config.ts +23 -0
  86. package/vitest.config.ts +61 -0
@@ -0,0 +1,231 @@
1
+ import { expect, test, describe } from 'vitest';
2
+ import {
3
+ joinBuffers,
4
+ bufToText,
5
+ padDigits,
6
+ sumArr,
7
+ isString,
8
+ delay,
9
+ absoluteUrl,
10
+ parseShardNumber,
11
+ parseModelUrl,
12
+ sortFileByShard,
13
+ isValidGgufFile,
14
+ } from './utils';
15
+
16
+ describe('joinBuffers', () => {
17
+ test('joins multiple buffers correctly', () => {
18
+ const buf1 = new Uint8Array([1, 2, 3]);
19
+ const buf2 = new Uint8Array([4, 5]);
20
+
21
+ const result = joinBuffers([buf1, buf2]);
22
+
23
+ expect(result).toEqual(new Uint8Array([1, 2, 3, 4, 5]));
24
+ });
25
+ });
26
+
27
+ describe('bufToText', () => {
28
+ test('converts buffer to string', () => {
29
+ const buffer = new TextEncoder().encode('hello world');
30
+ expect(bufToText(buffer)).toBe('hello world');
31
+ });
32
+ });
33
+
34
+ describe('padDigits', () => {
35
+ test('pads number with zeros', () => {
36
+ expect(padDigits(5, 3)).toBe('005');
37
+ expect(padDigits(42, 4)).toBe('0042');
38
+ expect(padDigits(1234, 2)).toBe('1234');
39
+ });
40
+ });
41
+
42
+ describe('sumArr', () => {
43
+ test('sums array of numbers', () => {
44
+ expect(sumArr([1, 2, 3])).toBe(6);
45
+ expect(sumArr([])).toBe(0);
46
+ expect(sumArr([5])).toBe(5);
47
+ });
48
+ });
49
+
50
+ describe('isString', () => {
51
+ test('checks if value is string', () => {
52
+ expect(isString('test')).toBe(true);
53
+ expect(isString('')).toBe(true);
54
+ expect(isString(null)).toBe(false);
55
+ expect(isString(undefined)).toBe(false);
56
+ expect(isString(123)).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('delay', () => {
61
+ test('delays execution', async () => {
62
+ const start = Date.now();
63
+ await delay(100);
64
+ const elapsed = Date.now() - start;
65
+ expect(elapsed).toBeGreaterThanOrEqual(100);
66
+ });
67
+ });
68
+
69
+ describe('absoluteUrl', () => {
70
+ test('converts relative path to absolute url', () => {
71
+ // Mock document.baseURI
72
+ Object.defineProperty(document, 'baseURI', {
73
+ value: 'http://example.com/app/',
74
+ writable: true,
75
+ });
76
+
77
+ expect(absoluteUrl('test.html')).toBe('http://example.com/app/test.html');
78
+ expect(absoluteUrl('/test.html')).toBe('http://example.com/test.html');
79
+ });
80
+ });
81
+
82
+ describe('shard processing', () => {
83
+ test('parseShardNumber extracts correct info', () => {
84
+ expect(parseShardNumber('abcdef-123456-00001-of-00005.gguf')).toEqual({
85
+ baseURL: 'abcdef-123456',
86
+ current: 1,
87
+ total: 5,
88
+ });
89
+
90
+ expect(parseShardNumber('abcdef-123456.9090-q8_0.gguf')).toEqual({
91
+ baseURL: 'abcdef-123456.9090-q8_0.gguf',
92
+ current: 1,
93
+ total: 1,
94
+ });
95
+
96
+ expect(
97
+ parseShardNumber('abcdef-123456-00001-of-00005.gguf?param=value')
98
+ ).toEqual({
99
+ baseURL: 'abcdef-123456',
100
+ current: 1,
101
+ total: 5,
102
+ });
103
+
104
+ expect(
105
+ parseShardNumber('abcdef-123456-00001-of-00005.gguf?no-inline')
106
+ ).toEqual({
107
+ baseURL: 'abcdef-123456',
108
+ current: 1,
109
+ total: 5,
110
+ });
111
+
112
+ expect(
113
+ parseShardNumber(
114
+ 'abcdef-123456-00001-of-00005.gguf?param1=value1&param2=value2'
115
+ )
116
+ ).toEqual({
117
+ baseURL: 'abcdef-123456',
118
+ current: 1,
119
+ total: 5,
120
+ });
121
+ });
122
+
123
+ test('parseModelUrl generates correct shard URLs', () => {
124
+ const singleFile = 'model.gguf';
125
+ expect(parseModelUrl(singleFile)).toEqual(['model.gguf']);
126
+
127
+ const shardedFile = 'model-00001-of-00003.gguf';
128
+ expect(parseModelUrl(shardedFile)).toEqual([
129
+ 'model-00001-of-00003.gguf',
130
+ 'model-00002-of-00003.gguf',
131
+ 'model-00003-of-00003.gguf',
132
+ ]);
133
+
134
+ const complexPath = 'https://example.com/models/llama-00001-of-00002.gguf';
135
+ expect(parseModelUrl(complexPath)).toEqual([
136
+ 'https://example.com/models/llama-00001-of-00002.gguf',
137
+ 'https://example.com/models/llama-00002-of-00002.gguf',
138
+ ]);
139
+
140
+ const shardedFileWithQuery = 'model-00001-of-00003.gguf?param=value';
141
+ expect(parseModelUrl(shardedFileWithQuery)).toEqual([
142
+ 'model-00001-of-00003.gguf?param=value',
143
+ 'model-00002-of-00003.gguf?param=value',
144
+ 'model-00003-of-00003.gguf?param=value',
145
+ ]);
146
+
147
+ const complexPathWithQuery =
148
+ 'https://example.com/models/llama-00001-of-00002.gguf?no-inline';
149
+ expect(parseModelUrl(complexPathWithQuery)).toEqual([
150
+ 'https://example.com/models/llama-00001-of-00002.gguf?no-inline',
151
+ 'https://example.com/models/llama-00002-of-00002.gguf?no-inline',
152
+ ]);
153
+ });
154
+
155
+ test('sortFileByShard sorts files by shard number', () => {
156
+ const files = [
157
+ new File(
158
+ [],
159
+ 'e2fc714c4727ee9395f324cd2e7f331f-model-00003-of-00005.gguf'
160
+ ),
161
+ new File(
162
+ [],
163
+ '187ef4436122d1cc2f40dc2b92f0eba0-model-00001-of-00005.gguf'
164
+ ),
165
+ new File(
166
+ [],
167
+ 'c4357687ea2b461cb07cf0a0a3de939f-model-00002-of-00005.gguf'
168
+ ),
169
+ new File(
170
+ [],
171
+ '6a4d40512eabd63221cbdf3df4636cd7-model-00005-of-00005.gguf'
172
+ ),
173
+ new File(
174
+ [],
175
+ '0952e4c6ba320f5278605eb5333eec0f-model-00004-of-00005.gguf'
176
+ ),
177
+ ];
178
+
179
+ sortFileByShard(files);
180
+
181
+ expect(files.map((f) => parseShardNumber(f.name).current)).toEqual([
182
+ 1, 2, 3, 4, 5,
183
+ ]);
184
+
185
+ // Single file should not be affected
186
+ const singleFile = [new File([], 'model.gguf')];
187
+ sortFileByShard(singleFile);
188
+ expect(singleFile[0].name).toBe('model.gguf');
189
+
190
+ // Regular blobs should not be affected
191
+ const blobs = [new Blob(), new Blob()];
192
+ sortFileByShard(blobs);
193
+ expect(blobs.length).toBe(2);
194
+ });
195
+ });
196
+
197
+ describe('GGUF file validation', () => {
198
+ test('isValidGgufFile should correctly validate GGUF files', () => {
199
+ // Basic valid cases
200
+ expect(isValidGgufFile('model.gguf')).toBe(true);
201
+ expect(isValidGgufFile('path/to/model.gguf')).toBe(true);
202
+
203
+ // With Vite query parameters
204
+ expect(isValidGgufFile('model.gguf?no-inline')).toBe(true);
205
+ expect(isValidGgufFile('foo.gguf?no-inline')).toBe(true);
206
+
207
+ // With query parameters
208
+ expect(isValidGgufFile('model.gguf?param=value')).toBe(true);
209
+ expect(isValidGgufFile('model.gguf?param1&param2')).toBe(true);
210
+ expect(isValidGgufFile('model.gguf?no-inline&v=123')).toBe(true);
211
+ expect(isValidGgufFile('model.gguf?param1=value1&param2=value2')).toBe(
212
+ true
213
+ );
214
+ expect(isValidGgufFile('model.gguf?param=value&special=!@#$%^&*()')).toBe(
215
+ true
216
+ );
217
+ expect(isValidGgufFile('model.gguf?')).toBe(true);
218
+
219
+ // With fragments
220
+ expect(isValidGgufFile('model.gguf?param=value#fragment')).toBe(true);
221
+
222
+ // Invalid cases
223
+ expect(isValidGgufFile('model.bin')).toBe(false);
224
+ expect(isValidGgufFile('model.gguf.bin')).toBe(false);
225
+ expect(isValidGgufFile('modelgguf')).toBe(false);
226
+ expect(isValidGgufFile('path/to/model.txt?ext=gguf')).toBe(false);
227
+
228
+ // Case sensitivity
229
+ expect(isValidGgufFile('model.GGUF?param=value')).toBe(false);
230
+ });
231
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,403 @@
1
+ export const joinBuffers = (buffers: Uint8Array[]): Uint8Array => {
2
+ const totalSize = buffers.reduce((acc, buf) => acc + buf.length, 0);
3
+ const output = new Uint8Array(totalSize);
4
+ output.set(buffers[0], 0);
5
+ for (let i = 1; i < buffers.length; i++) {
6
+ output.set(buffers[i], buffers[i - 1].length);
7
+ }
8
+ return output;
9
+ };
10
+
11
+ const textDecoder = new TextDecoder();
12
+
13
+ /**
14
+ * Convert list of bytes (number) to text
15
+ * @param buffer
16
+ * @returns a string
17
+ */
18
+ export const bufToText = (buffer: ArrayBuffer | Uint8Array): string => {
19
+ return textDecoder.decode(buffer);
20
+ };
21
+
22
+ /**
23
+ * Get default stdout/stderr config for wasm module
24
+ */
25
+ export const getWModuleConfig = (pathConfig: {
26
+ [filename: string]: string;
27
+ }) => {
28
+ return {
29
+ noInitialRun: true,
30
+ print: function (text: any) {
31
+ if (arguments.length > 1)
32
+ text = Array.prototype.slice.call(arguments).join(' ');
33
+ console.log(text);
34
+ },
35
+ printErr: function (text: any) {
36
+ if (arguments.length > 1)
37
+ text = Array.prototype.slice.call(arguments).join(' ');
38
+ console.warn(text);
39
+ },
40
+ // @ts-ignore
41
+ locateFile: function (filename: string, basePath: string) {
42
+ const p = pathConfig[filename];
43
+ console.log(`Loading "${filename}" from "${p}"`);
44
+ return p;
45
+ },
46
+ };
47
+ };
48
+
49
+ export interface ShardInfo {
50
+ baseURL: string;
51
+ current: number;
52
+ total: number;
53
+ }
54
+
55
+ const URL_PARTS_REGEX = /-(\d{5})-of-(\d{5})\.gguf(?:\?.*)?$/;
56
+
57
+ /**
58
+ * Parse shard number and total from a file name or URL
59
+ */
60
+ export const parseShardNumber = (fnameOrUrl: string): ShardInfo => {
61
+ const matches = fnameOrUrl.match(URL_PARTS_REGEX);
62
+ if (!matches) {
63
+ return {
64
+ baseURL: fnameOrUrl,
65
+ current: 1,
66
+ total: 1,
67
+ };
68
+ } else {
69
+ return {
70
+ baseURL: fnameOrUrl.replace(URL_PARTS_REGEX, ''),
71
+ current: parseInt(matches[1]),
72
+ total: parseInt(matches[2]),
73
+ };
74
+ }
75
+ };
76
+
77
+ /**
78
+ * Parses a model URL and returns an array of URLs based on the following patterns:
79
+ * - If the input URL is an array, it returns the array itself.
80
+ * - If the input URL is a string in the `gguf-split` format, it returns an array containing the URL of each shard in ascending order.
81
+ * - Otherwise, it returns an array containing the input URL as a single element array.
82
+ * @param modelUrl URL or list of URLs
83
+ */
84
+ export const parseModelUrl = (modelUrl: string): string[] => {
85
+ const { baseURL, current, total } = parseShardNumber(modelUrl);
86
+ if (current == total && total == 1) {
87
+ return [modelUrl];
88
+ } else {
89
+ const queryMatch = modelUrl.match(/\.gguf(\?.*)?$/);
90
+ const queryParams = queryMatch?.[1] ?? '';
91
+ const paddedShardIds = Array.from({ length: total }, (_, index) =>
92
+ (index + 1).toString().padStart(5, '0')
93
+ );
94
+ return paddedShardIds.map(
95
+ (current) =>
96
+ `${baseURL}-${current}-of-${total.toString().padStart(5, '0')}.gguf${queryParams}`
97
+ );
98
+ }
99
+ };
100
+
101
+ /**
102
+ * Check if the given blobs are files or not, then sort them by shard number
103
+ */
104
+ export const sortFileByShard = (blobs: Blob[]): void => {
105
+ const isFiles = blobs.every((b) => !!(b as File).name);
106
+ if (isFiles && blobs.length > 1) {
107
+ const files = blobs as File[];
108
+ files.sort((a, b) => {
109
+ const infoA = parseShardNumber(a.name);
110
+ const infoB = parseShardNumber(b.name);
111
+ return infoA.current - infoB.current;
112
+ });
113
+ }
114
+ };
115
+
116
+ export const isMmproj = async (blob: Blob): Promise<boolean> => {
117
+ const META_NAME = 'general.architecture';
118
+ const META_VAL = 'clip';
119
+ const tmp = blob.slice(0, 128 * 1024);
120
+ const header = await tmp.arrayBuffer();
121
+
122
+ const buf = new Uint8Array(header);
123
+ const nameBytes = new TextEncoder().encode(META_NAME);
124
+ const valBytes = new TextEncoder().encode(META_VAL);
125
+
126
+ // Find offset of META_NAME in buffer
127
+ let offset = -1;
128
+ outer: for (let i = 0; i <= buf.length - nameBytes.length; i++) {
129
+ for (let j = 0; j < nameBytes.length; j++) {
130
+ if (buf[i + j] !== nameBytes[j]) continue outer;
131
+ }
132
+ offset = i;
133
+ break;
134
+ }
135
+ if (offset === -1) return false;
136
+
137
+ // Read valLen as uint64 at offset+8*3 (little-endian, read low 32 bits)
138
+ if (offset + 8 * 4 + 4 > buf.length) return false;
139
+ const view = new DataView(header);
140
+ const valLen = view.getBigUint64(offset + 8 * 3, true);
141
+ if (valLen !== 4n) return false;
142
+
143
+ // Read 4 bytes at offset+8*4, compare with META_VAL bytes
144
+ for (let i = 0; i < valBytes.length; i++) {
145
+ if (buf[offset + 8 * 4 + i] !== valBytes[i]) return false;
146
+ }
147
+ return true;
148
+ };
149
+
150
+ export const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
151
+
152
+ export const absoluteUrl = (relativePath: string) =>
153
+ new URL(relativePath, document.baseURI).href;
154
+
155
+ export const padDigits = (number: number, digits: number) => {
156
+ return (
157
+ Array(Math.max(digits - String(number).length + 1, 0)).join('0') + number
158
+ );
159
+ };
160
+
161
+ export const sumArr = (arr: number[]) =>
162
+ arr.reduce((prev, curr) => prev + curr, 0);
163
+
164
+ export const isString = (value: any): boolean => !!value?.startsWith;
165
+
166
+ export const MMPROJ_FILE_NAME = 'mmproj.gguf';
167
+
168
+ type ModelShard = { blob: Blob; name: string };
169
+ export const prepareBlobs = async (
170
+ blobsInp: Blob[]
171
+ ): Promise<{
172
+ llm: ModelShard[];
173
+ mmproj: ModelShard | null;
174
+ all: ModelShard[];
175
+ }> => {
176
+ const blobs: Blob[] = [];
177
+ let blobMmproj: Blob | null = null;
178
+
179
+ for (const blob of blobsInp) {
180
+ if (await isMmproj(blob)) {
181
+ blobMmproj = blob;
182
+ } else {
183
+ blobs.push(blob);
184
+ }
185
+ }
186
+
187
+ // prepare model-XXXXX-of-XXXXX.gguf blobs
188
+ sortFileByShard(blobs);
189
+ const result = blobs.map((blob, i) => ({
190
+ blob,
191
+ name: `model-${padDigits(i + 1, 5)}-of-${padDigits(blobs.length, 5)}.gguf`,
192
+ }));
193
+
194
+ // prepare mmproj.gguf blob
195
+ if (blobMmproj) {
196
+ result.push({
197
+ blob: blobMmproj,
198
+ name: MMPROJ_FILE_NAME,
199
+ });
200
+ }
201
+
202
+ return {
203
+ llm: result.filter((f) => f.name !== MMPROJ_FILE_NAME),
204
+ mmproj: blobMmproj ? { blob: blobMmproj, name: MMPROJ_FILE_NAME } : null,
205
+ all: result,
206
+ };
207
+ };
208
+
209
+ /**
210
+ * Browser feature detection
211
+ * Copied from https://unpkg.com/wasm-feature-detect?module (Apache License)
212
+ */
213
+
214
+ /**
215
+ * @returns true if browser support multi-threads
216
+ */
217
+ export const isSupportMultiThread = () =>
218
+ (async (e) => {
219
+ try {
220
+ return (
221
+ 'undefined' != typeof MessageChannel &&
222
+ new MessageChannel().port1.postMessage(new SharedArrayBuffer(1)),
223
+ WebAssembly.validate(e)
224
+ );
225
+ } catch (e) {
226
+ return !1;
227
+ }
228
+ })(
229
+ new Uint8Array([
230
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 5, 4, 1, 3, 1,
231
+ 1, 10, 11, 1, 9, 0, 65, 0, 254, 16, 2, 0, 26, 11,
232
+ ])
233
+ );
234
+
235
+ /**
236
+ * @returns true if browser support wasm "native" exception handler
237
+ */
238
+ const isSupportExceptions = async () =>
239
+ WebAssembly.validate(
240
+ new Uint8Array([
241
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 8, 1, 6,
242
+ 0, 6, 64, 25, 11, 11,
243
+ ])
244
+ );
245
+
246
+ /**
247
+ * @returns true if browser support wasm SIMD
248
+ */
249
+ const isSupportSIMD = async () =>
250
+ WebAssembly.validate(
251
+ new Uint8Array([
252
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10,
253
+ 1, 8, 0, 65, 0, 253, 15, 253, 98, 11,
254
+ ])
255
+ );
256
+
257
+ /**
258
+ * @returns true if browser support JSPI
259
+ */
260
+ export const isSupportJSPI = () => {
261
+ return !!(WebAssembly as any).Suspending;
262
+ };
263
+
264
+ /**
265
+ * @returns true if brower support WebGPU. Note: for browser without JSPI support, compat mode will be used.
266
+ */
267
+ export const isSupportWebGPU = () => {
268
+ return !!(navigator as any).gpu;
269
+ };
270
+
271
+ /**
272
+ * @returns true if browser support WASM Memory64
273
+ */
274
+ export const isSupportMem64 = (): boolean => {
275
+ try {
276
+ new WebAssembly.Memory({
277
+ address: 'i64',
278
+ initial: 1n, // 1 page (64 KiB)
279
+ } as any);
280
+ return true;
281
+ } catch {
282
+ return false;
283
+ }
284
+ };
285
+
286
+ /**
287
+ * Throws an error if the environment is not compatible
288
+ */
289
+ export const checkEnvironmentCompatible = async (): Promise<void> => {
290
+ if (!(await isSupportExceptions())) {
291
+ throw new Error('WebAssembly runtime does not support exception handling');
292
+ }
293
+ if (!(await isSupportSIMD())) {
294
+ throw new Error('WebAssembly runtime does not support SIMD');
295
+ }
296
+ };
297
+
298
+ /**
299
+ * Check if browser is Safari
300
+ * Source: https://github.com/DamonOehlman/detect-browser/blob/master/src/index.ts
301
+ */
302
+ export const isSafari = (): boolean => {
303
+ return (
304
+ isSafariMobile() ||
305
+ !!navigator.userAgent.match(/Version\/([0-9\._]+).*Safari/)
306
+ ); // safari
307
+ };
308
+
309
+ /**
310
+ * Check if browser is Firefox
311
+ */
312
+ export const isFirefox = (): boolean => {
313
+ return !!navigator.userAgent.match(/Firefox\/([0-9\.]+)(?:\s|$)/);
314
+ };
315
+
316
+ /**
317
+ * Regular expression to validate GGUF file paths/URLs
318
+ * Matches paths ending with .gguf and optional query parameters
319
+ */
320
+ export const GGUF_FILE_REGEX = /^.*\.gguf(?:\?.*)?$/;
321
+
322
+ /**
323
+ * Validates if a given string is a valid GGUF file path/URL
324
+ * @param path The file path or URL to validate
325
+ * @returns true if the path is a valid GGUF file path/URL
326
+ */
327
+ export const isValidGgufFile = (path: string): boolean => {
328
+ return GGUF_FILE_REGEX.test(path);
329
+ };
330
+
331
+ /**
332
+ * Check if browser is Safari iOS / iPad / iPhone
333
+ * Source: https://github.com/DamonOehlman/detect-browser/blob/master/src/index.ts
334
+ */
335
+ export const isSafariMobile = (): boolean => {
336
+ return !!navigator.userAgent.match(/Version\/([0-9\._]+).*Mobile.*Safari.*/); // ios
337
+ };
338
+
339
+ /**
340
+ * Create a worker from a string
341
+ */
342
+ export const createWorker = (workerCode: string | Blob): Worker => {
343
+ const workerURL = URL.createObjectURL(
344
+ isString(workerCode)
345
+ ? new Blob([workerCode], { type: 'text/javascript' })
346
+ : (workerCode as Blob)
347
+ );
348
+ return new Worker(workerURL, { type: 'module' });
349
+ };
350
+
351
+ /**
352
+ * Convert callback to async iterator
353
+ */
354
+ export const cbToAsyncIter =
355
+ <A extends any[], T>(
356
+ fn: (
357
+ ...args: [
358
+ ...args: A,
359
+ callback: (val?: T, done?: boolean, err?: Error) => void,
360
+ ]
361
+ ) => void
362
+ ) =>
363
+ (...args: A): AsyncIterable<T> => {
364
+ let values: Promise<[T, boolean]>[] = [];
365
+ let resolve: (x: [T, boolean]) => void;
366
+ let reject: (e: Error) => void;
367
+ values.push(
368
+ new Promise((res, rej) => {
369
+ resolve = res;
370
+ reject = rej;
371
+ })
372
+ );
373
+ fn(...args, (val?: T, done?: boolean, err?: Error) => {
374
+ if (err) {
375
+ reject(err);
376
+ return;
377
+ }
378
+ resolve([val!, done!]);
379
+ values.push(
380
+ new Promise((res, rej) => {
381
+ resolve = res;
382
+ reject = rej;
383
+ })
384
+ );
385
+ });
386
+ return (async function* () {
387
+ let val: T;
388
+ for (let i = 0, done = false; !done; i++) {
389
+ [val, done] = await values[i];
390
+ delete values[i];
391
+ if (val !== undefined) yield val;
392
+ }
393
+ })();
394
+ };
395
+
396
+ /**
397
+ * Check if we can use async file read, where the wasm env can asynchronously read a Blob.
398
+ * Please refer to README-dev.md for more details.
399
+ */
400
+ export const canUseAsyncFileRead = (compat: boolean) =>
401
+ isSupportJSPI() || compat;
402
+
403
+ export const needCompat = () => !isSupportJSPI() || !isSupportMem64();