@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,358 @@
1
+ import { getHFFileSHA256 } from './huggingface';
2
+ import type { DownloadProgressCallback } from './model-manager';
3
+ import { COSBackend } from './storage/cos';
4
+ import type { StorageBackend, StorageFileHint } from './storage/index';
5
+
6
+ const PREFIX_METADATA = '__metadata__';
7
+
8
+ export type DownloadOptions = {
9
+ /**
10
+ * Callback function to track download progress
11
+ */
12
+ progressCallback?: DownloadProgressCallback;
13
+ /**
14
+ * Additional metadata to be stored with the downloaded file
15
+ */
16
+ metadataAdditional?: Record<string, any>;
17
+ /**
18
+ * Custom headers for the request. Useful for authentication (e.g. Bearer token)
19
+ */
20
+ headers?: Record<string, string>;
21
+ /**
22
+ * Abort signal for the request
23
+ */
24
+ signal?: AbortSignal;
25
+ };
26
+
27
+ // To prevent breaking change, we fill etag with a pre-defined value
28
+ export const POLYFILL_ETAG = 'polyfill_for_older_version';
29
+
30
+ export interface CacheEntry {
31
+ /**
32
+ * Storage key for this file, in the format: `${hashSHA1(fullURL)}_${fileName}`
33
+ */
34
+ name: string;
35
+ /**
36
+ * Size of file (in bytes)
37
+ */
38
+ size: number;
39
+ /**
40
+ * Other metadata
41
+ */
42
+ metadata: CacheEntryMetadata;
43
+ }
44
+
45
+ export interface CacheEntryMetadata {
46
+ /**
47
+ * ETag header from remote request
48
+ * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
49
+ */
50
+ etag: string;
51
+ /**
52
+ * Remote file size (in bytes), used for integrity check
53
+ */
54
+ originalSize: number;
55
+ /**
56
+ * Original URL of the remote model. Unused for now
57
+ */
58
+ originalURL: string;
59
+ /**
60
+ * URL to mmproj file, if exists
61
+ */
62
+ mmprojURL?: string | undefined;
63
+ /**
64
+ * Optional SHA256, mostly used by COS backend
65
+ */
66
+ sha256?: string | undefined;
67
+ }
68
+
69
+ function hintFromMetadata(
70
+ metadata: CacheEntryMetadata | null
71
+ ): StorageFileHint | undefined {
72
+ if (!metadata) return undefined;
73
+ if (metadata.sha256) return { sha256: metadata.sha256 };
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * Manages cached model files, backed by a pluggable StorageBackend.
79
+ *
80
+ * Defaults to OPFS (Origin Private File System).
81
+ */
82
+ export class CacheManager {
83
+ private sb: StorageBackend;
84
+
85
+ /**
86
+ * @param backends Array of storage backends to use, in order of preference ; if first is available, use it, otherwise try the next one.
87
+ */
88
+ constructor(backends: StorageBackend[] = [new COSBackend()]) {
89
+ for (const backend of backends) {
90
+ if (backend.isSupported()) {
91
+ this.sb = backend;
92
+ return;
93
+ }
94
+ }
95
+ throw new Error('No supported storage backend found');
96
+ }
97
+
98
+ /**
99
+ * Convert a given URL into a storage key.
100
+ *
101
+ * Format: `${hashSHA1(fullURL)}_${fileName}`
102
+ */
103
+ async getNameFromURL(url: string): Promise<string> {
104
+ return urlToFileName(url, '');
105
+ }
106
+
107
+ /**
108
+ * @deprecated Use `download()` instead
109
+ *
110
+ * Write a new file to cache. This will overwrite existing file.
111
+ *
112
+ * @param name The file name returned by `getNameFromURL()` or `list()`
113
+ */
114
+ async write(
115
+ name: string,
116
+ stream: ReadableStream,
117
+ metadata: CacheEntryMetadata
118
+ ): Promise<void> {
119
+ // write file first, then metadata
120
+ await this.sb.write(name, stream);
121
+ await this.writeMetadata(name, metadata);
122
+ }
123
+
124
+ async download(url: string, options: DownloadOptions = {}): Promise<void> {
125
+ const fileKey = await urlToFileName(url, '');
126
+
127
+ // Fetch sha256 before the GET so we can skip the download entirely if the
128
+ // file is already in COS (avoids opening a connection just to cancel it).
129
+ const sha256 = await getHFFileSHA256(url, options.headers ?? {});
130
+ const hint = sha256 ? { sha256 } : undefined;
131
+
132
+ if (hint && (await this.sb.getSize(fileKey, hint)) !== -1) {
133
+ // File already in COS. Metadata is origin-local (OPFS), so it may be
134
+ // absent on a different origin or after a crash between write and
135
+ // writeMetadata. Ensure it exists before returning.
136
+ if (!(await this.getMetadata(fileKey))) {
137
+ const head = await fetch(url, {
138
+ method: 'HEAD',
139
+ ...(options.headers ? { headers: options.headers } : {}),
140
+ });
141
+ const contentLength = head.headers.get('content-length');
142
+ const etag = (head.headers.get('etag') || '').replace(
143
+ /[^A-Za-z0-9]/g,
144
+ ''
145
+ );
146
+ await this.writeMetadata(fileKey, {
147
+ originalURL: url,
148
+ originalSize: parseInt(contentLength ?? '0', 10),
149
+ etag,
150
+ sha256,
151
+ ...(options.metadataAdditional ?? {}),
152
+ });
153
+ }
154
+ return;
155
+ }
156
+
157
+ const response = await fetch(url, {
158
+ ...(options.headers ? { headers: options.headers } : {}),
159
+ ...(options.signal ? { signal: options.signal } : {}),
160
+ });
161
+
162
+ if (!response.ok || !response.body) {
163
+ throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
164
+ }
165
+
166
+ const contentLength = response.headers.get('content-length');
167
+ const etag = (response.headers.get('etag') || '').replace(
168
+ /[^A-Za-z0-9]/g,
169
+ ''
170
+ );
171
+ const total = parseInt(contentLength ?? '0', 10);
172
+
173
+ const progressCallback = options.progressCallback;
174
+ let loaded = 0;
175
+ let lastProgressAt = 0;
176
+
177
+ const progressStream = new TransformStream<Uint8Array, Uint8Array>({
178
+ transform(chunk, controller) {
179
+ loaded += chunk.byteLength;
180
+ if (progressCallback) {
181
+ const now = Date.now();
182
+ if (now - lastProgressAt > 100) {
183
+ lastProgressAt = now;
184
+ progressCallback({ loaded, total });
185
+ }
186
+ }
187
+ controller.enqueue(chunk);
188
+ },
189
+ flush() {
190
+ progressCallback?.({ loaded, total: total || loaded });
191
+ },
192
+ });
193
+
194
+ const metadata: CacheEntryMetadata = {
195
+ originalURL: url,
196
+ originalSize: total,
197
+ etag,
198
+ ...(options.metadataAdditional ?? {}),
199
+ };
200
+ if (sha256) {
201
+ metadata.sha256 = sha256;
202
+ }
203
+
204
+ await this.sb.write(
205
+ fileKey,
206
+ response.body.pipeThrough(progressStream),
207
+ hint
208
+ );
209
+ await this.writeMetadata(fileKey, metadata);
210
+ }
211
+
212
+ /**
213
+ * Open a file in cache for reading
214
+ *
215
+ * @param nameOrURL The file name returned by `getNameFromURL()` or `list()`, or the original URL of the remote file
216
+ * @returns Blob, or null if file does not exist
217
+ */
218
+ async open(nameOrURL: string): Promise<Blob | null> {
219
+ const hint1 = hintFromMetadata(await this.getMetadata(nameOrURL));
220
+ const direct = await this.sb.read(nameOrURL, hint1);
221
+ if (direct) return direct;
222
+ // also accept the original URL
223
+ const key = await urlToFileName(nameOrURL, '');
224
+ const hint2 = hintFromMetadata(await this.getMetadata(key));
225
+ return this.sb.read(key, hint2);
226
+ }
227
+
228
+ /**
229
+ * Get the size of a file in stored cache
230
+ *
231
+ * NOTE: in case the download is stopped mid-way (i.e. user close browser tab), the file maybe corrupted, size maybe different from `metadata.originalSize`
232
+ *
233
+ * @param name The file name returned by `getNameFromURL()` or `list()`
234
+ * @returns number of bytes, or -1 if file does not exist
235
+ */
236
+ async getSize(name: string): Promise<number> {
237
+ const hint = hintFromMetadata(await this.getMetadata(name));
238
+ return this.sb.getSize(name, hint);
239
+ }
240
+
241
+ /**
242
+ * Get metadata of a cached file
243
+ */
244
+ async getMetadata(name: string): Promise<CacheEntryMetadata | null> {
245
+ const blob = await this.sb.read(`${PREFIX_METADATA}${name}`);
246
+ const cachedSize = await this.sb.getSize(name);
247
+ if (!blob) {
248
+ return cachedSize > 0
249
+ ? // files created by older version of wllama don't have metadata; polyfill it
250
+ {
251
+ etag: POLYFILL_ETAG,
252
+ originalSize: cachedSize,
253
+ originalURL: '',
254
+ }
255
+ : // cached file not found
256
+ null;
257
+ }
258
+ try {
259
+ return await new Response(blob).json();
260
+ } catch (e) {
261
+ // metadata corrupted; caller will re-download
262
+ return null;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * List all files currently in cache
268
+ */
269
+ async list(): Promise<CacheEntry[]> {
270
+ const all = await this.sb.list();
271
+ const metadataMap: Record<string, CacheEntryMetadata> = {};
272
+
273
+ for (const { key } of all) {
274
+ if (key.startsWith(PREFIX_METADATA)) {
275
+ const blob = await this.sb.read(key);
276
+ if (blob) {
277
+ const meta = await new Response(blob).json().catch(() => null);
278
+ metadataMap[key.slice(PREFIX_METADATA.length)] = meta;
279
+ }
280
+ }
281
+ }
282
+
283
+ const result: CacheEntry[] = [];
284
+ for (const { key, size } of all) {
285
+ if (!key.startsWith(PREFIX_METADATA)) {
286
+ result.push({
287
+ name: key,
288
+ size,
289
+ metadata: metadataMap[key] || {
290
+ originalSize: size,
291
+ originalURL: '',
292
+ etag: '',
293
+ },
294
+ });
295
+ }
296
+ }
297
+ return result;
298
+ }
299
+
300
+ /**
301
+ * Clear all files currently in cache
302
+ */
303
+ async clear(): Promise<void> {
304
+ await this.deleteMany(() => true);
305
+ }
306
+
307
+ /**
308
+ * Delete a single file in cache
309
+ *
310
+ * @param nameOrURL Can be either an URL or a name returned by `getNameFromURL()` or `list()`
311
+ */
312
+ async delete(nameOrURL: string): Promise<void> {
313
+ const name2 = await this.getNameFromURL(nameOrURL);
314
+ await this.deleteMany(
315
+ (entry) => entry.name === nameOrURL || entry.name === name2
316
+ );
317
+ }
318
+
319
+ /**
320
+ * Delete multiple files in cache.
321
+ *
322
+ * @param predicate A predicate like `array.filter(item => boolean)`
323
+ */
324
+ async deleteMany(predicate: (e: CacheEntry) => boolean): Promise<void> {
325
+ const list = await this.list();
326
+ for (const item of list) {
327
+ if (predicate(item)) {
328
+ await this.sb.delete(item.name);
329
+ await this.sb.delete(`${PREFIX_METADATA}${item.name}`);
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Write the metadata of the file to disk.
336
+ */
337
+ async writeMetadata(
338
+ name: string,
339
+ metadata: CacheEntryMetadata
340
+ ): Promise<void> {
341
+ const blob = new Blob([JSON.stringify(metadata)], { type: 'text/plain' });
342
+ await this.sb.write(`${PREFIX_METADATA}${name}`, blob.stream());
343
+ }
344
+ }
345
+
346
+ export default CacheManager;
347
+
348
+ async function urlToFileName(url: string, prefix: string): Promise<string> {
349
+ const hashBuffer = await crypto.subtle.digest(
350
+ 'SHA-1',
351
+ new TextEncoder().encode(url)
352
+ );
353
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
354
+ const hashHex = hashArray
355
+ .map((b) => b.toString(16).padStart(2, '0'))
356
+ .join('');
357
+ return `${prefix}${hashHex}_${url.split('/').pop()}`;
358
+ }
package/src/debug.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { WASM_SOURCE_MAP } from './wasm/source-map';
2
+
3
+ interface DecodedMap {
4
+ firstId: number;
5
+ funcNames: (string | null)[]; // indexed by (funcId - firstId)
6
+ }
7
+
8
+ const cache = new Map<string, DecodedMap>();
9
+
10
+ async function loadMap(buildKey: string): Promise<DecodedMap> {
11
+ if (cache.has(buildKey)) return cache.get(buildKey)!;
12
+
13
+ const b64 = WASM_SOURCE_MAP[buildKey];
14
+ if (!b64) throw new Error(`No source map for build "${buildKey}"`);
15
+
16
+ const gzipped = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
17
+ const ds = new DecompressionStream('gzip');
18
+ const writer = ds.writable.getWriter();
19
+ writer.write(gzipped);
20
+ writer.close();
21
+ const buf = await new Response(ds.readable).arrayBuffer();
22
+ const dv = new DataView(buf);
23
+ const bytes = new Uint8Array(buf);
24
+
25
+ const firstId = dv.getUint32(0, true);
26
+ const funcCount = dv.getUint32(4, true);
27
+ const numNames = dv.getUint32(8, true);
28
+
29
+ // Read name table
30
+ const td = new TextDecoder();
31
+ const names: string[] = [];
32
+ let pos = 12;
33
+ for (let i = 0; i < numNames; i++) {
34
+ const len = bytes[pos++];
35
+ names.push(td.decode(bytes.subarray(pos, pos + len)));
36
+ pos += len;
37
+ }
38
+
39
+ // Read u16 index array
40
+ const funcNames: (string | null)[] = [];
41
+ for (let i = 0; i < funcCount; i++) {
42
+ const idx = dv.getUint16(pos, true);
43
+ pos += 2;
44
+ funcNames.push(idx === 0xffff ? null : names[idx]);
45
+ }
46
+
47
+ const entry: DecodedMap = { firstId, funcNames };
48
+ cache.set(buildKey, entry);
49
+ return entry;
50
+ }
51
+
52
+ export const Debug = {
53
+ /**
54
+ * Resolves a list of wasm function indices to their cleaned symbol names.
55
+ */
56
+ decodeFuncIds: async (
57
+ funcIds: number[],
58
+ isCompatBuild: boolean
59
+ ): Promise<{ funcId: number; name: string }[]> => {
60
+ const buildKey = isCompatBuild ? 'compat' : 'default';
61
+ const { firstId, funcNames } = await loadMap(buildKey);
62
+ return funcIds.map((funcId) => {
63
+ const i = funcId - firstId;
64
+ const name =
65
+ i >= 0 && i < funcNames.length && funcNames[i]
66
+ ? funcNames[i]!
67
+ : '(unknown)';
68
+ return { funcId, name };
69
+ });
70
+ },
71
+ /**
72
+ * Annotates a wasm stack trace string with resolved function names.
73
+ *
74
+ * Example input from Chrome:
75
+ * at http://localhost:8080/esm/wasm/wllama.wasm:wasm-function[775]:0x74251
76
+ * at async blob:http://localhost:8080/53a863cc-7227-45cc-8594-ddbbf5257f20:317:28
77
+ *
78
+ * Example input from Firefox:
79
+ * @http://localhost:8080/esm/wasm/wllama.wasm:wasm-function[796]:0x7dfe2
80
+ * at wModuleInit/WebAssembly.promising/< (9b6a2acd-d909-44e2-b021-d42fb9087cfb:15:32) index.js:1433:45
81
+ *
82
+ * Example input from Safari:
83
+ * 2441@wasm-function[2441]
84
+ * at wrapper (d746f19e-4523-4f36-ba06-d0969acc0b05:22:126009)
85
+ *
86
+ * Example output:
87
+ * wasm-func[775] (server_response::send)
88
+ */
89
+ decodeStackTrace: async (
90
+ stack: string,
91
+ isCompatBuild: boolean
92
+ ): Promise<string> => {
93
+ // match wasm-function[N] from Chrome, Firefox and Safari stack formats
94
+ const re = /wasm-function\[(\d+)\]/g;
95
+ const funcIds = [
96
+ ...new Set([...stack.matchAll(re)].map((m) => parseInt(m[1]))),
97
+ ];
98
+ if (funcIds.length === 0) return stack;
99
+
100
+ const resolved = await Debug.decodeFuncIds(funcIds, isCompatBuild);
101
+
102
+ return resolved
103
+ .map((r) => {
104
+ if (r.name === '(unknown)') {
105
+ return ` wasm-func[${r.funcId}] (unknown)`;
106
+ }
107
+ return ` wasm-func[${r.funcId}] (${r.name})`;
108
+ })
109
+ .join('\n');
110
+ },
111
+ };