kjarni-wasm 0.1.4 → 0.1.5

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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # kjarni-wasm
2
2
 
3
- Local AI inference in the browser. Embeddings, classification, reranking and chat,
4
- running in a Web Worker so the page never freezes.
3
+ Local AI inference in the browser. Embeddings, classification, reranking, semantic
4
+ search and chat, running in a Web Worker so the page never freezes.
5
5
 
6
6
  ```bash
7
7
  npm i kjarni-wasm
@@ -20,7 +20,9 @@ Or without a bundler, straight from a CDN:
20
20
  ```ts
21
21
  import { Kjarni } from "kjarni-wasm";
22
22
 
23
- const kjarni = await Kjarni.load({ encoder: "/models/minilm-l6-v2-q8.kjq" });
23
+ const kjarni = await Kjarni.load({
24
+ encoder: "https://huggingface.co/olafuraron/all-MiniLM-L6-v2-q8/resolve/main/all-MiniLM-L6-v2-q8.kjq",
25
+ });
24
26
 
25
27
  const [doctor, physician] = await kjarni.encode(["doctor", "physician"]);
26
28
  console.log(Kjarni.cosine(doctor, physician)); // 0.8598
@@ -29,6 +31,8 @@ console.log(Kjarni.cosine(doctor, physician)); // 0.8598
29
31
  console.log(await kjarni.similarity("doctor", "physician"));
30
32
  ```
31
33
 
34
+ That URL is a real, public 23 MB file. It downloads once, the browser caches it, and nothing else leaves the machine. See [Models](#models) for the rest, and serve them from your own origin in production rather than hotlinking.
35
+
32
36
  Every method returns a promise and the work happens on a worker thread. A model
33
37
  loads or a reply generates while the page keeps painting.
34
38
 
@@ -48,9 +52,9 @@ Models are `.kjq` files: config, tokenizer and int8 weights in one download.
48
52
 
49
53
  ```ts
