mlx-bun 0.0.10 → 0.0.12

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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
@@ -0,0 +1,242 @@
1
+ // Model Hub REST wrappers — thin loopback JSON routes for the web chat's
2
+ // Model Hub panel (docs/design/web-chat-redesign.md §9 Phase 3, beat-matrix
3
+ // Axis 3 "Hub" row). Same posture as src/memory/rest.ts: pure (request|url)
4
+ // => Response handlers with no `ctx` dependency, dispatched from
5
+ // src/server.ts by path+method, so they're unit-testable without booting a
6
+ // model context (see tests/hub-rest.test.ts).
7
+ //
8
+ // Three sources, same as optiq Lab's Hub, plus the hardware-fit column optiq
9
+ // lacks:
10
+ // - GET /api/hub/local — registry's canonical downloaded models, each
11
+ // with a /fit-computed verdict (predicted decode tok/s + fits at a
12
+ // default context) — the beat-matrix "real hardware verdict per row".
13
+ // - GET /api/hub/search — server-side HF model search, filtered to
14
+ // MLX-compatible results, normalized. NEVER auto-downloads.
15
+ // - POST /api/hub/download — kicks off a background download via the
16
+ // existing downloadModel() + process tracker (src/download.ts);
17
+ // progress is already visible via GET /downloads. Refuses a duplicate
18
+ // in-flight download for the same repo.
19
+ // - POST /api/hub/serve — model-swap investigation (see the doc header
20
+ // above the handler): today this ALWAYS returns
21
+ // { ok:false, restart_required:true, command } — see
22
+ // docs/design/runtime-isolation.md for why no in-process swap seam is
23
+ // reachable from the web chat's serving path today.
24
+
25
+ import { loadModelConfig } from "./config";
26
+ import { fit } from "./fit";
27
+ import { downloadModel, hfToken, isDownloadActive, type DownloadOptions } from "./download";
28
+ import { Registry, visionCapable, type ModelRecord } from "./registry";
29
+ import { supportTier } from "./model/support";
30
+
31
+ function jsonOk<T extends object>(body: T, init?: ResponseInit): Response {
32
+ return Response.json({ ok: true, ...body }, init);
33
+ }
34
+
35
+ function jsonErr(error: string, status = 400): Response {
36
+ return Response.json({ ok: false, error }, { status });
37
+ }
38
+
39
+ // ---- GET /api/hub/local -------------------------------------------------
40
+
41
+ /** One row per canonical repo, mirroring /library's shape but scoped to
42
+ * what the Hub panel needs: size/quant/capabilities + a fit verdict at a
43
+ * fixed 8k context (the same "typical" context /fit and /library use, so
44
+ * the three surfaces never disagree). `reg`/`hubDir` are injectable so
45
+ * tests can point at a synthetic hub dir instead of the real
46
+ * ~/.cache/huggingface/hub (matching tests/registry-fit.test.ts's pattern);
47
+ * the live server route omits both and gets the real registry. */
48
+ export async function handleHubLocal(
49
+ opts: { reg?: Registry; hubDir?: string } = {},
50
+ ): Promise<Response> {
51
+ const reg = opts.reg ?? new Registry();
52
+ const ownReg = !opts.reg;
53
+ try {
54
+ await reg.scan(opts.hubDir);
55
+ const rows = [];
56
+ for (const m of reg.listCanonical()) {
57
+ rows.push(await hubLocalRow(m));
58
+ }
59
+ return jsonOk({ models: rows });
60
+ } finally {
61
+ if (ownReg) reg.close();
62
+ }
63
+ }
64
+
65
+ async function hubLocalRow(m: ModelRecord) {
66
+ const tier = supportTier(m.modelType, m.repoId);
67
+ let assessment: {
68
+ fits: boolean;
69
+ max_safe_context: number;
70
+ predicted_decode_tps: number;
71
+ } | null = null;
72
+ try {
73
+ const config = await loadModelConfig(m.path);
74
+ const r = fit(config, m.sizeBytes, 8192, undefined, undefined, m.expertsBytes);
75
+ assessment = {
76
+ fits: r.fits,
77
+ max_safe_context: r.maxSafeContext,
78
+ predicted_decode_tps: r.predictedDecodeTps,
79
+ };
80
+ } catch {
81
+ /* unreadable config — assessment stays null, row still lists */
82
+ }
83
+ return {
84
+ repo_id: m.repoId,
85
+ model_type: m.modelType,
86
+ size_bytes: m.sizeBytes,
87
+ quant_bits: m.quantBits,
88
+ quant_group_size: m.quantGroupSize,
89
+ vision: visionCapable(m),
90
+ supported: tier !== null,
91
+ support_tier: tier,
92
+ assessment,
93
+ };
94
+ }
95
+
96
+ // ---- GET /api/hub/search -------------------------------------------------
97
+
98
+ export interface HubSearchResult {
99
+ id: string;
100
+ downloads: number;
101
+ likes: number;
102
+ size_estimate: number | null;
103
+ }
104
+
105
+ /** The HF Hub's model-search API, filtered to the `mlx` library tag (the
106
+ * tag mlx-community and every other MLX-converted repo carries) so results
107
+ * are MLX-compatible by construction — same convention the download path
108
+ * already speaks HTTPS against (huggingface.co/api/*). Injectable `fetchFn`
109
+ * for tests; defaults to the real global fetch. */
110
+ export async function searchHub(
111
+ query: string,
112
+ opts: { fetchFn?: typeof fetch; limit?: number; endpoint?: string } = {},
113
+ ): Promise<
114
+ | { ok: true; offline: false; results: HubSearchResult[] }
115
+ | { ok: false; offline: true; error: string }
116
+ > {
117
+ const fetchFn = opts.fetchFn ?? fetch;
118
+ const endpoint = opts.endpoint ?? "https://huggingface.co";
119
+ const limit = opts.limit ?? 30;
120
+ const token = hfToken();
121
+ const url =
122
+ `${endpoint}/api/models?search=${encodeURIComponent(query)}` +
123
+ `&filter=mlx&sort=downloads&direction=-1&limit=${limit}`;
124
+ let res: Response;
125
+ try {
126
+ res = await fetchFn(url, {
127
+ headers: token ? { authorization: `Bearer ${token}` } : {},
128
+ signal: AbortSignal.timeout(10_000),
129
+ });
130
+ } catch (e) {
131
+ // Network unreachable (offline, DNS failure, timeout) — never surface
132
+ // this as a 500; the Hub panel shows an explicit offline state instead.
133
+ return { ok: false, offline: true, error: (e as Error).message };
134
+ }
135
+ if (!res.ok) {
136
+ return { ok: false, offline: true, error: `HF search ${res.status}` };
137
+ }
138
+ let body: unknown;
139
+ try {
140
+ body = await res.json();
141
+ } catch (e) {
142
+ return { ok: false, offline: true, error: `HF search: bad response (${(e as Error).message})` };
143
+ }
144
+ const rows = Array.isArray(body) ? body : [];
145
+ const results: HubSearchResult[] = rows
146
+ // Belt-and-suspenders: the API's filter=mlx already restricts to the
147
+ // tag, but a defensive re-check keeps a future upstream filter change
148
+ // from silently widening results to non-MLX repos.
149
+ .filter((r) => Array.isArray((r as Record<string, unknown>).tags)
150
+ ? ((r as { tags: string[] }).tags).includes("mlx")
151
+ : true)
152
+ .map((r) => {
153
+ const row = r as Record<string, unknown>;
154
+ return {
155
+ id: String(row.id ?? row.modelId ?? ""),
156
+ downloads: typeof row.downloads === "number" ? row.downloads : 0,
157
+ likes: typeof row.likes === "number" ? row.likes : 0,
158
+ // The search endpoint doesn't return file sizes (that's a second
159
+ // per-repo call this v1 skips to keep search fast) — null, not a
160
+ // fabricated number; the frontend's fit badge lands after download.
161
+ size_estimate: null,
162
+ };
163
+ })
164
+ .filter((r) => r.id.length > 0);
165
+ return { ok: true, offline: false, results };
166
+ }
167
+
168
+ export async function handleHubSearch(url: URL): Promise<Response> {
169
+ const q = url.searchParams.get("q")?.trim() ?? "";
170
+ if (!q) return jsonErr("missing ?q=");
171
+ const result = await searchHub(q);
172
+ if (!result.ok) return jsonOk({ offline: true, error: result.error, results: [] });
173
+ return jsonOk({ offline: false, results: result.results });
174
+ }
175
+
176
+ // ---- POST /api/hub/download -----------------------------------------------
177
+
178
+ // downloadModel()'s own DownloadStatus row (src/download.ts's downloadLog,
179
+ // read via isDownloadActive) only exists once listRepoFiles' network round
180
+ // trip resolves — a rapid double-submit of this route can both pass that
181
+ // check before either row is written. Route-local admission closes that
182
+ // window: a repo is marked in-flight synchronously, before the first
183
+ // `await`, and cleared in a `finally` once downloadModel settles either way.
184
+ const inFlightHubDownloads = new Set<string>();
185
+
186
+ export async function handleHubDownload(request: Request): Promise<Response> {
187
+ const body = (await request.json().catch(() => ({}))) as { repo?: string };
188
+ const repo = body.repo?.trim();
189
+ if (!repo) return jsonErr("missing \"repo\"");
190
+ if (inFlightHubDownloads.has(repo) || isDownloadActive(repo))
191
+ return jsonErr(`a download for ${repo} is already in progress`, 409);
192
+ inFlightHubDownloads.add(repo);
193
+ // Fire-and-forget, mirroring the CLI's background-download pattern
194
+ // (cli.ts's `downloadModel(recRepo).then(...).catch(...)`) — progress is
195
+ // already visible via GET /downloads (the process-global tracker
196
+ // downloadModel() itself writes to); this route just starts it and
197
+ // returns immediately rather than blocking on a multi-GB transfer.
198
+ const opts: DownloadOptions = {};
199
+ downloadModel(repo, opts)
200
+ .then(async () => {
201
+ try {
202
+ const reg = new Registry();
203
+ try { await reg.scan(); } finally { reg.close(); }
204
+ } catch { /* best-effort refresh; /downloads still shows completion */ }
205
+ })
206
+ .catch(() => { /* status recorded on the tracker; /downloads shows the error */ })
207
+ .finally(() => inFlightHubDownloads.delete(repo));
208
+ return jsonOk({ repo, started: true });
209
+ }
210
+
211
+ // ---- POST /api/hub/serve --------------------------------------------------
212
+
213
+ /** Model-swap investigation (docs/design/runtime-isolation.md): the web
214
+ * chat's `/ws/chat` is explicitly NOT proxied even under `--isolate`
215
+ * (src/serve/isolate.ts serves it a 501 with a pointer back to the direct,
216
+ * non-isolated server) and `--isolate` itself is opt-in, not the default —
217
+ * so the process the web chat actually talks to always has exactly one
218
+ * model loaded in-process with no drop-weights-and-reload seam. The
219
+ * isolate.ts `ModelPool` (P2, spawn-overlap child-per-model) IS a real,
220
+ * working live-swap mechanism, but it lives entirely on the `--isolate`
221
+ * reverse-proxy's `/v1/*` HTTP surface — not reachable from the WebSocket
222
+ * chat path this Hub panel serves. Faking a swap here (killing/reloading
223
+ * the in-process model while `/ws/chat` sessions are attached) would leave
224
+ * the server in a half-state the ground rules forbid ("the AI may crash,
225
+ * the UI never may" — a self-inflicted crash is not better). Honest
226
+ * answer: refuse, and hand back the exact restart command. Follow-up:
227
+ * wire /ws/chat through the isolate proxy (or move session state above the
228
+ * engine boundary) so ModelPool's spawn-overlap swap is actually reachable
229
+ * from the web chat — tracked as the Hub's live-swap follow-up. */
230
+ export async function handleHubServe(request: Request): Promise<Response> {
231
+ const body = (await request.json().catch(() => ({}))) as { model?: string };
232
+ const model = body.model?.trim();
233
+ if (!model) return jsonErr("missing \"model\"");
234
+ // ok:false is deliberate — this is not an error the caller mishandled, it
235
+ // is the honest capability answer ("no live swap today"), but it is also
236
+ // not success: the frontend must not treat this as "now serving `model`".
237
+ return Response.json({
238
+ ok: false,
239
+ restart_required: true,
240
+ command: `mlx-bun serve ${model}`,
241
+ });
242
+ }
package/src/index.ts CHANGED
@@ -12,7 +12,13 @@ export { loadModelConfig } from "./config";
12
12
  export { Weights } from "./weights";
