mlx-bun 0.0.9 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +255 -0
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +98 -0
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +18 -2
- package/docs/design/ssd-kv-cold-tier.md +288 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/decode-roofline-lookagain.md +92 -6
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/kernel-perf-review-2026-07.md +105 -26
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +251 -119
- package/docs/reference/training.md +20 -16
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +402 -107
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -16
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +618 -122
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +127 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +179 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +391 -34
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +123 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +99 -25
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +338 -32
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +61 -3
- package/src/serve/batch-scheduler.ts +1001 -76
- package/src/serve/generation-gateway.ts +292 -30
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1460 -105
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +285 -0
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +29 -26
- package/src/train/job.ts +0 -2
- package/src/train/loss.ts +53 -11
- package/src/train/prefix-shared.ts +30 -7
- package/src/train/segmented.ts +150 -46
- package/src/train/trainer.ts +22 -15
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/hub-rest.ts
ADDED
|
@@ -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
|
+
}
|