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,132 @@
1
+ // GENERATED-ADJACENT source module — part of the src/web/src/* split (plan
2
+ // §7/§9 Phase 2). Built into src/web/app.js by scripts/build-web.ts.
3
+ //
4
+ // DATASET CONTROLLER — template-driven builder. Behavior-identical port of
5
+ // the original controllers.dataset IIFE in app.html.
6
+
7
+ import { api, jobStream } from "./api";
8
+ import type { ApiEnvelope } from "./protocol";
9
+ import { $, toast, num, pushToHub, activeModelId } from "./shell";
10
+ import { esc, renderSteps } from "./markdown";
11
+
12
+ const STEPS = ["Template", "Inputs", "Generate", "Done"];
13
+
14
+ interface DatasetField {
15
+ name: string;
16
+ label?: string;
17
+ type?: "textarea" | "number" | "text";
18
+ hint?: string;
19
+ default?: unknown;
20
+ required?: boolean;
21
+ }
22
+ interface DatasetTemplate {
23
+ id: string;
24
+ label?: string;
25
+ description?: string;
26
+ needs_llm?: boolean;
27
+ output_format?: string;
28
+ fields?: DatasetField[];
29
+ }
30
+
31
+ export function createDatasetController() {
32
+ let step = 0, templates: DatasetTemplate[] = [], selected: DatasetTemplate | null = null, jobId: string | null = null, es: EventSource | null = null, log: string[] = [];
33
+
34
+ function show(n: number): void {
35
+ step = n;
36
+ document.querySelectorAll<HTMLElement>("[data-dstep]").forEach((d) => d.style.display = (+d.dataset.dstep! === n) ? "" : "none");
37
+ renderSteps($("d-steps"), STEPS, n);
38
+ }
39
+
40
+ async function loadTemplates(): Promise<void> {
41
+ const grid = $("d-templates");
42
+ grid.innerHTML = Array.from({ length: 4 }, () => '<div class="card" style="height:130px"><div class="shimmer" style="height:18px;width:50%;margin-bottom:12px"></div><div class="shimmer" style="height:13px;width:90%;margin-bottom:6px"></div><div class="shimmer" style="height:13px;width:70%"></div></div>').join("");
43
+ const d = await api("/api/dataset/templates").catch(() => ({ templates: null }));
44
+ templates = (d as { templates?: DatasetTemplate[] }).templates || [];
45
+ if (!templates.length) { grid.innerHTML = '<div class="empty" style="grid-column:1/-1"><h3>No templates available</h3><p>The server didn\'t return any dataset templates. Make sure the server is running.</p></div>'; return; }
46
+ // non-LLM templates first
47
+ const ordered = templates.slice().sort((a, b) => (a.needs_llm ? 1 : 0) - (b.needs_llm ? 1 : 0));
48
+ grid.innerHTML = ordered.map((t) =>
49
+ '<div class="card" data-tid="' + esc(t.id) + '" style="cursor:pointer;display:flex;flex-direction:column;gap:8px">' +
50
+ '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
51
+ '<h3 style="margin:0;font-size:16px;font-weight:700;letter-spacing:-.01em;color:var(--ink);text-transform:none">' + esc(t.label || t.id) + "</h3>" +
52
+ (t.needs_llm ? '<span class="pill warn" style="padding:3px 9px;font-size:11px"><span class="dot"></span>uses local model</span>' : "") +
53
+ (t.output_format ? '<span class="soon" style="color:var(--dim)">' + esc(t.output_format) + "</span>" : "") +
54
+ "</div>" +
55
+ '<p style="color:var(--dim);font-size:13.5px;line-height:1.5;margin:0">' + esc(t.description || "") + "</p>" +
56
+ "</div>").join("");
57
+ grid.querySelectorAll<HTMLElement>("[data-tid]").forEach((c) =>
58
+ c.onclick = () => selectTemplate(templates.find((t) => t.id === c.dataset.tid)!));
59
+ }
60
+
61
+ function selectTemplate(t: DatasetTemplate): void {
62
+ selected = t;
63
+ $("d-form-title").textContent = t.label || t.id;
64
+ $("d-form-desc").textContent = t.description || "";
65
+ const form = $("d-form");
66
+ form.innerHTML = (t.fields || []).map((f) => {
67
+ const lbl = esc(f.label || f.name) + (f.required ? " *" : "");
68
+ const ph = f.hint ? ' placeholder="' + esc(f.hint) + '"' : "";
69
+ const dv = f.default != null ? f.default : "";
70
+ let ctrl: string;
71
+ if (f.type === "textarea") ctrl = '<textarea data-fld="' + esc(f.name) + '"' + ph + ">" + esc(dv) + "</textarea>";
72
+ else if (f.type === "number") ctrl = '<input type="number" data-fld="' + esc(f.name) + '" value="' + esc(dv) + '"' + ph + ">";
73
+ else ctrl = '<input type="text" data-fld="' + esc(f.name) + '" value="' + esc(dv) + '"' + ph + ">";
74
+ return '<div class="field"><label>' + lbl + "</label>" + ctrl + (f.hint ? '<div class="hint">' + esc(f.hint) + "</div>" : "") + "</div>";
75
+ }).join("");
76
+ show(1);
77
+ }
78
+
79
+ async function submit(): Promise<void> {
80
+ if (!selected) return;
81
+ const inputs: Record<string, unknown> = {};
82
+ $("d-form").querySelectorAll<HTMLInputElement | HTMLTextAreaElement>("[data-fld]").forEach((i) => {
83
+ inputs[i.dataset.fld!] = i.type === "number" ? (i.value === "" ? null : +i.value) : i.value;
84
+ });
85
+ const body: Record<string, unknown> = { template_id: selected.id, inputs };
86
+ if (selected.needs_llm && activeModelId) body.model_name = activeModelId;
87
+ const btn = $("d-submit") as HTMLButtonElement; btn.disabled = true;
88
+ const d = await api("/api/dataset/submit", { method: "POST", body }).catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
89
+ btn.disabled = false;
90
+ if (!d.ok) { toast((d.error as string) || "could not start generation", "err"); return; }
91
+ jobId = (d.job_id as string) || null;
92
+ $("d-push-panel").innerHTML = "";
93
+ $("d-out").textContent = (d.output_dir as string) || "";
94
+ log = []; $("d-log").textContent = ""; $("d-bar").style.width = "0%"; $("d-pct").textContent = "0%"; $("d-msg").textContent = "Starting…";
95
+ $("d-ntrain").textContent = $("d-nvalid").textContent = "—";
96
+ show(2);
97
+ attach(jobId!);
98
+ }
99
+
100
+ function attach(id: string): void {
101
+ if (es) es.close();
102
+ es = jobStream(id, {
103
+ log: (e) => { log.push(e.line); $("d-log").textContent = log.slice(-200).join("\n"); $("d-log").scrollTop = $("d-log").scrollHeight; },
104
+ stage: (e) => {
105
+ if (e.progress != null) { const p = Math.round(e.progress * 100); $("d-bar").style.width = p + "%"; $("d-pct").textContent = p + "%"; }
106
+ if (e.message) $("d-msg").textContent = e.message;
107
+ if (e.n_train != null) $("d-ntrain").textContent = num(e.n_train);
108
+ if (e.n_valid != null) $("d-nvalid").textContent = num(e.n_valid);
109
+ if (e.stage === "done") { if (e.output_dir) $("d-out").textContent = e.output_dir; finish(e); }
110
+ },
111
+ done: (e) => finish(e),
112
+ failed: (e) => { $("d-msg").textContent = "Failed: " + (e.error || "unknown error"); toast(e.error || "generation failed", "err"); es && es.close(); },
113
+ });
114
+ }
115
+ function finish(e?: { n_train?: number; n_valid?: number }): void {
116
+ $("d-bar").style.width = "100%"; $("d-pct").textContent = "100%"; es && es.close();
117
+ if (e && e.n_train != null) $("d-ntrain").textContent = num(e.n_train);
118
+ if (e && e.n_valid != null) $("d-nvalid").textContent = num(e.n_valid);
119
+ show(3);
120
+ }
121
+
122
+ return {
123
+ init() {
124
+ show(0);
125
+ loadTemplates();
126
+ document.querySelectorAll<HTMLButtonElement>("#s-dataset [data-dback]").forEach((b) => b.onclick = () => show(0));
127
+ ($("d-submit") as HTMLButtonElement).onclick = submit;
128
+ ($("d-push") as HTMLButtonElement).onclick = () => pushToHub($("d-push-panel"), { kind: "dataset", job_id: jobId || undefined });
129
+ ($("d-again") as HTMLButtonElement).onclick = () => { selected = null; $("d-push-panel").innerHTML = ""; show(0); };
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,267 @@
1
+ // GENERATED-ADJACENT source module — part of the src/web/src/* split (plan
2
+ // §7/§9 Phase 2). Built into src/web/app.js by scripts/build-web.ts.
3
+ //
4
+ // FINE-TUNE CONTROLLER — 5-step wizard with live loss chart.
5
+ // Behavior-identical port of the original controllers.finetune IIFE.
6
+
7
+ import { api, jobStream } from "./api";
8
+ import type { ApiEnvelope } from "./protocol";
9
+ import { $, toast, num, pushToHub, controllers } from "./shell";
10
+ import { esc, renderSteps } from "./markdown";
11
+
12
+ const STEPS = ["Base", "Dataset", "Hyperparams", "Train", "Done"];
13
+
14
+ interface FileWithRelPath extends File {
15
+ webkitRelativePath: string;
16
+ }
17
+
18
+ interface LossPoint { step: number; loss: number | null }
19
+
20
+ export function createFinetuneController() {
21
+ let step = 0, method: "sft" | "dpo" | "orpo" = "sft", lrTouched = false, rankTouched = false, es: EventSource | null = null, log: string[] = [];
22
+ let datasetOk = false, adapterPath = "";
23
+ const trainPts: LossPoint[] = [], valPts: LossPoint[] = [];
24
+
25
+ function show(n: number): void {
26
+ step = n;
27
+ document.querySelectorAll<HTMLElement>("[data-fstep]").forEach((d) => d.style.display = (+d.dataset.fstep! === n) ? "" : "none");
28
+ renderSteps($("f-steps"), STEPS, n);
29
+ }
30
+
31
+ // OS folder picker for the base model — resolve by NAME server-side (no
32
+ // upload, works with the HF cache's symlinked layout). Mirrors Quantize.
33
+ async function resolveBaseByName(name: string, relPath?: string): Promise<void> {
34
+ if (!name && !relPath) return;
35
+ const d = await api("/api/model/resolve-folder", { method: "POST", body: { folder_name: name || "", rel_path: relPath || "" } })
36
+ .catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
37
+ if (!d.ok) { toast((d.error as string) || "could not locate this folder", "err"); return; }
38
+ ($("f-model") as HTMLInputElement).value = d.path as string;
39
+ }
40
+ async function pickBaseFolder(): Promise<void> {
41
+ if (window.showDirectoryPicker) {
42
+ try { const h = await window.showDirectoryPicker({ id: "mlx-models", mode: "read" }); return resolveBaseByName(h.name); }
43
+ catch { return; }
44
+ }
45
+ $("f-folder").click();
46
+ }
47
+
48
+ async function inspectDataset(): Promise<void> {
49
+ const path = ($("f-data") as HTMLInputElement).value.trim();
50
+ if (!path) { toast("Enter a dataset directory.", "err"); return; }
51
+ const out = $("f-inspect-out");
52
+ out.innerHTML = '<div class="flash"><span class="shimmer">inspecting…</span></div>';
53
+ const d = await api("/api/finetune/inspect-dataset", { method: "POST", body: { path } }).catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
54
+ if (!d.ok) { out.innerHTML = '<div class="flash err">' + esc(d.error || "could not read dataset") + "</div>"; datasetOk = false; ($("f-next1") as HTMLButtonElement).disabled = true; return; }
55
+ datasetOk = true; ($("f-next1") as HTMLButtonElement).disabled = false;
56
+ out.innerHTML = '<div class="flash ok"><strong>' + num(d.n_train as number) + "</strong> train rows" +
57
+ (d.n_valid ? " · <strong>" + num(d.n_valid as number) + "</strong> valid rows" : "") +
58
+ (d.format ? " · format <strong>" + esc(d.format) + "</strong>" : "") + "</div>";
59
+ }
60
+
61
+ function setMethod(m: "sft" | "dpo" | "orpo"): void {
62
+ method = m;
63
+ $("f-method").querySelectorAll<HTMLButtonElement>("button").forEach((b) => b.classList.toggle("on", b.dataset.v === m));
64
+ $("f-dpo-extra").style.display = m === "dpo" ? "" : "none";
65
+ $("f-orpo-extra").style.display = m === "orpo" ? "" : "none";
66
+ $("f-method-hint").textContent = m === "dpo"
67
+ ? "Direct Preference Optimization — data is {prompt, chosen, rejected}."
68
+ : m === "orpo"
69
+ ? "Odds Ratio Preference Optimization (reference-free) — data is {prompt, chosen, rejected}."
70
+ : "Supervised fine-tuning — data is messages or prompt+completion.";
71
+ if (!lrTouched) ($("f-lr") as HTMLInputElement).value = String(m === "dpo" ? 0.00005 : m === "orpo" ? 0.00001 : 0.0002);
72
+ // ORPO's validated LoRA recipe uses rank 16 (vs 8 for SFT); apply unless edited.
73
+ if (!rankTouched) ($("f-rank") as HTMLInputElement).value = String(m === "orpo" ? 16 : 8);
74
+ }
75
+
76
+ function collectHP(): Record<string, unknown> {
77
+ const modules = ($("f-modules") as HTMLInputElement).value.split(",").map((s) => s.trim()).filter(Boolean);
78
+ const hp: Record<string, unknown> = {
79
+ model_dir: ($("f-model") as HTMLInputElement).value.trim(),
80
+ data_dir: ($("f-data") as HTMLInputElement).value.trim(),
81
+ method,
82
+ rank: +($("f-rank") as HTMLInputElement).value, scale: +($("f-scale") as HTMLInputElement).value, lora_dropout: +($("f-dropout") as HTMLInputElement).value,
83
+ target_modules: modules, num_layers: +($("f-layers") as HTMLInputElement).value, iters: +($("f-iters") as HTMLInputElement).value,
84
+ batch_size: +($("f-batch") as HTMLInputElement).value, learning_rate: +($("f-lr") as HTMLInputElement).value, max_seq_length: +($("f-seq") as HTMLInputElement).value,
85
+ };
86
+ if (method === "dpo") { hp.dpo_beta = +($("f-beta") as HTMLInputElement).value; hp.dpo_lr_schedule = ($("f-sched") as HTMLSelectElement).value; }
87
+ if (method === "orpo") {
88
+ hp.orpo_lambda = +($("f-orpo-lambda") as HTMLInputElement).value; hp.orpo_lr_schedule = ($("f-orpo-sched") as HTMLSelectElement).value;
89
+ // web-ui-pass-plan.md #7: sft_scope only makes sense for ORPO's
90
+ // monolithic loss (the SFT term needs a scope); default matches the
91
+ // segmented control's default ("full", paper/TRL chosen-NLL).
92
+ const scopeBtn = $("f-orpo-scope").querySelector("button.on") as HTMLElement | null;
93
+ hp.sft_scope = (scopeBtn && scopeBtn.dataset.v) || "full";
94
+ }
95
+ return hp;
96
+ }
97
+
98
+ async function submit(): Promise<void> {
99
+ const btn = $("f-submit") as HTMLButtonElement; btn.disabled = true;
100
+ const d = await api("/api/finetune/submit", { method: "POST", body: collectHP() }).catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
101
+ btn.disabled = false;
102
+ if (!d.ok) { toast((d.error as string) || "could not start training", "err"); return; }
103
+ adapterPath = (d.adapter_path as string) || "";
104
+ $("f-out").textContent = adapterPath;
105
+ $("f-merge-out").innerHTML = $("f-exp-out").innerHTML = $("f-push-panel").innerHTML = "";
106
+ trainPts.length = 0; valPts.length = 0; log = [];
107
+ $("f-log").textContent = ""; $("f-bar").style.width = "0%"; $("f-pct").textContent = "0%"; $("f-msg").textContent = "Starting…";
108
+ $("f-step").textContent = $("f-loss").textContent = $("f-curlr").textContent = $("f-curtps").textContent = "—";
109
+ drawChart();
110
+ show(3);
111
+ attach(d.job_id as string);
112
+ }
113
+
114
+ function attach(jobId: string): void {
115
+ if (es) es.close();
116
+ es = jobStream(jobId, {
117
+ log: (e) => { log.push(e.line); $("f-log").textContent = log.slice(-200).join("\n"); $("f-log").scrollTop = $("f-log").scrollHeight; },
118
+ metric: (e) => {
119
+ if (e.kind === "val") { valPts.push({ step: e.step, loss: e.loss }); $("f-leg-val").style.display = ""; }
120
+ else {
121
+ trainPts.push({ step: e.step, loss: e.loss });
122
+ $("f-step").textContent = num(e.step);
123
+ $("f-loss").textContent = e.loss != null ? e.loss.toFixed(4) : "—";
124
+ if (e.learning_rate != null) $("f-curlr").textContent = e.learning_rate.toExponential(2);
125
+ if (e.tokens_per_sec != null) $("f-curtps").textContent = e.tokens_per_sec.toFixed(1);
126
+ if (e.progress != null) { const p = Math.round(e.progress * 100); $("f-bar").style.width = p + "%"; $("f-pct").textContent = p + "%"; }
127
+ if (e.message) $("f-msg").textContent = e.message;
128
+ }
129
+ drawChart();
130
+ },
131
+ stage: (e) => {
132
+ if (e.progress != null) { const p = Math.round(e.progress * 100); $("f-bar").style.width = p + "%"; $("f-pct").textContent = p + "%"; }
133
+ if (e.message) $("f-msg").textContent = e.message;
134
+ if (e.stage === "done") { if (e.adapter_path) { adapterPath = e.adapter_path; $("f-out").textContent = e.adapter_path; } finish(); }
135
+ },
136
+ done: () => finish(),
137
+ failed: (e) => { $("f-msg").textContent = "Failed: " + (e.error || "unknown error"); toast(e.error || "training failed", "err"); es && es.close(); },
138
+ });
139
+ }
140
+ function finish(): void {
141
+ $("f-bar").style.width = "100%"; $("f-pct").textContent = "100%"; es && es.close();
142
+ if (!adapterPath) adapterPath = ($("f-out").textContent || "").trim();
143
+ const base = ($("f-model") as HTMLInputElement).value.trim();
144
+ // prefill the done-step affordances from this run's paths
145
+ ($("f-merge-a") as HTMLInputElement).value = adapterPath;
146
+ ($("f-exp-base") as HTMLInputElement).value = base;
147
+ ($("f-exp-adapter") as HTMLInputElement).value = adapterPath;
148
+ show(4);
149
+ // web-ui-pass-plan.md #15 staleness half: the new adapter is on disk now —
150
+ // refresh the chat adapter chip immediately rather than waiting for the
151
+ // user to navigate to Chat (enter() also refreshes, this just removes the
152
+ // wait for the common case of finishing a run and going straight to try it).
153
+ if (controllers.chat && controllers.chat.refreshAdapters) (controllers.chat.refreshAdapters as () => void)();
154
+ }
155
+
156
+ async function mergeAdapters(): Promise<void> {
157
+ const a = ($("f-merge-a") as HTMLInputElement).value.trim(), b = ($("f-merge-b") as HTMLInputElement).value.trim();
158
+ const out = $("f-merge-out");
159
+ if (!a || !b) { out.innerHTML = '<div class="flash err">Enter both adapter paths.</div>'; return; }
160
+ out.innerHTML = '<div class="flash"><span class="shimmer">merging…</span></div>';
161
+ const btn = $("f-merge-go") as HTMLButtonElement; btn.disabled = true;
162
+ const d = await api("/api/finetune/merge", { method: "POST", body: { adapter_a: a, adapter_b: b } }).catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
163
+ btn.disabled = false;
164
+ if (!d.ok) { out.innerHTML = '<div class="flash err">' + esc(d.error || "merge failed") + "</div>"; return; }
165
+ const stats = d.stats && typeof d.stats === "object"
166
+ ? Object.entries(d.stats as Record<string, unknown>).map(([k, v]) => esc(k) + " " + esc(v)).join(" · ")
167
+ : (d.stats ? esc(d.stats) : "");
168
+ out.innerHTML = '<div class="flash ok">Merged → <code>' + esc(d.merged_path || "") + "</code>" + (stats ? "<br>" + stats : "") + "</div>";
169
+ toast("Adapters merged", "ok");
170
+ }
171
+
172
+ async function exportModel(): Promise<void> {
173
+ const base = ($("f-exp-base") as HTMLInputElement).value.trim(), adapter = ($("f-exp-adapter") as HTMLInputElement).value.trim();
174
+ const out = $("f-exp-out");
175
+ if (!base || !adapter) { out.innerHTML = '<div class="flash err">Enter a base model and adapter path.</div>'; return; }
176
+ out.innerHTML = '<div class="flash"><span class="shimmer">exporting…</span></div>';
177
+ const btn = $("f-exp-go") as HTMLButtonElement; btn.disabled = true;
178
+ const d = await api("/api/finetune/export", { method: "POST", body: { base_model: base, adapter_path: adapter } }).catch((): ApiEnvelope => ({ ok: false, error: "request failed" }));
179
+ btn.disabled = false;
180
+ if (!d.ok) { out.innerHTML = '<div class="flash err">' + esc(d.error || "export failed") + "</div>"; return; }
181
+ out.innerHTML = '<div class="flash ok">Exported → <code>' + esc(d.export_path || "") + "</code></div>";
182
+ toast("Model exported", "ok");
183
+ }
184
+
185
+ /* inline SVG line plot — themed green (train) / blue (val) */
186
+ function drawChart(): void {
187
+ const svg = $("f-chart");
188
+ const W = 600, H = 200, padL = 8, padR = 8, padT = 12, padB = 12;
189
+ const all = trainPts.concat(valPts);
190
+ if (!all.length) { svg.innerHTML = '<text x="300" y="104" text-anchor="middle" fill="var(--dimmer)" font-size="13">waiting for the first metric…</text>'; return; }
191
+ const losses = all.map((p) => p.loss).filter((x): x is number => x != null && isFinite(x));
192
+ let minL = Math.min(...losses), maxL = Math.max(...losses);
193
+ if (minL === maxL) { minL -= 0.5; maxL += 0.5; }
194
+ const range = maxL - minL || 1;
195
+ const maxStep = Math.max(...all.map((p) => p.step || 0), 1);
196
+ const xOf = (s: number) => padL + (s / maxStep) * (W - padL - padR);
197
+ const yOf = (l: number) => padT + (1 - (l - minL) / range) * (H - padT - padB);
198
+ const path = (pts: LossPoint[], color: string, fillId: string): string => {
199
+ if (!pts.length) return "";
200
+ const d = pts.map((p, i) => (i ? "L" : "M") + xOf(p.step).toFixed(1) + " " + yOf(p.loss ?? 0).toFixed(1)).join(" ");
201
+ let out = '<path d="' + d + '" fill="none" stroke="' + color + '" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>';
202
+ // soft area fill under the curve
203
+ const area = d + " L" + xOf(pts[pts.length - 1]!.step).toFixed(1) + " " + (H - padB) + " L" + xOf(pts[0]!.step).toFixed(1) + " " + (H - padB) + " Z";
204
+ out = '<path d="' + area + '" fill="url(#' + fillId + ')" opacity="0.18"/>' + out;
205
+ return out;
206
+ };
207
+ // gridlines
208
+ let grid = "";
209
+ for (let i = 1; i < 4; i++) { const y = padT + (i / 4) * (H - padT - padB); grid += '<line x1="0" y1="' + y.toFixed(1) + '" x2="' + W + '" y2="' + y.toFixed(1) + '" stroke="rgba(255,255,255,.06)" stroke-width="1"/>'; }
210
+ svg.innerHTML =
211
+ '<defs>' +
212
+ '<linearGradient id="gf-train" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#30d158"/><stop offset="1" stop-color="#30d158" stop-opacity="0"/></linearGradient>' +
213
+ '<linearGradient id="gf-val" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0a84ff"/><stop offset="1" stop-color="#0a84ff" stop-opacity="0"/></linearGradient>' +
214
+ '</defs>' + grid +
215
+ path(valPts, "#0a84ff", "gf-val") +
216
+ path(trainPts, "#30d158", "gf-train");
217
+ }
218
+
219
+ return {
220
+ init() {
221
+ show(0);
222
+ ($("f-next0") as HTMLButtonElement).onclick = () => { if (!($("f-model") as HTMLInputElement).value.trim()) { toast("Enter a base model path.", "err"); return; } show(1); };
223
+ // OS folder dialog for the base model (native; resolves by name, no upload)
224
+ ($("f-browse") as HTMLButtonElement).onclick = pickBaseFolder;
225
+ $("f-folder").addEventListener("change", (e) => {
226
+ const input = e.target as HTMLInputElement;
227
+ const f = input.files![0] as FileWithRelPath; input.value = "";
228
+ if (f) resolveBaseByName((f.webkitRelativePath || f.name).split("/")[0]!, f.webkitRelativePath || "");
229
+ });
230
+ {
231
+ const drop = $("f-drop");
232
+ const hi = (on: boolean) => { drop.style.outline = on ? "2px dashed var(--blue)" : ""; drop.style.outlineOffset = on ? "4px" : ""; };
233
+ ["dragenter", "dragover"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); hi(true); }));
234
+ ["dragleave", "drop"].forEach((ev) => drop.addEventListener(ev, (e) => {
235
+ e.preventDefault(); hi(false);
236
+ const de = e as DragEvent;
237
+ if (ev !== "drop" || !de.dataTransfer) return;
238
+ for (const it of [...(de.dataTransfer.items || [])]) {
239
+ const en = it.webkitGetAsEntry && it.webkitGetAsEntry();
240
+ if (en && en.isDirectory) return void resolveBaseByName(en.name);
241
+ }
242
+ const f = de.dataTransfer.files && de.dataTransfer.files[0] as FileWithRelPath | undefined;
243
+ if (f) resolveBaseByName((f.webkitRelativePath || f.name).split("/")[0]!);
244
+ }));
245
+ }
246
+ ($("f-inspect") as HTMLButtonElement).onclick = inspectDataset;
247
+ $("f-data").addEventListener("keydown", (e) => { if ((e as KeyboardEvent).key === "Enter") inspectDataset(); });
248
+ $("f-data").addEventListener("input", () => { datasetOk = false; ($("f-next1") as HTMLButtonElement).disabled = true; $("f-inspect-out").innerHTML = ""; });
249
+ ($("f-next1") as HTMLButtonElement).onclick = () => { if (datasetOk) show(2); };
250
+ $("f-method").querySelectorAll<HTMLButtonElement>("button").forEach((b) => b.onclick = () => setMethod(b.dataset.v as "sft" | "dpo" | "orpo"));
251
+ $("f-orpo-scope").querySelectorAll<HTMLButtonElement>("button").forEach((b) => b.onclick = () =>
252
+ $("f-orpo-scope").querySelectorAll("button").forEach((x) => x.classList.toggle("on", x === b)));
253
+ $("f-lr").addEventListener("input", () => { lrTouched = true; });
254
+ $("f-rank").addEventListener("input", () => { rankTouched = true; });
255
+ document.querySelectorAll<HTMLButtonElement>("#s-finetune [data-fback]").forEach((b) => b.onclick = () => show(step - 1));
256
+ ($("f-submit") as HTMLButtonElement).onclick = submit;
257
+ ($("f-merge-go") as HTMLButtonElement).onclick = mergeAdapters;
258
+ ($("f-exp-go") as HTMLButtonElement).onclick = exportModel;
259
+ ($("f-push") as HTMLButtonElement).onclick = () => pushToHub($("f-push-panel"), { kind: "finetune", source_path: adapterPath || ($("f-out").textContent || "").trim() });
260
+ ($("f-again") as HTMLButtonElement).onclick = () => {
261
+ $("f-inspect-out").innerHTML = "";
262
+ $("f-merge-out").innerHTML = $("f-exp-out").innerHTML = $("f-push-panel").innerHTML = "";
263
+ show(0);
264
+ };
265
+ },
266
+ };
267
+ }