13
13
  export { Gemma4Model } from "./model/gemma4";
14
14
  export { MiniCPM5Model } from "./model/minicpm5";
15
- export { createModel, type RuntimeModel } from "./model/factory";
15
+ export {
16
+ createModel,
17
+ openGlm52RuntimeModel,
18
+ openModel,
19
+ type Glm52RuntimeOpenOptions,
20
+ type RuntimeModel,
21
+ } from "./model/factory";
16
22
  export { Qwen3Model } from "./model/qwen3";
17
23
  export { generate } from "./generate";
18
24
  export { loadTokenizer } from "./tokenizer";
@@ -21,4 +27,10 @@ export { ChatTemplate } from "./chat-template";
21
27
  export { chooseAutoModel, COEXIST_FRACTION, DEFAULT_REPO_ID, fit, largestRecommendedRepoId, recommendedRepoId, skuMatrix, thisMachine } from "./fit";
22
28
  export { downloadModel } from "./download";
23
29
  export { Registry } from "./registry";
24
- export { createServer, loadContext } from "./server";
30
+ export {
31
+ createServer,
32
+ loadContext,
33
+ type LoadContextOptions,
34
+ type ServerContext,
35
+ type ServerOptions,
36
+ } from "./server";
@@ -149,6 +149,7 @@ interface QueuedSpawn {
149
149
  jobId: string;
150
150
  entry: string;
151
151
  bin: string;
152
+ spawn: typeof Bun.spawn;
152
153
  onComplete?: (jobId: string, code: number) => void;
153
154
  }