50
54
  const kjarni = await Kjarni.load({
51
- encoder: "/models/minilm-l6-v2-q8.kjq",
52
- classifier: "/models/distilbert-sentiment-q8.kjq",
53
- reranker: "/models/ms-marco-MiniLM-L-6-v2-q8.kjq",
55
+ encoder: model("all-MiniLM-L6-v2-q8"),
56
+ classifier: model("distilbert-sentiment-q8"),
57
+ reranker: model("ms-marco-MiniLM-L-6-v2-q8"),
54
58
  onProgress: (capability, loaded, total) => {
55
59
  console.log(`${capability}: ${Math.round((loaded / total) * 100)}%`);
56
60
  },
@@ -85,22 +89,32 @@ for (const { index, score } of ranked) console.log(score, documents[index]);
85
89
 
86
90
  ## Chat
87
91
 
92
+ A 0.5B language model runs in the tab, and tokens arrive as they are decoded.
93
+
88
94
  ```ts
89
95
  const kjarni = await Kjarni.load({
90
- chat: "/models/qwen05b-q8.kjq",
96
+ chat: model("qwen05b-q8"),
91
97
  chatModelId: "qwen2.5-0.5b-instruct",
92
98
  });
93
99
 
94
- for await (const piece of kjarni.chat("Explain RAG in one sentence.")) {
95
- output.textContent += piece;
96
- }
100
+ let out = "";
101
+ for await (const piece of kjarni.chat("Explain RAG in one sentence.")) out += piece;
97
102
  ```
98
103
 
99
- Tokens arrive as they are decoded, not in one lump at the end. The worker is what
100
- makes that useful: generation occupies its thread completely, so the same call on
101
- the main thread would freeze the tab until the reply finished.
104
+ This works because the weights stay block-quantised in memory. wasm32 caps a
105
+ single allocation at `isize::MAX`, which is 2 GB, and Qwen2.5 0.5B expanded to f32
106
+ needs 1.98 GB, so a loader that dequantises up front traps on load. The `.kjq`
107
+ container carries block-quantised weights the engine reads in place, which is
108
+ 500 MB rather than 1.98 GB, and decodes faster than f32 rather than slower.
109
+
110
+ Expect a few tokens per second on a laptop CPU. Run it in a Web Worker: generation
111
+ occupies the thread until it returns, so on the main thread the tab stops painting
112
+ for the whole reply.
102
113
 
103
- `temperature: 0` selects greedy decoding, which is deterministic.
114
+ `chat()` completes a prompt; it does not keep turns. Multi-turn conversation, where
115
+ the model's own chat template is applied and earlier turns are remembered, exists
116
+ on `WasmChat` (`send_stream`, `clear_history`) and is not yet surfaced through this
117
+ wrapper.
104
118
 
105
119
  ## Cleaning up
106
120
 
@@ -116,6 +130,45 @@ reference leaks the weights. `close()` frees the models and stops the worker.
116
130
  A browser with WebAssembly and module workers: Chrome and Edge 80+, Firefox 114+,
117
131
  Safari 15+. No WebGPU required, inference runs on the CPU.
118
132
 
133
+ ## Models
134
+
135
+ `.kjq` is a single file holding config, tokenizer and int8 weights. These are
136
+ public and hosted on Hugging Face, one repository per model, with the repository
137
+ and file names matching:
138
+
139
+ ```ts
140
+ const model = (name: string) =>
141
+ `https://huggingface.co/olafuraron/${name}/resolve/main/${name}.kjq`;
142
+ ```
143
+
144
+ | Pass as | Name | Size | What it does |
145
+ | ------- | ---- | ---- | ------------ |
146
+ | `encoder` | `all-MiniLM-L6-v2-q8` | 23 MB | embeddings, similarity, search |
147
+ | `reranker` | `ms-marco-MiniLM-L-6-v2-q8` | 23 MB | cross-encoder reranking |
148
+ | `classifier` | `distilbert-sentiment-q8` | 68 MB | positive / negative sentiment |
149
+ | `chat` | `qwen05b-q8` | 508 MB | a local language model, block-quantised so it fits |
150
+
151
+ Hugging Face serves these with permissive CORS, so a browser can fetch them from
152
+ any origin. For production, copy what you need and serve it from your own origin:
153
+ you control caching, and your page does not depend on someone else's uptime.
154
+
155
+ Load only what you use, and wire `onProgress` to something visible: these are real
156
+ downloads, and a silent minute reads as a broken page.
157
+
158
+ ## The same engine elsewhere
159
+
160
+ Kjarni is one Rust engine behind several packages. The browser is one target; if
161
+ you need the server half of the same product:
162
+
163
+ - **[Live demo](https://kjarni.ai/demo/)**: this package, running in a page
164
+ - **[Kjarni](https://www.nuget.org/packages/Kjarni)** and
165
+ **[Kjarni.Extensions.AI](https://www.nuget.org/packages/Kjarni.Extensions.AI)**
166
+ on NuGet for C#, including `IEmbeddingGenerator` and `IChatClient`
167
+ - **[Go](https://pkg.go.dev/github.com/olafurjohannsson/kjarni-go)**,
168
+ **[C++](https://github.com/olafurjohannsson/kjarni/tree/main/crates/kjarni-ffi/examples/cpp)**
169
+ and a CLI that reads stdin and writes JSON
170
+ - **[Source and issues](https://github.com/olafurjohannsson/kjarni)**
171
+
119
172
  ## Making your own `.kjq`
120
173
 
121
174
  See [the format guide](../scripts/README.md). Any BERT-family encoder,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kjarni-wasm",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Local AI inference in the browser: embeddings, classification, reranking, semantic search and chat, running in a Web Worker.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,6 +18,10 @@ export class WasmChat {
18
18
  private constructor();
19
19
  free(): void;
20
20
  [Symbol.dispose](): void;
21
+ /**
22
+ * Drops the conversation, keeping the system prompt.
23
+ */
24
+ clear_history(): void;
21
25
  /**
22
26
  * The model's context window, in tokens.
23
27
  */
@@ -41,6 +45,10 @@ export class WasmChat {
41
45
  * itself is in `generate_stream_core`, which builds and is tested natively.
42
46
  */
43
47
  generate_stream(prompt: string, max_new_tokens: number, temperature: number, on_token: Function): string;
48
+ /**
49
+ * Number of messages held, system prompt included.
50
+ */
51
+ history_len(): number;
44
52
  /**
45
53
  * Load a decoder model from a `.kjq` container.
46
54
  *
@@ -48,6 +56,18 @@ export class WasmChat {
48
56
  * one the model still generates, but its turn markers will not be applied.
49
57
  */
50
58
  static load(data: Uint8Array, model_id?: string | null): WasmChat;
59
+ /**
60
+ * Sends a message in an ongoing conversation, streaming the reply.
61
+ *
62
+ * Unlike `generate_stream`, which completes raw text, this keeps the turns so
63
+ * far and formats them with the model's own chat template, so an instruct
64
+ * model answers instead of continuing the sentence. The reply is appended to
65
+ * the history, so the next call sees it.
66
+ *
67
+ * Browser-only: `js_sys::Function` has no native equivalent. The work is in
68
+ * `send_stream_core`, which builds and is tested natively.
69
+ */
70
+ send_stream(message: string, max_new_tokens: number, temperature: number, on_token: Function): string;
51
71
  }
52
72
 
53
73
  /**
@@ -190,10 +210,13 @@ export interface InitOutput {
190
210
  readonly __wbg_wasmindexbuilder_free: (a: number, b: number) => void;
191
211
  readonly __wbg_wasmmodel_free: (a: number, b: number) => void;
192
212
  readonly set_debug_logging: (a: number) => void;
213
+ readonly wasmchat_clear_history: (a: number) => void;
193
214
  readonly wasmchat_context_size: (a: number) => number;
194
215
  readonly wasmchat_generate: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
195
216
  readonly wasmchat_generate_stream: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
217
+ readonly wasmchat_history_len: (a: number) => number;
196
218
  readonly wasmchat_load: (a: number, b: number, c: number, d: number, e: number) => void;
219
+ readonly wasmchat_send_stream: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
197
220
  readonly wasmclassifier_classify: (a: number, b: number, c: number, d: number) => void;
198
221
  readonly wasmclassifier_classify_batch: (a: number, b: number, c: number, d: number) => void;
199
222
  readonly wasmclassifier_labels: (a: number, b: number) => void;
@@ -225,8 +248,8 @@ export interface InitOutput {
225
248
  readonly wasmsearch_add_chunk: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
226
249
  readonly __wbg_wasmreranker_free: (a: number, b: number) => void;
227
250
  readonly __wbg_wasmsearch_free: (a: number, b: number) => void;
228
- readonly __wasm_bindgen_func_elem_5410: (a: number, b: number, c: number, d: number) => void;
229
- readonly __wasm_bindgen_func_elem_5426: (a: number, b: number, c: number, d: number) => void;
251
+ readonly __wasm_bindgen_func_elem_5449: (a: number, b: number, c: number, d: number) => void;
252
+ readonly __wasm_bindgen_func_elem_5465: (a: number, b: number, c: number, d: number) => void;
230
253
  readonly __wbindgen_export: (a: number, b: number) => number;
231
254
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
232
255
  readonly __wbindgen_export3: (a: number) => void;
@@ -30,6 +30,12 @@ export class WasmChat {
30
30
  const ptr = this.__destroy_into_raw();
31
31
  wasm.__wbg_wasmchat_free(ptr, 0);
32
32
  }
33
+ /**
34
+ * Drops the conversation, keeping the system prompt.
35
+ */
36
+ clear_history() {
37
+ wasm.wasmchat_clear_history(this.__wbg_ptr);
38
+ }
33
39
  /**
34
40
  * The model's context window, in tokens.
35
41
  * @returns {number}
@@ -117,6 +123,14 @@ export class WasmChat {
117
123
  wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1);
118
124
  }
119
125
  }
126
+ /**
127
+ * Number of messages held, system prompt included.
128
+ * @returns {number}
129
+ */
130
+ history_len() {
131
+ const ret = wasm.wasmchat_history_len(this.__wbg_ptr);
132
+ return ret >>> 0;
133
+ }
120
134
  /**
121
135
  * Load a decoder model from a `.kjq` container.
122
136
  *
@@ -145,6 +159,49 @@ export class WasmChat {
145
159
  wasm.__wbindgen_add_to_stack_pointer(16);
146
160
  }
147
161
  }
162
+ /**
163
+ * Sends a message in an ongoing conversation, streaming the reply.
164
+ *
165
+ * Unlike `generate_stream`, which completes raw text, this keeps the turns so
166
+ * far and formats them with the model's own chat template, so an instruct
167
+ * model answers instead of continuing the sentence. The reply is appended to
168
+ * the history, so the next call sees it.
169
+ *
170
+ * Browser-only: `js_sys::Function` has no native equivalent. The work is in
171
+ * `send_stream_core`, which builds and is tested natively.
172
+ * @param {string} message
173
+ * @param {number} max_new_tokens
174
+ * @param {number} temperature
175
+ * @param {Function} on_token
176
+ * @returns {string}
177
+ */
178
+ send_stream(message, max_new_tokens, temperature, on_token) {
179
+ let deferred3_0;
180
+ let deferred3_1;
181
+ try {
182
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
183
+ const ptr0 = passStringToWasm0(message, wasm.__wbindgen_export, wasm.__wbindgen_export2);
184
+ const len0 = WASM_VECTOR_LEN;
185
+ wasm.wasmchat_send_stream(retptr, this.__wbg_ptr, ptr0, len0, max_new_tokens, temperature, addBorrowedObject(on_token));
186
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
187
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
188
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
189
+ var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
190
+ var ptr2 = r0;
191
+ var len2 = r1;
192
+ if (r3) {
193
+ ptr2 = 0; len2 = 0;
194
+ throw takeObject(r2);
195
+ }
196
+ deferred3_0 = ptr2;
197
+ deferred3_1 = len2;
198
+ return getStringFromWasm0(ptr2, len2);
199
+ } finally {
200
+ wasm.__wbindgen_add_to_stack_pointer(16);
201
+ heap[stack_pointer++] = undefined;
202
+ wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1);
203
+ }
204
+ }
148
205
  }
149
206
  if (Symbol.dispose) WasmChat.prototype[Symbol.dispose] = WasmChat.prototype.free;
150
207
 
@@ -1035,7 +1092,7 @@ function __wbg_get_imports() {
1035
1092
  const a = state0.a;
1036
1093
  state0.a = 0;
1037
1094
  try {
1038
- return __wasm_bindgen_func_elem_5426(a, state0.b, arg0, arg1);
1095
+ return __wasm_bindgen_func_elem_5465(a, state0.b, arg0, arg1);
1039
1096
  } finally {
1040
1097
  state0.a = a;
1041
1098
  }
@@ -1058,6 +1115,14 @@ function __wbg_get_imports() {
1058
1115
  const ret = Date.now();
1059
1116
  return ret;
1060
1117
  },
1118
+ __wbg_now_e7c6795a7f81e10f: function(arg0) {
1119
+ const ret = getObject(arg0).now();
1120
+ return ret;
1121
+ },
1122
+ __wbg_performance_3fcf6e32a7e1ed0a: function(arg0) {
1123
+ const ret = getObject(arg0).performance;
1124
+ return addHeapObject(ret);
1125
+ },
1061
1126
  __wbg_process_44c7a14e11e9f69e: function(arg0) {
1062
1127
  const ret = getObject(arg0).process;
1063
1128
  return addHeapObject(ret);
@@ -1141,8 +1206,8 @@ function __wbg_get_imports() {
1141
1206
  return addHeapObject(ret);
1142
1207
  },
1143
1208
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
1144
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 2036, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1145
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_5410);
1209
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 2043, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1210
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_5449);
1146
1211
  return addHeapObject(ret);
1147
1212
  },
1148
1213
  __wbindgen_cast_0000000000000002: function(arg0) {
@@ -1179,10 +1244,10 @@ function __wbg_get_imports() {
1179
1244
  };
1180
1245
  }
1181
1246
 
1182
- function __wasm_bindgen_func_elem_5410(arg0, arg1, arg2) {
1247
+ function __wasm_bindgen_func_elem_5449(arg0, arg1, arg2) {
1183
1248
  try {
1184
1249
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1185
- wasm.__wasm_bindgen_func_elem_5410(retptr, arg0, arg1, addHeapObject(arg2));
1250
+ wasm.__wasm_bindgen_func_elem_5449(retptr, arg0, arg1, addHeapObject(arg2));
1186
1251
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1187
1252
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1188
1253
  if (r1) {
@@ -1193,8 +1258,8 @@ function __wasm_bindgen_func_elem_5410(arg0, arg1, arg2) {
1193
1258
  }
1194
1259
  }
1195
1260
 
1196
- function __wasm_bindgen_func_elem_5426(arg0, arg1, arg2, arg3) {
1197
- wasm.__wasm_bindgen_func_elem_5426(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
1261
+ function __wasm_bindgen_func_elem_5465(arg0, arg1, arg2, arg3) {
1262
+ wasm.__wasm_bindgen_func_elem_5465(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
1198
1263
  }
1199
1264
 
1200
1265
  const WasmChatFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -7,10 +7,13 @@ export const __wbg_wasmencoder_free: (a: number, b: number) => void;
7
7
  export const __wbg_wasmindexbuilder_free: (a: number, b: number) => void;
8
8
  export const __wbg_wasmmodel_free: (a: number, b: number) => void;
9
9
  export const set_debug_logging: (a: number) => void;
10
+ export const wasmchat_clear_history: (a: number) => void;
10
11
  export const wasmchat_context_size: (a: number) => number;
11
12
  export const wasmchat_generate: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
12
13
  export const wasmchat_generate_stream: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
14
+ export const wasmchat_history_len: (a: number) => number;
13
15
  export const wasmchat_load: (a: number, b: number, c: number, d: number, e: number) => void;
16
+ export const wasmchat_send_stream: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
14
17
  export const wasmclassifier_classify: (a: number, b: number, c: number, d: number) => void;
15
18
  export const wasmclassifier_classify_batch: (a: number, b: number, c: number, d: number) => void;
16
19
  export const wasmclassifier_labels: (a: number, b: number) => void;
@@ -42,8 +45,8 @@ export const wasmsearch_doc_count: (a: number) => number;
42
45
  export const wasmsearch_add_chunk: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
43
46
  export const __wbg_wasmreranker_free: (a: number, b: number) => void;
44
47
  export const __wbg_wasmsearch_free: (a: number, b: number) => void;
45
- export const __wasm_bindgen_func_elem_5410: (a: number, b: number, c: number, d: number) => void;
46
- export const __wasm_bindgen_func_elem_5426: (a: number, b: number, c: number, d: number) => void;
48
+ export const __wasm_bindgen_func_elem_5449: (a: number, b: number, c: number, d: number) => void;
49
+ export const __wasm_bindgen_func_elem_5465: (a: number, b: number, c: number, d: number) => void;
47
50
  export const __wbindgen_export: (a: number, b: number) => number;
48
51
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
49
52
  export const __wbindgen_export3: (a: number) => void;