154
155
 
@@ -171,6 +172,8 @@ export interface SubprocessOpts {
171
172
  entry?: string;
172
173
  /** Override the runtime binary (tests). Defaults to "bun". */
173
174
  bin?: string;
175
+ /** Override process creation for deterministic failure-path tests. */
176
+ spawn?: typeof Bun.spawn;
174
177
  /** Called on the server (parent) after the child exits — used to invalidate
175
178
  * caches (e.g. the Library) so a finished quantize surfaces immediately. */
176
179
  onComplete?: (jobId: string, code: number) => void;
@@ -192,35 +195,46 @@ export function submitSubprocess(
192
195
  jobId: row.id,
193
196
  entry: opts.entry ?? JOB_ENTRY_PATH,
194
197
  bin: opts.bin ?? "bun",
198
+ spawn: opts.spawn ?? Bun.spawn,
195
199
  onComplete: opts.onComplete,
196
200
  };
197
- if (gpuLeaseHolder === null) {
198
- spawnNow(item);
199
- } else {
200
- spawnQueue.push(item);
201
- }
201
+ spawnQueue.push(item);
202
+ drainQueue();
202
203
  return { jobId: row.id, outputPath };
203
204
  }
204
205
 
205
206
  /** Spawn the child, acquire the lease, stream stdout/stderr into the log,
206
207
  * reconcile terminal status on exit, then release the lease and drain. */
207
208
  function spawnNow(item: QueuedSpawn): void {
208
- const { store, jobId, entry, bin } = item;
209
+ const { store, jobId, entry, bin, spawn } = item;
209
210
  gpuLeaseHolder = jobId;
210
211
 
211
212
  // The child opens its OWN JobStore over the SAME DB/logs — a sqlite
212
213
  // connection can't cross the process boundary, so we hand the paths via
213
214
  // env. A :memory: DB can't be shared with a child; subprocess jobs require
214
215
  // a file-backed DB.
215
- const proc = Bun.spawn([bin, entry, jobId], {
216
- stdout: "pipe",
217
- stderr: "pipe",
218
- env: {
219
- ...process.env,
220
- MLX_BUN_JOBS_DB: store.dbPath,
221
- MLX_BUN_JOBS_DIR: store.logsDir,
222
- },
223
- });
216
+ let proc: Bun.Subprocess<"ignore", "pipe", "pipe">;
217
+ try {
218
+ proc = spawn([bin, entry, jobId], {
219
+ stdout: "pipe",
220
+ stderr: "pipe",
221
+ env: {
222
+ ...process.env,
223
+ MLX_BUN_JOBS_DB: store.dbPath,
224
+ MLX_BUN_JOBS_DIR: store.logsDir,
225
+ },
226
+ });
227
+ } catch (e) {
228
+ try {
229
+ store.setStatus(jobId, "failed", {
230
+ error: errString(e),
231
+ endedAt: nowIso(),
232
+ });
233
+ } finally {
234
+ releaseLease(jobId);
235
+ }
236
+ return;
237
+ }
224
238
 
225
239
  const row = store.get(jobId);
226
240
  const logPath = row?.log_path;
@@ -259,9 +273,14 @@ function releaseLease(jobId: string): void {
259
273
 
260
274
  /** Run the next queued subprocess job if the lease is free. */
261
275
  export function drainQueue(): void {
262
- if (gpuLeaseHolder !== null) return;
263
- const next = spawnQueue.shift();
264
- if (next) spawnNow(next);
276
+ while (gpuLeaseHolder === null) {
277
+ const next = spawnQueue.shift();
278
+ if (!next) return;
279
+ // A synchronous spawn failure releases the lease. Keep draining
280
+ // iteratively so a run of bad queue entries cannot recurse or wedge the
281
+ // first startable job behind them.
282
+ spawnNow(next);
283
+ }
265
284
  }
266
285
 
267
286
  /** Read a child stream line-by-line, buffering partial trailing lines, and