mlx-bun 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (601) hide show
  1. package/README.md +1453 -88
  2. package/THIRD_PARTY_NOTICES.md +882 -0
  3. package/bin/mlx-bun.convert.mjs +4 -0
  4. package/bin/mlx-bun.fuse.mjs +4 -0
  5. package/bin/mlx-bun.generate.mjs +4 -0
  6. package/bin/mlx-bun.lora.mjs +4 -0
  7. package/bin/mlx-bun.mjs +13 -21
  8. package/bin/mlx-bun.server.mjs +4 -0
  9. package/bin/mlx-bun.upload.mjs +4 -0
  10. package/dist/web/app.js +9584 -0
  11. package/package.json +50 -53
  12. package/src/cli/ane-install.ts +76 -0
  13. package/src/cli/args.ts +179 -0
  14. package/src/cli/engine-entry.ts +68 -0
  15. package/src/cli/inference.ts +214 -0
  16. package/src/cli/job-entry.ts +97 -0
  17. package/src/cli/library-host.ts +117 -0
  18. package/src/cli/main.ts +57 -0
  19. package/src/cli/memory-engine.ts +163 -0
  20. package/src/cli/memory.ts +36 -0
  21. package/src/cli/mlx-lm-aliases.ts +309 -0
  22. package/src/cli/model-choice.ts +33 -0
  23. package/src/cli/model-selection.ts +84 -0
  24. package/src/cli/module-host.ts +11 -0
  25. package/src/cli/module-verbs.ts +50 -0
  26. package/src/cli/numerical-policy.ts +12 -0
  27. package/src/cli/perplexity.ts +89 -0
  28. package/src/cli/serve-host.ts +423 -0
  29. package/src/cli/serve-isolated.ts +217 -0
  30. package/src/cli/serve-options.ts +106 -0
  31. package/src/cli/serve-state.ts +205 -0
  32. package/src/cli/serve.ts +500 -0
  33. package/src/cli/served-model-host.ts +102 -0
  34. package/src/cli/server-entry.ts +148 -0
  35. package/src/cli/serving-unit.ts +177 -0
  36. package/src/{tui.ts → cli/terminal.ts} +13 -8
  37. package/src/cli/worker-adapters.ts +34 -0
  38. package/src/cli/worker-entry.ts +272 -0
  39. package/src/cli/worker-unit.ts +179 -0
  40. package/src/engine/adapter-operation.ts +26 -0
  41. package/src/engine/cache-services.ts +114 -0
  42. package/src/engine/completion.ts +39 -0
  43. package/src/engine/generation-gateway.ts +358 -0
  44. package/src/engine/index.ts +59 -0
  45. package/src/engine/kv-budget.ts +120 -0
  46. package/src/engine/media-preparation.ts +232 -0
  47. package/src/engine/model-binding.ts +24 -0
  48. package/src/engine/model-host.ts +513 -0
  49. package/src/engine/model-serving.ts +89 -0
  50. package/src/{serve → engine}/preparation.ts +10 -8
  51. package/src/{serve → engine}/session-completion-engine.ts +6 -6
  52. package/src/engine/telemetry.ts +109 -0
  53. package/src/hub/downloads.ts +116 -0
  54. package/src/jobs/db.ts +16 -7
  55. package/src/jobs/events.ts +29 -0
  56. package/src/jobs/executable.ts +5 -0
  57. package/src/jobs/host.ts +51 -0
  58. package/src/jobs/in-process.ts +55 -0
  59. package/src/{contracts/jobs.ts → jobs/protocol.ts} +7 -9
  60. package/src/jobs/runner.ts +150 -231
  61. package/src/jobs/service.ts +75 -0
  62. package/src/jobs/sse.ts +59 -23
  63. package/src/jobs/worker-memory.ts +46 -0
  64. package/src/jobs/worker-process.ts +163 -0
  65. package/src/jobs/worker-supervisor.ts +258 -0
  66. package/src/modules.ts +58 -0
  67. package/src/publishing/catalog-hub.ts +18 -0
  68. package/src/publishing/credentials.ts +31 -0
  69. package/src/publishing/upload.ts +37 -0
  70. package/src/residency/leased-adapters.ts +21 -0
  71. package/src/residency/local-records.ts +16 -0
  72. package/src/residency/model-residency.ts +405 -0
  73. package/src/residency/record-index.ts +28 -0
  74. package/src/residency/resident-estimate.ts +50 -0
  75. package/src/{anthropic.ts → server/anthropic.ts} +3 -81
  76. package/src/server/cache-routes.ts +36 -0
  77. package/src/{serve → server}/chat-request.ts +12 -10
  78. package/src/{serve → server}/chat-stage.ts +27 -22
  79. package/src/{client.ts → server/client.ts} +28 -2
  80. package/src/{serve → server}/completion-executor.ts +42 -42
  81. package/src/server/decision-body.ts +55 -0
  82. package/src/server/decision-routes.ts +134 -0
  83. package/src/server/decision-wire.ts +228 -0
  84. package/src/{serve → server}/discovery-routes.ts +42 -83
  85. package/src/server/generated-token-history.ts +57 -0
  86. package/src/{serve → server}/http.ts +25 -13
  87. package/src/{serve → server}/inference-request.ts +5 -5
  88. package/src/server/job-routes.ts +20 -0
  89. package/src/server/media-prompt.ts +63 -0
  90. package/src/server/memory-completion-client.ts +46 -0
  91. package/src/server/model-routes.ts +192 -0
  92. package/src/{serve → server}/openai-wire.ts +3 -2
  93. package/src/{serve → server}/pipeline.ts +1 -1
  94. package/src/server/prompt-builder.ts +13 -0
  95. package/src/server/prompt-contracts.ts +17 -0
  96. package/src/server/proxy-routes.ts +187 -0
  97. package/src/server/publishing-routes.ts +41 -0
  98. package/src/{serve → server}/request-plan.ts +8 -8
  99. package/src/{serve → server}/request-prep.ts +82 -68
  100. package/src/{serve → server}/responses-client.ts +30 -13
  101. package/src/{responses.ts → server/responses.ts} +12 -93
  102. package/src/server/routes.ts +205 -0
  103. package/src/server/start.ts +72 -0
  104. package/src/server/status-routes.ts +108 -0
  105. package/src/{serve → server}/text-completion-stage.ts +25 -4
  106. package/src/server/text-prompt.ts +12 -0
  107. package/src/{serve → server}/token-streams.ts +36 -38
  108. package/src/server/worker-routes.ts +311 -0
  109. package/src/storage/paths.ts +61 -0
  110. package/src/web/assets.ts +37 -0
  111. package/src/web/{src → browser}/api.ts +5 -23
  112. package/src/web/browser/chat-host.ts +17 -0
  113. package/src/web/browser/installed-panels.ts +6 -0
  114. package/src/web/browser/main.ts +55 -0
  115. package/src/web/browser/markdown.ts +279 -0
  116. package/src/web/browser/memory-panel.ts +9 -0
  117. package/src/web/browser/palette.ts +64 -0
  118. package/src/web/browser/protocol.ts +13 -0
  119. package/src/web/browser/shell.ts +467 -0
  120. package/src/web/{src → browser}/status.ts +32 -36
  121. package/src/web/build.ts +51 -0
  122. package/src/web/public/app.html +1054 -0
  123. package/src/web/{sw.js → public/sw.js} +4 -3
  124. package/src/web/{vendor → public/vendor}/hljs-theme.css +2 -2
  125. package/src/web/{vendor → public/vendor}/hljs.js +2 -2
  126. package/THIRD_PARTY_LICENSES.md +0 -51
  127. package/docs/README.md +0 -94
  128. package/docs/archive/hlg-sampling.md +0 -350
  129. package/docs/archive/investigations/audio-input-plan.md +0 -356
  130. package/docs/archive/investigations/chunk-finetune-experiment.md +0 -84
  131. package/docs/archive/investigations/colibri-glm52-port.md +0 -1486
  132. package/docs/archive/investigations/colibri-oracle-pin.md +0 -1284
  133. package/docs/archive/investigations/curve-bisector-routing.md +0 -123
  134. package/docs/archive/investigations/decode-roofline-lookagain.md +0 -348
  135. package/docs/archive/investigations/diffusion-gemma-port.md +0 -602
  136. package/docs/archive/investigations/dspark-handoff.md +0 -224
  137. package/docs/archive/investigations/expert-offload-single-user-moe.md +0 -292
  138. package/docs/archive/investigations/faithful-l1-consolidation.md +0 -310
  139. package/docs/archive/investigations/hlg-sampling-investigation.md +0 -970
  140. package/docs/archive/investigations/indexshare-performance-spike.md +0 -357
  141. package/docs/archive/investigations/kernel-perf-review-2026-07.md +0 -171
  142. package/docs/archive/investigations/lab-build-journal.md +0 -157
  143. package/docs/archive/investigations/minicpm5-decode-megakernel.md +0 -489
  144. package/docs/archive/investigations/openjarvis-fit.md +0 -198
  145. package/docs/archive/investigations/openwiki-evaluation.md +0 -249
  146. package/docs/archive/investigations/optimization_plan.md +0 -346
  147. package/docs/archive/investigations/orpo-base-uf-experiment-and-directions.md +0 -120
  148. package/docs/archive/investigations/orpo-flash-cce-pin-leak.md +0 -167
  149. package/docs/archive/investigations/orpo-uf-testing-handoff.md +0 -126
  150. package/docs/archive/investigations/pareto-specialized-runtime-findings.md +0 -92
  151. package/docs/archive/investigations/pi-builtin-investigation.md +0 -308
  152. package/docs/archive/investigations/pre-colibri-stabilization.md +0 -329
  153. package/docs/archive/investigations/qwen38-27b-campaign-log.md +0 -1092
  154. package/docs/archive/investigations/segmented-backward-handoff.md +0 -148
  155. package/docs/archive/investigations/starter-model-port-handoff.md +0 -342
  156. package/docs/archive/investigations/steel-flash-cce-handoff.md +0 -489
  157. package/docs/archive/investigations/the-dreaming-handoff.md +0 -141
  158. package/docs/archive/investigations/trainer-validation-experiment.md +0 -173
  159. package/docs/archive/planning/chat-ui-vision-memory-first.md +0 -199
  160. package/docs/archive/planning/chat-ui-vision.md +0 -50
  161. package/docs/archive/planning/chunk-task-roadmap.md +0 -77
  162. package/docs/archive/planning/curve-sampler-research-plan.md +0 -331
  163. package/docs/archive/planning/journal.md +0 -316
  164. package/docs/archive/planning/memory-docs-and-dag-plan.md +0 -194
  165. package/docs/archive/planning/pi-integration-review.md +0 -178
  166. package/docs/archive/planning/release-notes-v0.0.10.md +0 -57
  167. package/docs/archive/planning/release-notes-v0.0.11.md +0 -239
  168. package/docs/archive/planning/release-notes-v0.0.12.md +0 -80
  169. package/docs/archive/planning/release-notes-v0.0.13.md +0 -77
  170. package/docs/archive/planning/release-notes-v0.0.9.md +0 -122
  171. package/docs/archive/planning/release-notes-v0.2.0.md +0 -49
  172. package/docs/archive/planning/release-notes-v0.3.0.md +0 -35
  173. package/docs/archive/planning/web-ui-pass-plan.md +0 -213
  174. package/docs/archive/planning/website-readme-pass-plan.md +0 -224
  175. package/docs/design/batching.md +0 -1841
  176. package/docs/design/decode-speed-program.md +0 -2349
  177. package/docs/design/dreaming-nightly-pipeline.md +0 -684
  178. package/docs/design/generic-model-support.md +0 -705
  179. package/docs/design/kv-cache.md +0 -824
  180. package/docs/design/orpo-training.md +0 -680
  181. package/docs/design/speculative-decoding.md +0 -1990
  182. package/docs/design/turboquant.md +0 -2212
  183. package/docs/design/unified-engine-frontier-plan.md +0 -1880
  184. package/docs/design/web-chat-redesign.md +0 -695
  185. package/docs/planning/PRODUCT_ROADMAP.md +0 -594
  186. package/docs/planning/ResearchTopics.md +0 -119
  187. package/docs/planning/release-notes-v0.4.0.md +0 -45
  188. package/docs/planning/video-series.md +0 -421
  189. package/docs/reference/benchmarks.md +0 -3613
  190. package/docs/reference/cli.md +0 -585
  191. package/docs/reference/distribution.md +0 -355
  192. package/docs/reference/environment.md +0 -398
  193. package/docs/reference/glossary.md +0 -39
  194. package/docs/reference/library-api.md +0 -674
  195. package/docs/reference/memory.md +0 -260
  196. package/docs/reference/models.md +0 -334
  197. package/docs/reference/server-api.md +0 -1775
  198. package/docs/reference/server-config.md +0 -846
  199. package/docs/reference/training.md +0 -462
  200. package/docs/reference/troubleshooting.md +0 -158
  201. package/src/admission-plan.ts +0 -18
  202. package/src/audio/conformer.ts +0 -647
  203. package/src/audio/decode.ts +0 -133
  204. package/src/audio/features.ts +0 -202
  205. package/src/audio/transcode.ts +0 -67
  206. package/src/backends/mlx/assistant-target.ts +0 -46
  207. package/src/backends/mlx/autoregressive-draft-rows.ts +0 -120
  208. package/src/backends/mlx/autoregressive.ts +0 -108
  209. package/src/backends/mlx/batch-group.ts +0 -1790
  210. package/src/backends/mlx/cache-attachment.ts +0 -67
  211. package/src/backends/mlx/cache-layout.ts +0 -31
  212. package/src/backends/mlx/checkpoint-state.ts +0 -53
  213. package/src/backends/mlx/continuation-persistence.ts +0 -106
  214. package/src/backends/mlx/continuation-request.ts +0 -43
  215. package/src/backends/mlx/continuation.ts +0 -77
  216. package/src/backends/mlx/diffusion.ts +0 -39
  217. package/src/backends/mlx/draft-target.ts +0 -32
  218. package/src/backends/mlx/evaluation.ts +0 -105
  219. package/src/backends/mlx/fill-append.ts +0 -45
  220. package/src/backends/mlx/full-kv-row-append.ts +0 -32
  221. package/src/backends/mlx/full-kv-row-donor.ts +0 -54
  222. package/src/backends/mlx/full-transitioning-kv-rows.ts +0 -65
  223. package/src/backends/mlx/gateway-binding.ts +0 -130
  224. package/src/backends/mlx/graph.ts +0 -38
  225. package/src/backends/mlx/kv-codec.ts +0 -9
  226. package/src/backends/mlx/kv-maintenance.ts +0 -133
  227. package/src/backends/mlx/kv-tensor-rows.ts +0 -212
  228. package/src/backends/mlx/memory-client.ts +0 -189
  229. package/src/backends/mlx/methods.ts +0 -90
  230. package/src/backends/mlx/model-prompt.ts +0 -164
  231. package/src/backends/mlx/model-serving.ts +0 -79
  232. package/src/backends/mlx/prefill-cohort.ts +0 -89
  233. package/src/backends/mlx/prefill-rows.ts +0 -200
  234. package/src/backends/mlx/prefill.ts +0 -33
  235. package/src/backends/mlx/projected-context-rows.ts +0 -72
  236. package/src/backends/mlx/request-policy.ts +0 -20
  237. package/src/backends/mlx/request-state-policy.ts +0 -41
  238. package/src/backends/mlx/rollback.ts +0 -89
  239. package/src/backends/mlx/row-sampling.ts +0 -49
  240. package/src/backends/mlx/serial-executor.ts +0 -242
  241. package/src/backends/mlx/speculative-group.ts +0 -359
  242. package/src/backends/mlx/speculative-round.ts +0 -161
  243. package/src/backends/mlx/speculative.ts +0 -114
  244. package/src/backends/mlx/state-rows.ts +0 -104
  245. package/src/backends/mlx/state-views.ts +0 -46
  246. package/src/backends/mlx/target-layout-capability.ts +0 -35
  247. package/src/backends/mlx/text-engine.ts +0 -73
  248. package/src/backends/mlx/transitioning-kv-rows.ts +0 -165
  249. package/src/chat-template.ts +0 -382
  250. package/src/cli.ts +0 -3168
  251. package/src/config.ts +0 -399
  252. package/src/contracts/completion.ts +0 -16
  253. package/src/contracts/execution.ts +0 -60
  254. package/src/contracts/generation.ts +0 -103
  255. package/src/contracts/host.ts +0 -7
  256. package/src/contracts/pi-provider.ts +0 -30
  257. package/src/contracts/pi.ts +0 -259
  258. package/src/contracts/prefix-cache.ts +0 -23
  259. package/src/contracts/resources.ts +0 -25
  260. package/src/contracts/sampling.ts +0 -23
  261. package/src/contracts/scheduling.ts +0 -45
  262. package/src/contracts/tsconfig.json +0 -14
  263. package/src/dataset/generators.ts +0 -1087
  264. package/src/dataset/index.ts +0 -12
  265. package/src/dataset/job.ts +0 -81
  266. package/src/dataset/llm.ts +0 -71
  267. package/src/dataset/registry.ts +0 -528
  268. package/src/diffusion/diffusion-generate.ts +0 -507
  269. package/src/download.ts +0 -505
  270. package/src/embed.ts +0 -77
  271. package/src/engine/admission.ts +0 -59
  272. package/src/engine/cancellation.ts +0 -38
  273. package/src/engine/engine.ts +0 -56
  274. package/src/engine/execution-coordinator.ts +0 -70
  275. package/src/engine/execution-plan.ts +0 -53
  276. package/src/engine/resources.ts +0 -53
  277. package/src/engine/scheduler.ts +0 -72
  278. package/src/engine/session.ts +0 -247
  279. package/src/eval/README.md +0 -130
  280. package/src/eval/ifeval.ts +0 -81
  281. package/src/eval/kl-prompts.ts +0 -29
  282. package/src/eval/kl.ts +0 -590
  283. package/src/eval/perplexity.ts +0 -164
  284. package/src/eval/quality-db.ts +0 -92
  285. package/src/eval/reference-logits.ts +0 -207
  286. package/src/eval/runner.ts +0 -124
  287. package/src/eval/score.ts +0 -49
  288. package/src/eval/tasks/bfcl.ts +0 -535
  289. package/src/eval/tasks/gsm8k.ts +0 -98
  290. package/src/eval/tasks/hashhop.ts +0 -139
  291. package/src/eval/tasks/humaneval.ts +0 -275
  292. package/src/eval/tasks/ifeval.ts +0 -525
  293. package/src/eval/tasks/mmlu.ts +0 -157
  294. package/src/eval/ultrafeedback.ts +0 -60
  295. package/src/evaldb.ts +0 -199
  296. package/src/expert-io.ts +0 -393
  297. package/src/expert-offload-build.ts +0 -77
  298. package/src/expert-offload.ts +0 -76
  299. package/src/expert-residency.ts +0 -1134
  300. package/src/expert-usage.ts +0 -415
  301. package/src/fill/echo-index.ts +0 -188
  302. package/src/fill/fill-session.ts +0 -452
  303. package/src/fill/proposal.ts +0 -68
  304. package/src/fill/schema-rows.ts +0 -506
  305. package/src/fill/tool-boundary.ts +0 -103
  306. package/src/fit.ts +0 -249
  307. package/src/generate.ts +0 -1346
  308. package/src/grammar.ts +0 -591
  309. package/src/harness-pi.ts +0 -184
  310. package/src/hf-push.ts +0 -628
  311. package/src/hub-rest.ts +0 -242
  312. package/src/index.ts +0 -63
  313. package/src/inference/denoising.ts +0 -17
  314. package/src/inference/draft-acceptance.ts +0 -41
  315. package/src/inference/draft-output.ts +0 -49
  316. package/src/inference/graph.ts +0 -33
  317. package/src/inference/prefill.ts +0 -39
  318. package/src/inference/rollback.ts +0 -7
  319. package/src/jobs/index.ts +0 -31
  320. package/src/jobs/job-entry.ts +0 -107
  321. package/src/jobs/task-client.ts +0 -19
  322. package/src/jobs/types.ts +0 -2
  323. package/src/kv-scheme.ts +0 -257
  324. package/src/kv-store.ts +0 -1099
  325. package/src/lab/README.md +0 -18
  326. package/src/lab/curve/curve-designer.html +0 -303
  327. package/src/lab/curve/curve-sampler.ts +0 -122
  328. package/src/lab/expert-trace/expert-trace.ts +0 -69
  329. package/src/lab/paged-kv/paged-kv-rows.ts +0 -83
  330. package/src/lab/paged-kv/paged-kv.ts +0 -270
  331. package/src/library.ts +0 -40
  332. package/src/lora.ts +0 -408
  333. package/src/md-modules.d.ts +0 -49
  334. package/src/media-fetch.ts +0 -442
  335. package/src/memory/article.ts +0 -558
  336. package/src/memory/chunk-validation.ts +0 -150
  337. package/src/memory/chunk.ts +0 -318
  338. package/src/memory/cluster.ts +0 -351
  339. package/src/memory/coverage.ts +0 -134
  340. package/src/memory/crosslink.ts +0 -0
  341. package/src/memory/db.ts +0 -253
  342. package/src/memory/entity.ts +0 -203
  343. package/src/memory/gate.ts +0 -187
  344. package/src/memory/history.ts +0 -152
  345. package/src/memory/ingest.ts +0 -335
  346. package/src/memory/model.ts +0 -183
  347. package/src/memory/normalize.ts +0 -671
  348. package/src/memory/parse.ts +0 -22
  349. package/src/memory/pipeline.ts +0 -275
  350. package/src/memory/prompts.ts +0 -65
  351. package/src/memory/query.ts +0 -334
  352. package/src/memory/reconcile.ts +0 -734
  353. package/src/memory/redirect.ts +0 -108
  354. package/src/memory/reindex.ts +0 -325
  355. package/src/memory/resolve.ts +0 -307
  356. package/src/memory/rest.ts +0 -245
  357. package/src/memory/route.ts +0 -272
  358. package/src/memory/schedule.ts +0 -165
  359. package/src/memory/stages.ts +0 -777
  360. package/src/memory/synthesize.ts +0 -1638
  361. package/src/memory/tools.ts +0 -749
  362. package/src/memory/vault.ts +0 -1194
  363. package/src/memory/wikify.ts +0 -1064
  364. package/src/mlx/array.ts +0 -388
  365. package/src/mlx/autograd.ts +0 -304
  366. package/src/mlx/checkpoint.ts +0 -149
  367. package/src/mlx/compile.ts +0 -168
  368. package/src/mlx/custom-vjp.ts +0 -188
  369. package/src/mlx/ffi.ts +0 -512
  370. package/src/mlx/materialize.ts +0 -23
  371. package/src/mlx/metal-kernel.ts +0 -159
  372. package/src/mlx/ops.ts +0 -1021
  373. package/src/mlx/quantized-rows.ts +0 -36
  374. package/src/mlx/token-bitmask.ts +0 -52
  375. package/src/mlx/turboquant-kv-decode.ts +0 -82
  376. package/src/mlx/turboquant-kv-inverse.ts +0 -122
  377. package/src/mlx/turboquant-ops.ts +0 -861
  378. package/src/mlx/turboquant-tables.ts +0 -237
  379. package/src/mmap.ts +0 -90
  380. package/src/model/artifact-identity.ts +0 -73
  381. package/src/model/batched-kv.ts +0 -235
  382. package/src/model/batched-mask.ts +0 -279
  383. package/src/model/batched-quant.ts +0 -212
  384. package/src/model/batched-quantized-kv.ts +0 -144
  385. package/src/model/batched-rotating-quant.ts +0 -353
  386. package/src/model/batched-rotating-state.ts +0 -149
  387. package/src/model/batched-rotating.ts +0 -338
  388. package/src/model/batched-row-storage.ts +0 -179
  389. package/src/model/batched-ssm.ts +0 -52
  390. package/src/model/batched-turboquant-kv.ts +0 -93
  391. package/src/model/compiled-decode.ts +0 -712
  392. package/src/model/decoded-kv-donor.ts +0 -12
  393. package/src/model/delayed-quantized-kv.ts +0 -72
  394. package/src/model/delayed-rotating-quantized-kv.ts +0 -93
  395. package/src/model/delayed-turboquant-kv.ts +0 -55
  396. package/src/model/diffusion-gemma.ts +0 -1026
  397. package/src/model/factory.ts +0 -189
  398. package/src/model/fingerprint.ts +0 -33
  399. package/src/model/flash-attention.ts +0 -694
  400. package/src/model/full-prefill-padding.ts +0 -56
  401. package/src/model/full-prefill-row.ts +0 -105
  402. package/src/model/gemma4-base.ts +0 -2351
  403. package/src/model/gemma4.ts +0 -1315
  404. package/src/model/generated/gemma4-12b.ts +0 -429
  405. package/src/model/generated/gemma4-26b.ts +0 -448
  406. package/src/model/generated/gemma4-e4b.ts +0 -717
  407. package/src/model/generated/index.ts +0 -18
  408. package/src/model/glm52-atlas.ts +0 -511
  409. package/src/model/glm52-cache.ts +0 -792
  410. package/src/model/glm52-config.ts +0 -262
  411. package/src/model/glm52-container.ts +0 -305
  412. package/src/model/glm52-coupling.ts +0 -388
  413. package/src/model/glm52-dsa.ts +0 -606
  414. package/src/model/glm52-expert-layout.ts +0 -253
  415. package/src/model/glm52-layout.ts +0 -152
  416. package/src/model/glm52-memory.ts +0 -500
  417. package/src/model/glm52-mla.ts +0 -792
  418. package/src/model/glm52-moe.ts +0 -223
  419. package/src/model/glm52-pilot.ts +0 -537
  420. package/src/model/glm52-reference.ts +0 -445
  421. package/src/model/glm52-residency.ts +0 -557
  422. package/src/model/glm52-resident-weights.ts +0 -135
  423. package/src/model/glm52-streamed-experts.ts +0 -453
  424. package/src/model/glm52-streamed-metal.ts +0 -610
  425. package/src/model/glm52-weights.ts +0 -503
  426. package/src/model/glm52.ts +0 -1132
  427. package/src/model/implementation.ts +0 -69
  428. package/src/model/kv-attention-view.ts +0 -67
  429. package/src/model/minicpm5.ts +0 -376
  430. package/src/model/profile.ts +0 -443
  431. package/src/model/qwen-append.ts +0 -12
  432. package/src/model/qwen-conv.ts +0 -52
  433. package/src/model/qwen3-delta-state.ts +0 -74
  434. package/src/model/qwen3-delta.ts +0 -451
  435. package/src/model/qwen3-moe.ts +0 -404
  436. package/src/model/qwen3-mrope.ts +0 -252
  437. package/src/model/qwen3.ts +0 -277
  438. package/src/model/qwen3_5-checkpoint.ts +0 -164
  439. package/src/model/qwen3_5.ts +0 -819
  440. package/src/model/rotating-kv-donor.ts +0 -35
  441. package/src/model/rotating-kv-layout.ts +0 -262
  442. package/src/model/rotating-row-transaction.ts +0 -35
  443. package/src/model/speculative-rotating-kv.ts +0 -101
  444. package/src/model/ssm-prefill-padding.ts +0 -39
  445. package/src/model/support.ts +0 -102
  446. package/src/model/trellis-balanced-scatter.ts +0 -51
  447. package/src/model/trellis-linear.ts +0 -672
  448. package/src/model/trellis-shared-m.ts +0 -151
  449. package/src/model/trellis-shared-scatter.ts +0 -144
  450. package/src/model/trellis-splitk-prefill.ts +0 -158
  451. package/src/model/trellis-tiled-prefill.ts +0 -136
  452. package/src/model/trellis-vector-expand.ts +0 -62
  453. package/src/model/trellis-wide-prefill.ts +0 -94
  454. package/src/model/turboquant-codec.ts +0 -83
  455. package/src/model/universal/archs.ts +0 -332
  456. package/src/model/universal/dense.ts +0 -612
  457. package/src/model/universal/modules.ts +0 -170
  458. package/src/model/universal/rope.ts +0 -381
  459. package/src/native/expert_io.c +0 -523
  460. package/src/native/frame_extract.swift +0 -140
  461. package/src/native-pack.ts +0 -162
  462. package/src/pi-provider.ts +0 -53
  463. package/src/pi-session.ts +0 -81
  464. package/src/pi-terminal.ts +0 -274
  465. package/src/pi-web.ts +0 -1932
  466. package/src/preflight.ts +0 -158
  467. package/src/prompt-cache.ts +0 -460
  468. package/src/quantize/allocator.ts +0 -627
  469. package/src/quantize/atomic-output.ts +0 -38
  470. package/src/quantize/calibration.ts +0 -181
  471. package/src/quantize/config-writer.ts +0 -145
  472. package/src/quantize/index.ts +0 -84
  473. package/src/quantize/job.ts +0 -162
  474. package/src/quantize/quantizer.ts +0 -666
  475. package/src/quantize/rotate.ts +0 -603
  476. package/src/quantize/safetensors-writer.ts +0 -249
  477. package/src/quantize/sensitivity.ts +0 -216
  478. package/src/quantize/trellis.ts +0 -586
  479. package/src/quantize/weight-transform.ts +0 -292
  480. package/src/registry.ts +0 -602
  481. package/src/runtime-config.ts +0 -81
  482. package/src/safetensors.ts +0 -158
  483. package/src/sampler.ts +0 -1011
  484. package/src/serve/admin-routes.ts +0 -184
  485. package/src/serve/assets/curve-terrain.html +0 -105
  486. package/src/serve/assets/training-inference-map.html +0 -575
  487. package/src/serve/aux-routes.ts +0 -132
  488. package/src/serve/batch-scheduler.ts +0 -8
  489. package/src/serve/checkpoint-identity.ts +0 -64
  490. package/src/serve/generation-gateway.ts +0 -496
  491. package/src/serve/isolate.ts +0 -609
  492. package/src/serve/kv-budget.ts +0 -14
  493. package/src/serve/lab-routes.ts +0 -341
  494. package/src/serve/lane-registry.ts +0 -45
  495. package/src/serve/model-admin-routes.ts +0 -155
  496. package/src/serve/model-binding.ts +0 -43
  497. package/src/serve/model-host.ts +0 -511
  498. package/src/serve/parent-application.ts +0 -56
  499. package/src/serve/prompt-response-trace.ts +0 -147
  500. package/src/serve/response-lease.ts +0 -24
  501. package/src/serve/session-search.ts +0 -326
  502. package/src/serve/static-routes.ts +0 -101
  503. package/src/server.ts +0 -1314
  504. package/src/spec/assistant-rows.ts +0 -92
  505. package/src/spec/assistant-source.ts +0 -126
  506. package/src/spec/cache-identity.ts +0 -22
  507. package/src/spec/deepspec-rows.ts +0 -29
  508. package/src/spec/deepspec-source.ts +0 -145
  509. package/src/spec/dflash-source.ts +0 -149
  510. package/src/spec/drafter.ts +0 -304
  511. package/src/spec/dspark/ab-stats.ts +0 -136
  512. package/src/spec/dspark/calibration.ts +0 -112
  513. package/src/spec/dspark/data-dflash.ts +0 -156
  514. package/src/spec/dspark/data.ts +0 -201
  515. package/src/spec/dspark/deepspec-module.ts +0 -776
  516. package/src/spec/dspark/generate-dflash.ts +0 -245
  517. package/src/spec/dspark/generate.ts +0 -290
  518. package/src/spec/dspark/loader.ts +0 -23
  519. package/src/spec/dspark/loss.ts +0 -213
  520. package/src/spec/dspark/module-dflash.ts +0 -560
  521. package/src/spec/dspark/module.ts +0 -540
  522. package/src/spec/dspark/quantize-drafter.ts +0 -91
  523. package/src/spec/dspark/sample.ts +0 -112
  524. package/src/spec/generate.ts +0 -276
  525. package/src/spec/glm52-mtp-graph.ts +0 -29
  526. package/src/spec/glm52-mtp-rows.ts +0 -132
  527. package/src/spec/glm52-mtp-source.ts +0 -317
  528. package/src/spec/glm52-mtp-state.ts +0 -33
  529. package/src/spec/ngram-source.ts +0 -204
  530. package/src/spec/projected-draft-rows.ts +0 -73
  531. package/src/spec/qwen-mtp-module.ts +0 -171
  532. package/src/spec/qwen-mtp-rows.ts +0 -219
  533. package/src/spec/qwen-mtp-source.ts +0 -383
  534. package/src/spec/qwen-mtp-state.ts +0 -30
  535. package/src/spec/serve-loop.ts +0 -585
  536. package/src/spec/source.ts +0 -215
  537. package/src/spec/two-model-rows.ts +0 -61
  538. package/src/spec/two-model.ts +0 -113
  539. package/src/ssd-cache.ts +0 -372
  540. package/src/ssd-durability.ts +0 -211
  541. package/src/tokenizer.ts +0 -335
  542. package/src/tool-approvals.ts +0 -131
  543. package/src/tool-call.ts +0 -695
  544. package/src/train/dataset.ts +0 -484
  545. package/src/train/diffusion-lora.ts +0 -216
  546. package/src/train/export.ts +0 -32
  547. package/src/train/flash-cce.ts +0 -1234
  548. package/src/train/forward.ts +0 -266
  549. package/src/train/fuse.ts +0 -243
  550. package/src/train/index.ts +0 -46
  551. package/src/train/job.ts +0 -205
  552. package/src/train/lora-params.ts +0 -254
  553. package/src/train/loss.ts +0 -1591
  554. package/src/train/merge.ts +0 -195
  555. package/src/train/optimizer.ts +0 -183
  556. package/src/train/prefix-shared.ts +0 -490
  557. package/src/train/rank.ts +0 -137
  558. package/src/train/segmented.ts +0 -1828
  559. package/src/train/steel-qmm-header.ts +0 -1461
  560. package/src/train/trainer.ts +0 -1221
  561. package/src/train/watch.ts +0 -542
  562. package/src/vision/diffusion-vision.ts +0 -620
  563. package/src/vision/embedder.ts +0 -168
  564. package/src/vision/preprocess.ts +0 -247
  565. package/src/vision/prompt.ts +0 -373
  566. package/src/vision/qwen3vl-preprocess.ts +0 -409
  567. package/src/vision/qwen3vl-prompt.ts +0 -196
  568. package/src/vision/qwen3vl-tower.ts +0 -555
  569. package/src/vision/siglip.ts +0 -599
  570. package/src/vision/video-frames.ts +0 -148
  571. package/src/web/app.html +0 -2143
  572. package/src/web/app.js +0 -5703
  573. package/src/web/skills/memory/SKILL.md +0 -120
  574. package/src/web/skills/web-research/SKILL.md +0 -46
  575. package/src/web/skills.ts +0 -97
  576. package/src/web/src/adapters-panel.ts +0 -288
  577. package/src/web/src/assistant.ts +0 -451
  578. package/src/web/src/chat.ts +0 -1057
  579. package/src/web/src/composer.ts +0 -959
  580. package/src/web/src/dataset.ts +0 -132
  581. package/src/web/src/finetune.ts +0 -267
  582. package/src/web/src/hub.ts +0 -428
  583. package/src/web/src/main.ts +0 -98
  584. package/src/web/src/markdown.ts +0 -0
  585. package/src/web/src/memory-panel.ts +0 -572
  586. package/src/web/src/model-picker.ts +0 -159
  587. package/src/web/src/palette.ts +0 -346
  588. package/src/web/src/protocol.ts +0 -26
  589. package/src/web/src/quantize.ts +0 -167
  590. package/src/web/src/rag.ts +0 -274
  591. package/src/web/src/sessions.ts +0 -469
  592. package/src/web/src/shell.ts +0 -804
  593. package/src/web/src/ui-catalog.ts +0 -78
  594. package/src/web/vendor/README.md +0 -61
  595. package/src/web-assets.ts +0 -47
  596. package/src/web-tools.ts +0 -478
  597. package/src/weights.ts +0 -255
  598. /package/src/{serve → server}/completion-sink.ts +0 -0
  599. /package/src/web/{icon.svg → public/icon.svg} +0 -0
  600. /package/src/web/{manifest.webmanifest → public/manifest.webmanifest} +0 -0
  601. /package/src/web/{vendor → public/vendor}/hljs-LICENSE +0 -0
@@ -1,1775 +0,0 @@
1
- # Server API
2
-
3
- `mlx-bun serve` exposes an OpenAI-compatible HTTP API on one model. The
4
- request's `model` field is ignored; the loaded model's id is echoed back.
5
- The default concurrency cap is eight. Eligible requests use continuous batching;
6
- `--batch 1` pins serial execution. Both use the shared generation session for
7
- output, cancellation and cleanup. Each model implementation supplies its own
8
- execution methods; protocol handlers do not select a concrete model class.
9
- Embedding execution also acquires the shared model/GPU lease, so it waits for
10
- active generation or model mutation before running.
11
-
12
- This is the canonical wire reference: every route the server registers is
13
- listed below, and every field documented here exists in the code that
14
- serves it (`src/server.ts`, `src/serve/*-routes.ts`, `src/serve/isolate.ts`,
15
- `src/pi-web.ts`). Start flags (`--port`, `--memory-budget`,
16
- `--prompt-cache`, `--batch`, `--kv-quant`, `--thinking`, sampling
17
- defaults) and which combinations compose live in
18
- [server-config.md](./server-config.md); the CLI in [cli.md](./cli.md).
19
-
20
- ## Route index
21
-
22
- Dispatch order matters only for `/ws/chat` (upgrade) and the static shell;
23
- every other path is unique. Unmatched paths return
24
- `404 {"error":{"message":"not found"}}`.
25
-
26
- | Method | Path | Section |
27
- | --- | --- | --- |
28
- | POST | `/v1/chat/completions` | [Chat completions](#post-v1chatcompletions) |
29
- | POST | `/v1/completions` | [Text completions](#post-v1completions-raw-text-completion) |
30
- | POST | `/v1/messages` | [Anthropic Messages](#post-v1messages-anthropic-messages-api) |
31
- | POST | `/v1/responses` | [OpenAI Responses](#post-v1responses-openai-responses-api) |
32
- | POST | `/v1/embeddings` | [Embeddings](#post-v1embeddings-openai-embeddings-api) |
33
- | GET | `/v1` | [API index](#get-v1) |
34
- | GET | `/v1/models`, `/v1/models/<id>` | [Models](#get-v1models) |
35
- | GET / POST | `/v1/adapters` | [Adapters](#adapters-lora-hot-swap) |
36
- | GET | `/v1/adapters/available` | [Adapters](#adapters-lora-hot-swap) |
37
- | DELETE | `/v1/adapters/<id>` | [Adapters](#adapters-lora-hot-swap) |
38
- | GET | `/health` | [Health](#get-health) |
39
- | GET | `/stats` | [Stats](#get-stats) |
40
- | GET | `/fit` | [Fit](#get-fit) |
41
- | GET | `/library` | [Library](#get-library) |
42
- | GET | `/downloads` | [Downloads](#get-downloads) |
43
- | POST | `/admin/cache/flush` | [SSD cache flush](#post-admincacheflush) |
44
- | GET | `/api/jobs`, `/api/jobs/<id>`, `/api/jobs/<id>/stream` | [Jobs](#jobs-apijobs) |
45
- | POST | `/api/finetune/submit`, `/inspect-dataset`, `/merge`, `/export` | [Fine-tune](#fine-tune-apifinetune) |
46
- | POST | `/api/quantize/inspect`, `/submit`, `/resolve-folder` | [Quantize](#quantize-apiquantize) |
47
- | GET / POST | `/api/dataset/templates`, `/api/dataset/submit` | [Dataset](#dataset-apidataset) |
48
- | POST | `/api/model/resolve-folder` | [Quantize](#quantize-apiquantize) |
49
- | POST | `/api/{quantize,finetune,dataset}/push` | [Push to Hub](#post-apiquantizefinetunedatasetpush) |
50
- | GET / POST | `/api/gc/plan`, `/api/gc/execute` | [GC](#get-apigcplan--post-apigcexecute) |
51
- | GET / POST | `/api/memory/*` | [Memory](#get-apimemory--post-apimemoryinit) |
52
- | GET | `/v1/memory/synthesize` | [Memory synthesis](#get-v1memorysynthesize) |
53
- | GET / POST / DELETE | `/api/settings/hf-token`, `/api/settings/tool-approvals` | [Settings](#settings-apisettings) |
54
- | GET / POST | `/api/hub/local`, `/search`, `/download`, `/serve` | [Model Hub](#model-hub-apihub) |
55
- | GET | `/api/sessions/search`, `/api/sessions/export` | [Sessions](#get-apisessionssearch--get-apisessionsexport) |
56
- | GET | `/`, `/assets/*`, `/manifest.webmanifest`, `/sw.js`, `/curves`, `/curve-terrain`, `/dag` | [Static](#web-app-static-routes--pwa-installability) |
57
- | GET | `/status`, `/chat`, `/quantize`, `/finetune`, `/dataset` | 302 to `/#<path>` ([Static](#web-app-static-routes--pwa-installability)) |
58
- | POST | `/signal`, `/generate` | [Curve Designer](#curve-designer-post-signal--post-generate) |
59
- | WS | `/ws/chat` | [Web chat WebSocket](#web-chat-websocket-wschat) |
60
-
61
- Internal routes (not part of the client contract):
62
-
63
- | Method | Path | Where | Notes |
64
- | --- | --- | --- | --- |
65
- | POST | `/admin/drain` | `src/server.ts` | Only registered when the server is bound to a unix socket (`--unix`, i.e. an `--isolate` engine child). Quiesces the gateway and demotes the whole prompt cache to the SSD tier; returns `{ "drained": true, "demotions": N }`. Never exposed on TCP. |
66
- | POST | `/admin/lease` | `src/server.ts` | Unix-socket workers only. Flushes durability, then holds the gateway's native execution lease while the response connection stays open. The isolation parent uses it for managed GPU jobs; disconnect releases the lease. Never exposed on TCP. |
67
- | GET | `/engine` | `src/serve/isolate.ts` | Only on the `--isolate` parent proxy: `{ isolated: true, pid, restarts, socket, response_store, pool?: { resident, default } }`. Worker fields describe the current default worker, or are `null` while it is evicted; inspection never loads it. |
68
-
69
- Under `--isolate`, the parent proxies everything to the engine child;
70
- `/ws/chat` is answered `501` there (web chat needs a non-isolated server),
71
- `/v1/chat/completions`, `/v1/completions`, `/v1/messages`, `/v1/responses`,
72
- and `/v1/embeddings` are routed by the body's `model` field when a
73
- `--model-pool` is configured, an unreachable child is
74
- `502 {"error":{"type":"engine_unavailable"}}`, and a client disconnect
75
- mid-proxy is `499`.
76
-
77
- ## Diagnostics: `x-mlx-bun-trace-id` and `MLX_BUN_P2R_TRACE`
78
-
79
- `/v1/chat/completions` and `/v1/completions` read an optional
80
- `x-mlx-bun-trace-id` request header. With `MLX_BUN_P2R_TRACE=1` set on the
81
- server, each of those requests emits one JSON record to stderr, prefixed
82
- `[p2r] `, when it finishes (`src/serve/prompt-response-trace.ts`). Without
83
- the env var nothing is allocated and the header is ignored.
84
-
85
- ```jsonc
86
- {
87
- "version": 1,
88
- "traceId": "<x-mlx-bun-trace-id, or the request id when absent>",
89
- "requestId": "chatcmpl-…" | "cmpl-…",
90
- "route": "/v1/chat/completions" | "/v1/completions",
91
- "clock": "monotonic-ms",
92
- "outcome": "success" | "error" | "abort",
93
- "totalMs": 0.0,
94
- "events": [ // sorted by startMs
95
- { "phase": "request.body_parse", "startMs": 0.0, "durationMs": 0.0,
96
- "attributes": { /* optional string|number|boolean|null map */ } }
97
- ],
98
- "attributes": { "stage": "body_parse" | "prepare_completion" } // on some errors
99
- }
100
- ```
101
-
102
- Phases: `request.body_parse`, `request.prompt_prepare`, `completion.total`,
103
- `completion.placement`, `engine.admission_wait`, `cache.lookup_restore`,
104
- `prefill.total`, `prefill.batch_setup`, `prefill.chunk`,
105
- `prefill.kv_maintenance`, `token_zero.total`, `token_zero.forward`,
106
- `token_zero.head`, `token_zero.sample`, `response.first_write`,
107
- `response.final_write`. Marks (`response.first_write`,
108
- `response.final_write`) have `durationMs: 0`.
109
-
110
- ## POST /v1/chat/completions
111
-
112
- Request body (OpenAI chat schema; unknown fields ignored):
113
-
114
- ```jsonc
115
- {
116
- "messages": [ /* role: system | user | assistant | tool */ ],
117
- "stream": false,
118
- "max_tokens": 1024, // or max_completion_tokens (wins); unset =
119
- // no cap — generate until EOS or the
120
- // an explicit operator limit is reached;
121
- // --max-tokens sets a server default
122
- "temperature": 0.7, // 0 = greedy
123
- "top_p": 0, "top_k": 0, // 0 = off
124
- "seed": 1234, // omit for time-derived
125
- "min_p": 0.05, // min-p sampling (0 = off)
126
- "xtc_probability": 0.5, // XTC sampling: chance per step of dropping
127
- "xtc_threshold": 0.1, // every token above threshold except the
128
- // least likely (EOS + newline exempt,
129
- // matching mlx_lm.server)
130
- "logit_bias": { "42": -5.0 }, // additive per-token-id logit bias
131
- "repetition_penalty": 1.1, // optional
132
- "repetition_context_size": 20, // recent-token window (0 = whole history)
133
- "presence_penalty": 0.0, // subtracted once if the token occurred
134
- "presence_context_size": 20, // in the window (mlx-lm extension)
135
- "frequency_penalty": 0.0, // subtracted per occurrence in the window
136
- "frequency_context_size": 20, // (mlx-lm extension)
137
- "logprobs": false, // BOOL (mlx_lm.server's type, even on
138
- // /v1/completions): return each emitted
139
- // token's logprob
140
- "top_logprobs": -1, // int in [0, 11] or -1 = unset (mlx-lm's
141
- // validation — its cap is 11, not
142
- // OpenAI's 20); k > 0 returns the top-k
143
- // (token, logprob) pairs per position
144
- "stop": "\n\n", // or ["###", "\n\n"] (spec: up to 4)
145
- "tools": [ /* OpenAI function tools */ ],
146
- "tool_choice": "auto", // "none" disables tools; "auto" and the
147
- // {type:"function", function:{name}}
148
- // object are accepted and treated as auto
149
- "chat_template_kwargs": { // forwarded to the chat template
150
- "enable_thinking": false, // MiniCPM5 / Qwen3.5/3.8 / GLM-5.2: <think>
151
- // channel on/off (Qwen3.8 default: ON)
152
- "preserve_thinking": true // Qwen3.8: keep think blocks from earlier
153
- // assistant turns in the rendered prompt
154
- // (template default true — better prompt-
155
- // cache reuse in agent loops). Ignored by
156
- // templates that don't read it.
157
- },
158
- "reasoning_effort": "medium", // "none"|"minimal"|"low"|"medium"|"high"|"xhigh"
159
- // gates enable_thinking on Qwen3.5/3.8/
160
- // MiniCPM5: "none" → off, any other level →
161
- // on. Only consulted when chat_template_
162
- // kwargs.enable_thinking is not explicitly
163
- // set — an explicit enable_thinking always
164
- // wins. On templates with a reasoning-depth
165
- // variable (Qwen3.8) the level also maps
166
- // into the template: minimal/low → low,
167
- // medium → medium, high/xhigh → xhigh.
168
- // Any OTHER string is a 400.
169
- "hlg": { // HLG tone-curve sampling (per request).
170
- "enabled": true, // merged over --hlg-sampling server defaults.
171
- "width": 4, // logit-width of the tone plateau
172
- "shoulder": 4, // top shoulder width
173
- "toe": 6, // bottom toe width
174
- "pivot_offset": 6 // pivot point offset from top
175
- },
176
- "response_format": // structured output (grammar-constrained
177
- { "type": "json_object" }, // decoding) — also guided_grammar,
178
- // guided_regex, guided_choice,
179
- // structured_outputs; see "Structured
180
- // output" below
181
- "adapter": "id" // LoRA: "id", stacked "a+b", or "none"
182
- }
183
- ```
184
-
185
- Sampling defaults follow the model author's `generation_config.json`
186
- when a field is omitted (optiq serve's gen_config behavior); explicit
187
- request values always win. MiniCPM5 defaults to the no-think direct
188
- answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
189
-
190
- Assistant history accepts `reasoning` as an alias for `reasoning_content`,
191
- including Pi's reasoning on tool-call messages. An explicit `reasoning_content`
192
- string takes precedence, even when empty. Normalization preserves the original
193
- message and exposes the canonical field to templates, so Qwen's
194
- `preserve_thinking: true` retains that history across tool turns.
195
-
196
- GLM-5.2 also defaults to no-think: its generation primer is
197
- `<think></think>`. Enabling thinking renders `Reasoning Effort: Max` in the
198
- system turn and leaves an open `<think>` primer for generation. The same
199
- precedence applies through Chat Completions, Anthropic Messages, and Responses
200
- because all three normalize into this one chat request path.
201
-
202
- `stop` sequences are matched on **decoded text**, not token ids, so a
203
- sequence that spans token boundaries still fires. Generation halts at
204
- the first match; the stop sequence itself is excluded from the content
205
- and `finish_reason` is `"stop"`.
206
-
207
- Message `content` is a string or an array of parts:
208
- `{ "type": "text", "text": ... }` and
209
- `{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }`
210
- (`{ "type": "image", ... }` is accepted as an alias; http/https URLs also
211
- accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF, BMP via native OS codecs;
212
- requires a model with the vision sidecar).
213
-
214
- Remote (http/https) media URLs — image, audio, and video alike — go
215
- through a destination policy (`src/media-fetch.ts`): **private/loopback/
216
- link-local hosts are refused by default** (the request URL is
217
- attacker-controlled input — SSRF), every redirect hop is re-validated, and
218
- the fetch has a **10 s timeout** and a **64 MB response cap** (video:
219
- **256 MB** — clips are legitimately larger; frame sampling truncates long
220
- clips server-side). Violations are clean `400`s
221
- (`prompt build failed: image url rejected: …`). Serving media from a LAN
222
- host (a NAS, another machine) is the opt-in
223
- [`--allow-private-media`](server-config.md#start-flags) flag (or
224
- `MLX_BUN_ALLOW_PRIVATE_MEDIA=1`). `data:` URLs are decoded locally and
225
- never policy-checked.
226
-
227
- ### Video input (Qwen3.5-family models)
228
-
229
- Video rides the same content-part array:
230
-
231
- ```jsonc
232
- { "type": "video_url", "video_url": { "url": "data:video/mp4;base64,…|http(s)://…" } }
233
- { "type": "video", "data": "<base64>" } // alias
234
- ```
235
-
236
- The container decodes through macOS's own AVFoundation codecs (mp4/mov,
237
- H.264/HEVC/ProRes/… — whatever this macOS plays) via the
238
- `mlx-bun-frame-extract` helper that ships beside the binary and in the
239
- native pack; from a source tree it compiles on first use with the Xcode
240
- CLT. Frames are sampled at **2 fps, capped at 768 frames**
241
- (`src/vision/video-frames.ts`), then run the gated Qwen3-VL video pipeline
242
- (temporal frame pairs, per-frame-group attention, 3D mRoPE positions).
243
- Qwen3.5-family models only; video never combines with audio parts
244
- (`400 "video and audio content parts cannot be combined"`), and a non-Qwen
245
- model answers video parts with a `400` naming the model.
246
-
247
- ### Audio input
248
-
249
- Audio rides the same content-part array (OpenAI-canonical shape plus two
250
- aliases):
251
-
252
- ```jsonc
253
- { "type": "input_audio", "input_audio": { "data": "<base64>", "format": "wav" } }
254
- { "type": "audio", "data": "<base64>" } // alias
255
- { "type": "audio_url", "audio_url": { "url": "data:…|http(s)://…" } } // alias
256
- ```
257
-
258
- (`audio_url` http/https fetches follow the same destination policy,
259
- timeout, and size cap as `image_url` — see above.)
260
-
261
- ```bash
262
- curl -s http://localhost:8080/v1/chat/completions \
263
- -H 'content-type: application/json' \
264
- -d "$(jq -n --arg b64 "$(base64 -i clip.wav)" '{
265
- messages: [{ role: "user", content: [
266
- { type: "input_audio", input_audio: { data: $b64, format: "wav" } },
267
- { type: "text", text: "Transcribe this audio." }
268
- ]}],
269
- max_tokens: 64, temperature: 0
270
- }')"
271
- ```
272
-
273
- - **Models**: any model whose `config.json` carries an `audio_config`
274
- AND whose `optiq_vision.safetensors` sidecar ships the audio-tower
275
- tensors — gemma-4 **e4b** today. Auto-detected, no flags (mirrors
276
- vision). A request WITH audio on any other model gets an explicit
277
- `400` naming the model — never a silent text-only answer.
278
- - **Formats**: WAV is decoded natively (PCM 16/24/32-bit + float32, any
279
- channel count / sample rate). mp3, m4a/AAC, FLAC, ogg(-CAF), AIFF and
280
- anything else CoreAudio reads transcode via macOS `afconvert`
281
- (`src/audio/transcode.ts`); undecodable bytes are a `400`.
282
- - **Internal format**: 16 kHz mono — multi-channel input is mean-mixed,
283
- other rates resample. Clips truncate at **30 s** (the oracle
284
- processor's 480 000-sample cap); one clip costs
285
- `ceil(duration_ms / 40)` prompt tokens, at most **750**
286
- (`src/audio/decode.ts`).
287
- - Multiple clips per message and **mixed image+audio** requests work;
288
- media splices in document order.
289
- - **Routing**: audio requests always run on the **serial lane** (never
290
- batched, even under `--batch N`) and skip the prompt cache — soft
291
- tokens are identical placeholder ids, so prefix matching across
292
- different clips would false-hit.
293
- - The Anthropic surface (`/v1/messages`) has no audio block type in its
294
- protocol; audio blocks there are a `400` pointing back to this
295
- endpoint.
296
-
297
- Non-streaming response:
298
-
299
- ```jsonc
300
- {
301
- "id": "chatcmpl-…", "object": "chat.completion", "created": 1760000000,
302
- "model": "<loaded model id>",
303
- "choices": [{
304
- "index": 0,
305
- "message": {
306
- "role": "assistant",
307
- "content": "…", // null when the reply is ONLY tool calls
308
- "reasoning": "…", // present only when the model emitted a
309
- // <think> channel (thinking-capable
310
- // templates with thinking on)
311
- "tool_calls": [{ // present when the model called tools
312
- "id": "call_…", "type": "function",
313
- "function": { "name": "…", "arguments": "{…json…}" }
314
- }]
315
- },
316
- "logprobs": { /* see below; only when requested */ },
317
- "finish_reason": "stop" | "length" | "tool_calls"
318
- }],
319
- "usage": {
320
- "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
321
- "prompt_tokens_details": { "cached_tokens": 0 }, // prompt-cache reuse
322
- "lane": "serial", // "serial" | "serial+spec" | "batched" —
323
- // which execution lane served THIS
324
- // request. Chat completions and text
325
- // completions (streaming: final usage
326
- // chunk).
327
- "speculation": { // only when a draft source is mounted
328
- "drafted": 0, // draft tokens proposed
329
- "accepted": 0, // drafts accepted by the verify step
330
- "targetCalls": 0 // target-model forward calls
331
- },
332
- "fill": { // only under MLX_BUN_FILL=strict, when a
333
- // fill table was armed for the request
334
- "events": 0, // injected spans
335
- "injected": 0, // tokens appended by the engine
336
- "strict": 0, // …of those, from schema/template rows
337
- "echo": 0, // …from the echo index (MLX_BUN_FILL=echo)
338
- "spanLens": [], // length of each span, in order
339
- "wastedSamples": 0, // in-flight samples dropped at an ASSERT fill
340
- "parseFallback": 0, // tool-call parses rejected after a fill
341
- "indexTruncated": 0, // spans cut by EOS / delimiter / budget / span
342
- "decodeSteps": 0, // tokens the model actually sampled
343
- "verifyEvents": 0, // verify-policy spans that reached the forward
344
- "verifyAccepted": 0, // …tokens the model's own argmax agreed with
345
- "verifyRejected": 0, // …tokens dropped and rewound
346
- "verifyUnsupported": 0, // verify spans dropped: caches cannot rewind
347
- "checkpointMs": 0, // time in cache checkpoint / rollback
348
- "branchStops": 0 // echo spans stopped by a history fork
349
- }
350
- }
351
- }
352
- ```
353
-
354
- `usage.lane` is server-driven, never client-inferred (the web chat's perf-strip
355
- lane badge reads it, not a heuristic): `"batched"` when `--batch N` picked up
356
- the request, `"serial+spec"` when a mounted draft source actually
357
- contributed accepted draft tokens to this reply, `"serial"` otherwise. It
358
- rides the SAME usage block on both streaming (final chunk before `[DONE]`) and
359
- non-streaming responses.
360
-
361
- `usage.completion_tokens` counts the stopping EOS token in both native and
362
- speculative serial generation, matching mlx-lm. EOS is excluded from response
363
- content. An early callback stop or token budget does not count a later EOS
364
- that happened to be evaluated in the same speculative burst.
365
- An observed EOS reports `finish_reason: "stop"`, including when its counted
366
- position equals `max_tokens`.
367
-
368
- Speculative decoding is a **server-level mode** (`serve --draft-model`,
369
- or the model-free `serve --draft-kind ngram`, which mounts no draft
370
- model); there is no per-request draft field. The `speculation` usage
371
- extension appears on chat and text completions alike, non-streaming and on
372
- the final stream chunk. Spec-eligible requests are text-only on base weights.
373
- In the unreleased shared executor, Qwen grouped MTP, prompt lookup and standalone drafting support logprobs and
374
- bf16, uniform KV4/KV8 or per-layer affine KV, or TurboQuant. Qwen MTP and prompt lookup also support
375
- positive library conversion thresholds for uniform KV4/KV8. Per-layer mixed KV and
376
- adapters remain incompatible with speculation. Strict legacy serial execution
377
- retains its narrower bf16/KV4 policy. The KV4 compatibility control is
378
- `MLX_BUN_QWEN_SPEC_KV4=0`.
379
- Ineligible requests decode normally and omit the field. Qwen MTP reuses paired
380
- target/draft prefill and completed decode state through the shared RAM/SSD cache by default in the
381
- unreleased source, reported in `cached_tokens`. Configured SSD persistence
382
- supports reuse after restart; `MLX_BUN_MTP_PROMPT_CACHE=0` disables reuse.
383
- Generated checkpoints cover processed tokens and use queued SSD persistence.
384
- Long-conversation performance acceptance remains open. Qwen prompt lookup also
385
- retains prefill and completed-output state in the common RAM/SSD cache, with
386
- its committed token history stored as a companion attachment. Standalone drafting
387
- on Qwen targets stores the draft model’s attention/recurrent state and pending
388
- last draft token as companion state, including restoration after restart. Qualified Qwen
389
- MTP, prompt lookup and standalone drafting use shared execution at one or several active rows and
390
- report `usage.lane: "batched"`. Supported full-attention and rotating targets, including
391
- Llama, MiniCPM and Gemma, use shared bf16, uniform KV4/KV8 or per-layer affine KV or TurboQuant lookup and standalone drafting, with
392
- logprobs and generated RAM/SSD checkpoints. Delayed and per-layer affine rotating speculation uses the same row lifecycle; positive conversion thresholds are a library setting, while served schemes default to zero. Gemma assistant drafting also shares execution with bf16, uniform KV4/KV8 or per-layer affine KV or TurboQuant target storage and stores its
393
- last true target hidden alongside generated RAM/SSD prefixes. Donor attention
394
- handles row validity independently of the draft graph. DeepSpec and DSpark also use these shared target layouts and the common RAM/SSD cache
395
- for projected context, with independent confidence-based proposal lengths.
396
- Other provider/target combinations still
397
- use the serial executor.
398
- Prompt-lookup concurrent performance acceptance remains open. Ineligible requests can use continuous ordinary execution
399
- when their features support it, even while a draft is mounted.
400
-
401
- `usage.fill` reports **token fast-forwarding** (`MLX_BUN_FILL=strict|echo`).
402
- Strict mode appends structural spans from the request's tools and chat
403
- template at qualified parser boundaries, with no draft, verification or
404
- rollback. The model advances its hidden states and caches through the span,
405
- using its qualified append chunk size, then resumes sampling
406
- (design: [speculative-decoding.md](../design/speculative-decoding.md) §"Token
407
- fast-forwarding"). Injected tokens are billed in `completion_tokens` like any
408
- other generated token; `decodeSteps` counts ordinary sampling steps, so
409
- `injected / (injected + decodeSteps)` is the share appended without sampling.
410
- Injected positions still run through the model layers. The field is absent when the feature is off or the
411
- request's shape refuses it (fixed `seed`, `logprobs`, structured output,
412
- media, a mounted draft model, quantized KV).
413
-
414
- Under `MLX_BUN_FILL=echo` a second, weaker source joins: spans copied from
415
- earlier in the same session, carried under policy `verify`. Those ride the same
416
- single forward, are checked against the argmax already in its logits, and have
417
- their rejected tail rewound — so `verifyAccepted + verifyRejected` is what the
418
- echo index proposed and `injected` is what survived. `wastedSamples` counts
419
- only ASSERT fills (a verify fill consumes the in-flight sample as its first
420
- position's check instead of discarding it).
421
-
422
- An explicit `seed` controls a request-local random stream and can use
423
- continuous execution. Repeating a request reproduces sampling at the same
424
- model arithmetic and batch composition. Different batch shapes can change
425
- floating-point results even with identical random draws; `--batch 1` retains
426
- the existing fixed-shape control. This differs from mlx-lm's policy of routing
427
- explicit seeds to its single-request generator.
428
-
429
- ### logprobs / top_logprobs
430
-
431
- With `"logprobs": true` and/or `"top_logprobs": k` (1–11), the
432
- non-streaming response's `choices[0].logprobs` carries
433
- **`mlx_lm.server`'s block, not OpenAI's** (server.py
434
- `generate_response`): entries are keyed by token *id*, token strings
435
- are the raw vocab pieces (`convert_ids_to_tokens` — e.g. `"▁Hello"`),
436
- and there is no `bytes` field. The same block appears on
437
- `/v1/completions` (mlx-lm shares the response builder; there is no
438
- legacy `{tokens, token_logprobs}` text-completion shape).
439
-
440
- ```jsonc
441
- // top_logprobs: k > 0 (wins over logprobs when both are set — mlx-lm's if/elif)
442
- "logprobs": { "content": [
443
- { "id": 9906, "token": "▁Hello", "logprob": -0.02, // = top-1, merged from
444
- "top_logprobs": [ // dict(top[0], top_logprobs=top)
445
- { "id": 9906, "token": "▁Hello", "logprob": -0.02 },
446
- { "id": 13347, "token": "▁Hi", "logprob": -4.1 } // … k entries, sorted desc
447
- ] },
448
- // … one entry per generated token (reasoning/tool tokens included)
449
- ] }
450
-
451
- // logprobs: true only
452
- "logprobs": { "content": [ { "id": 9906, "logprob": -0.02 }, … ] }
453
- ```
454
-
455
- The distribution matches mlx-lm `generate_step` exactly: full-vocab
456
- log-softmax of the logits **after** logits processors (logit_bias,
457
- penalties), **before** the sampler's temperature/top-p/top-k/min-p/XTC.
458
- mlx-lm's top-k order is unspecified (argpartition); ours is sorted
459
- descending — the same set, so the entry is deterministically the argmax.
460
- Stream chunks never carry logprobs (mlx-lm's streaming responses don't
461
- either). Requests with logprobs can use ordinary continuous execution under
462
- `--batch N`; other feature exclusions still apply. Invalid values are rejected
463
- with mlx-lm's exact messages (see Errors).
464
-
465
- ### Streaming
466
-
467
- `"stream": true` is SSE: `data: <chunk>\n\n` per event, terminated by
468
- `data: [DONE]`. Chunks are `chat.completion.chunk` objects whose
469
- `choices[0].delta` carries `{role: "assistant", content: ""}` first, then
470
- `{reasoning}` increments while the model is inside its `<think>` channel,
471
- then `{content}` increments (multi-byte sequences are held back until
472
- decodable; text that could begin a `stop` sequence is held back until
473
- disambiguated, so no part of a stop sequence is ever streamed), then for
474
- tool calls a `{tool_calls: [{index, id, type, function}]}` delta; the last
475
- chunk carries `finish_reason` and `usage`. A generation error mid-stream is
476
- sent as `data: {"error":{"message":…}}` and the stream closes. While a
477
- large tool-call body is buffered for parsing, the server sends SSE comment
478
- heartbeats (`: keep-alive`) so client idle timers do not terminate the call.
479
-
480
- ### Tool calls
481
-
482
- Qwen's `<parameter=...>` values are raw text. The parser preserves HTML
483
- entities, indentation and JSON string escapes, removing only one framing
484
- newline at either end. Array/object parameters are JSON-decoded according to
485
- the tool schema. The outer Qwen markup is parsed before generic JSON repair,
486
- so a nested edit array is not mistaken for the tool-call envelope.
487
-
488
- Tool round-trip: send the assistant message with its `tool_calls` back,
489
- followed by `{ "role": "tool", "tool_call_id": …, "content": … }`
490
- messages; multi-turn prompt prefixes reuse the KV prompt cache
491
- automatically.
492
-
493
- Tool-call parsing is per model family (`selectToolStreamMode` in
494
- `src/server.ts`, parsers in `src/tool-call.ts`). Gemma 4 uses its native
495
- `<|tool_call>`…`<tool_call|>` sentinel tokens. Every other family parses
496
- **decoded text** once tools are active: OpenAI-JSON
497
- `<tool_call>{…}</tool_call>`, Qwen-style
498
- `<tool_call><function=name><parameter=key>value</parameter></function></tool_call>`,
499
- GLM-5.2's `name<arg_key>…<arg_value>…`, and MiniCPM5's
500
- `<function name="…"><param name="…">…</param></function>` (CDATA-wrapped
501
- values supported). Content before the tool markup still streams live; only
502
- the markup is withheld and converted to `tool_calls`. Argument values are
503
- decoded against the tool's JSON schema (string-typed params stay strings).
504
-
505
- When strict parsing fails, a repair pass runs before giving up: a
506
- ```` ```json ```` fence or prose prefix is stripped, single quotes and
507
- trailing commas are fixed, truncated braces are closed, a function name
508
- that landed inside `arguments` is lifted into the envelope, and Python
509
- literals `True`/`False`/`None` **outside string values** are coerced to
510
- `true`/`false`/`null` (`pythonLiteralsToJson`, `src/tool-call.ts` — Qwen3.5
511
- in particular emits Python booleans). The repair is not reported on the
512
- wire; markup that still fails to parse falls back to plain content.
513
-
514
- ### Structured output (grammar-constrained decoding)
515
-
516
- Both `/v1/chat/completions` and `/v1/completions` accept the
517
- OpenAI/oMLX/vLLM structured-output fields (snake_case on the wire; the
518
- field names are `GrammarRequest` in `src/grammar.ts`):
519
-
520
- ```jsonc
521
- {
522
- "response_format": // OpenAI: {type:"json_object"} = any valid
523
- { "type": "json_schema", // JSON; {type:"json_schema"} constrains
524
- "json_schema": { // to the schema; {type:"text"} = no-op
525
- "name": "…", "schema": { /* JSON schema */ }, "strict": true,
526
- "any_whitespace": true } }, // false = compact separators (no
527
- // whitespace freedom in the grammar —
528
- // escape hatch for base models that
529
- // greedily emit whitespace to max_tokens)
530
- "guided_grammar": "root ::= …", // raw EBNF grammar string (vLLM/oMLX)
531
- "guided_regex": "[A-Z][a-z]*", // regex — the regex∩EBNF subset ONLY
532
- // (classes, |, *, +, ?, parens);
533
- // regex-only syntax (\d, ., anchors)
534
- // hits the degrade path — a known gap
535
- "guided_choice": ["yes", "no"], // output is exactly one of these strings
536
- "structured_outputs": { /* … */ } // bare JSON schema (oMLX/vLLM alias)
537
- }
538
- ```
539
-
540
- | Field | Compiles to (xgrammar) | Output |
541
- | --- | --- | --- |
542
- | `response_format: {type:"json_object"}` | builtin JSON grammar | any valid JSON |
543
- | `response_format: {type:"json_schema", json_schema:{name, schema, strict?, any_whitespace?}}` | `compileJSONSchema(schema)` | schema-conformant JSON |
544
- | `guided_grammar` | `Grammar.fromEBNF` | text matching the grammar |
545
- | `guided_regex` | EBNF-wrapped regex | text matching the regex subset |
546
- | `guided_choice` | `root ::= ("a" \| "b" \| …)` | exactly one of the strings |
547
- | `structured_outputs` | bare schema (alias for `json_schema.schema`) | schema-conformant JSON |
548
-
549
- Precedence when several are set (`resolveGrammarRequest`): `guided_grammar`
550
- > `response_format` json_schema > json_object > `structured_outputs` >
551
- `guided_regex` > `guided_choice`. `response_format: {type:"text"}` or no
552
- field at all means no constraint.
553
-
554
- Enforcement is a per-step token bitmask at the sampler (xgrammar):
555
- invalid next tokens are masked to −inf **after** the logits processors,
556
- so valid-token numerics are untouched. Generation halts as soon as the
557
- grammar is satisfied (e.g. the closing `}` of a complete JSON object)
558
- with `finish_reason: "stop"`. Works on both lanes — serial and
559
- `--batch N` (per-row matchers).
560
-
561
- Degrade path — a grammar that fails to compile is never rejected (no
562
- 400/500): chat prepends a string-valued system instruction for the
563
- requested JSON schema, raw grammar, regex, or choice constraint, and the
564
- response carries a `Warning` header (`grammar not enforced: …`);
565
- `/v1/completions` has no chat template to inject into, so it emits the
566
- `Warning` header only. `MLX_BUN_GRAMMAR=0` disables mask compilation but
567
- does not silently ignore a requested constraint: it uses this same degrade
568
- path with the hint `grammar compilation disabled by MLX_BUN_GRAMMAR=0`.
569
- Design and fidelity notes: [docs/reference/server-api.md](./server-api.md).
570
-
571
- ### Errors
572
-
573
- Memory estimates do not reject or shorten requests by default. Actual runtime
574
- failures use the request/stream error path when recoverable; native Metal
575
- allocation failure can terminate the process. Saved generation checkpoints
576
- remain available for an identical retry.
577
-
578
- All errors are `{ "error": { "message": …, ... } }`.
579
-
580
- - `400` — malformed JSON (`invalid JSON body`), empty `messages`
581
- (`messages required`), unknown adapter id, vision request on a model
582
- without a sidecar (`model has no vision sidecar`), audio request on a
583
- model without an audio tower (message names the model), video on a
584
- non-Qwen3.5 model or combined with audio, prompt build failures
585
- (`prompt build failed: …`, which includes media fetch/transcode
586
- errors), non-numeric `logit_bias` keys/values (`logit_bias must be a
587
- dict of int to float`, mlx-lm's coercion error), invalid logprobs params
588
- (mlx-lm's exact validation: `logprobs must be of type bool`,
589
- `top_logprobs must be of type int` / `at least 0` / `at most 11`;
590
- `top_logprobs: -1` is the accepted "unset" sentinel), invalid
591
- `reasoning_effort` (`reasoning_effort must be one of 'none', 'minimal',
592
- 'low', 'medium', 'high', 'xhigh'`).
593
- - `400` with `"type": "memory_admission"`, `"code":
594
- "context_over_budget"` — the PROMPT itself leaves no generation slot
595
- within an explicitly configured context/budget limit (`src/serve/request-plan.ts`). A prompt that
596
- fits is never rejected for a broad `max_tokens`: the upper bound is
597
- capped to the remaining room (`max_tokens` is a ceiling, not a promise)
598
- and generation proceeds. This is opt-in via an operator budget/context cap
599
- (or the fixed GLM-5.2 layout), never enforced from default fit estimates.
600
- `/stats.admission.enforced_context_tokens` is null when no cap applies.
601
- - `429` with `"type": "resource_admission"`, `"code": "queue_full"` when
602
- a preparation or generation queue has 64 waiting requests. If SSE headers
603
- are already sent, the stream emits its terminal error instead.
604
- - `500` — a generation failure after admission; the stack is logged
605
- server-side, the body keeps only the message.
606
-
607
- ## POST /v1/completions (raw text completion)
608
-
609
- `mlx_lm.server`'s text-completion endpoint: **no chat template** — the
610
- `prompt` string is tokenized directly (the tokenizer's own BOS handling,
611
- exactly mlx-lm's `tokenizer.encode(request.prompt)`) and the model
612
- continues it. Same generation gateway, admission control, prompt cache,
613
- and adapter selection as chat.
614
-
615
- ```jsonc
616
- {
617
- "prompt": "Once upon a time", // REQUIRED, non-empty string only (token
618
- // arrays rejected, matching mlx_lm.server)
619
- "max_tokens": 512, // unset = no cap: generate until EOS or the
620
- // an explicit operator limit (DEVIATION: mlx_lm.server
621
- // defaults 512 — pass --max-tokens 512 to
622
- // reproduce); max_completion_tokens wins
623
- "stream": false,
624
- "stop": "\n\n", // string or array, decoded-text matching
625
- // plus every sampling/penalty field from /v1/chat/completions:
626
- // temperature, top_p, top_k, seed, min_p, xtc_probability,
627
- // xtc_threshold, logit_bias, repetition_penalty,
628
- // repetition_context_size, presence_penalty, presence_context_size,
629
- // frequency_penalty, frequency_context_size, hlg, adapter,
630
- // logprobs (BOOL, mlx-lm's type — not OpenAI's legacy int),
631
- // top_logprobs (0-11 or -1) — same response block as chat, see
632
- // "logprobs / top_logprobs" above (mlx-lm shares the builder),
633
- // response_format / guided_grammar / guided_regex / guided_choice /
634
- // structured_outputs — structured output, same semantics and
635
- // precedence as chat (see "Structured output" above); the degrade
636
- // path here emits the Warning header only (no template to inject a
637
- // system prompt into)
638
- }
639
- ```
640
-
641
- Response is the OpenAI text-completion object:
642
-
643
- ```jsonc
644
- {
645
- "id": "cmpl-…", "object": "text_completion", "created": 1760000000,
646
- "model": "<loaded model id>",
647
- "choices": [{ "index": 0, "text": "…", "finish_reason": "stop" | "length",
648
- "logprobs": { /* only when requested */ } }],
649
- "usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
650
- "prompt_tokens_details": { "cached_tokens": 0 },
651
- "lane": "serial" }
652
- // + "speculation" under --draft-model, as on chat
653
- }
654
- ```
655
-
656
- Streaming (`"stream": true`) is SSE: `text_completion` chunks whose
657
- `choices[0].text` carries the delta; the final chunk carries
658
- `finish_reason` and `usage` (always attached — mlx-lm gates usage behind
659
- `stream_options.include_usage`; ours is an additive superset), then
660
- `data: [DONE]`. There is no `echo` parameter (mlx_lm.server has none).
661
- `400` on a missing/empty/non-string `prompt`, plus the same logit_bias,
662
- logprobs, and admission errors as chat.
663
-
664
- ## POST /v1/messages (Anthropic Messages API)
665
-
666
- Anthropic-protocol surface over the same engine — on by default, like
667
- `optiq serve`. Point any Anthropic-SDK tool at the server
668
- (`ANTHROPIC_BASE_URL=http://localhost:8080`, any `x-api-key`) — Claude
669
- Code works as a client this way. Translation lives in `src/anthropic.ts`;
670
- the request funnels into the chat handler above.
671
-
672
- - `system` (string or text blocks), `messages` with string or
673
- content-block arrays; `tool_use` / `tool_result` blocks map to the
674
- native tool-calling path; `image` blocks (base64 or url source) hit the
675
- vision path on sidecar models, including image blocks nested inside a
676
- `tool_result` (the computer-use shape). Other image source types are a
677
- `400`. The protocol has no audio input block; audio-shaped blocks
678
- (`input_audio` / `audio` / `audio_url`) are rejected with a `400` pointing
679
- to `/v1/chat/completions`. Prior-turn `thinking` blocks are dropped on
680
- re-ingest.
681
- - `tools` (`{name, description, input_schema}`) map to function tools;
682
- entries without `name` + `input_schema` (server-tool types such as
683
- web_search) are dropped silently.
684
- - `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
685
- `stream` as in the Anthropic spec. The mlx-lm sampler/penalty
686
- extensions (`min_p`, `xtc_probability`, `xtc_threshold`,
687
- `repetition_penalty`, `presence_penalty`, `frequency_penalty` + their
688
- `*_context_size` windows) pass through as extras; `logit_bias` does
689
- not exist in this protocol and is not accepted here.
690
- - Response: `{id: "msg_…", type: "message", role: "assistant", content:
691
- [{type: "text"} | {type: "tool_use"}…], stop_reason, stop_sequence,
692
- usage: {input_tokens, output_tokens, cache_read_input_tokens?}}` —
693
- `cache_read_input_tokens` is present when the prompt cache contributed.
694
- - Streaming follows the Anthropic event grammar exactly:
695
- `message_start → content_block_start/delta/stop (text_delta,
696
- input_json_delta) → message_delta (stop_reason + usage) →
697
- message_stop`. Errors are `event: error` frames.
698
- - Errors: `{type: "error", error: {type: "invalid_request_error" |
699
- "api_error", message}}` — the chat handler's `4xx`/`5xx` are re-wrapped
700
- into this shape.
701
-
702
- ## POST /v1/responses (OpenAI Responses API)
703
-
704
- Responses-protocol surface (Codex, Cursor, Continue, Cline, and the
705
- OpenAI SDK speak this now). Oracle: optiq responses shim; port in
706
- `src/responses.ts`.
707
-
708
- - `input` (string or item array: `message`, `function_call`,
709
- `function_call_output`), `instructions` (merged with any
710
- system/developer items into one leading system message),
711
- `max_output_tokens` (or `max_tokens`), `temperature`, `top_p`, `top_k`,
712
- flat `tools`/`tool_choice` (built-in tool types — web_search, file_search,
713
- mcp, computer_use — dropped silently; `{type:"function", name}` maps to
714
- the chat object form, `"auto"`/`"none"`/`"required"` pass through),
715
- `stream`. The mlx-lm sampler/penalty extensions (`min_p`,
716
- `xtc_probability`, `xtc_threshold`, `logit_bias`, `repetition_penalty`,
717
- `presence_penalty`, `frequency_penalty` + `*_context_size`) pass through
718
- as extras.
719
- - **`previous_response_id` resumption**: pass a prior response id
720
- instead of resending the conversation; the server splices the stored
721
- input + output back in (instructions carry forward when omitted;
722
- reasoning items are not replayed). Store is per-process, 1 h TTL,
723
- 32 MiB byte-capped LRU — observable at `GET /stats` (`response_store`).
724
- Unknown/expired id → `404`.
725
- - Response: `{id: "resp_…", object: "response", status: "completed" |
726
- "incomplete", output: [{type: "reasoning", summary: [{type:
727
- "summary_text", text}]}?, {type: "message"}, {type: "function_call"}…],
728
- usage: {input_tokens, output_tokens, total_tokens,
729
- output_tokens_details: {reasoning_tokens}}, previous_response_id}`. A
730
- `reasoning` item is present only when the model emitted a think channel.
731
- - Streaming event chain: `response.created → response.in_progress →
732
- response.output_item.added → response.content_part.added →
733
- response.output_text.delta… → response.output_text.done →
734
- response.content_part.done → response.output_item.done →
735
- response.completed` (+ `response.function_call_arguments.delta/.done`
736
- for tool calls; a reasoning output item precedes the message when the
737
- model thinks).
738
- - Errors: `{error: {message, type: "invalid_request_error" |
739
- "server_error", param: null, code: null}}`.
740
-
741
- ## POST /v1/embeddings (OpenAI Embeddings API)
742
-
743
- Text embeddings, available when the **served model is an embedding model**
744
- (plain Qwen3 / Qwen3-Embedding) — consistent with the single-model server:
745
- `mlx-bun serve mlx-community/Qwen3-Embedding-4B-4bit-DWQ`, then POST here. On
746
- any other served model the route returns `400 invalid_request_error`
747
- (`src/serve/model-admin-routes.ts`).
748
-
749
- - `input` (string or array of strings; anything else is a `400`), `model`
750
- (ignored — the served model is used), and a **non-standard**
751
- `instruction` (optional): applies Qwen3-Embedding's query format
752
- `Instruct: {instruction}\nQuery:{text}`, which steers *which* similarity
753
- axis the geometry reflects. Omit it for plain document embeddings.
754
- - Pooling is last-token + L2-normalization (the vectors are unit-length);
755
- similarity is the dot product. Bit-exact vs the mlx-lm `qwen3` reference
756
- (`tests/parity/qwen3-embed-parity.test.ts`).
757
- - Response: `{ "object": "list", "data": [{ "object": "embedding", "index":
758
- 0, "embedding": [float…] }, …], "model": "<served id>", "usage": {
759
- "prompt_tokens": N, "total_tokens": N } }`. Embedding is a single forward
760
- (no decode loop), so it runs inline — not through the generation gateway.
761
-
762
- ```sh
763
- curl localhost:8080/v1/embeddings -H 'content-type: application/json' \
764
- -d '{"input": ["the cat sat on the mat", "a kitten by the window"]}'
765
- ```
766
-
767
- ## GET /v1
768
-
769
- API index for discovery tooling:
770
-
771
- ```jsonc
772
- { "name": "mlx-bun", "version": "<package version>", "model": "<served model id>",
773
- "endpoints": ["POST /v1/chat/completions", "POST /v1/completions", "POST /v1/messages",
774
- "POST /v1/responses", "POST /v1/embeddings", "GET /v1/models",
775
- "GET/POST/DELETE /v1/adapters", "GET /health", "GET /stats", "GET /fit",
776
- "GET /library", "GET /downloads"] }
777
- ```
778
-
779
- (The list is a curated subset; this document is the full inventory.)
780
-
781
- ## GET /v1/models
782
-
783
- `{ "object": "list", "data": [{ "id": "<model id>", "object": "model",
784
- "created": <unix s>, … }] }` — `created` is the server start time for every
785
- row.
786
-
787
- The served model is FIRST, with extra capability fields:
788
-
789
- ```jsonc
790
- {
791
- "id": "<served model id>", "object": "model", "created": 1760000000,
792
- "owned_by": "mlx-bun",
793
- "context_window": 32768,
794
- "reasoning": false, // switchable thinking channel (gates the
795
- // web chat's thinking toggle)
796
- "vision": false, // image input accepted (tower loaded or
797
- // lazily loadable)
798
- "audio": false, // input_audio content parts accepted
799
- // (tower loaded or lazily loadable)
800
- "batch_mode": "batch", // loaded cache capability; per-request
801
- // usage.lane is authoritative
802
- "tools": true,
803
- "structured_output": true,
804
- "embeddings": false,
805
- "adapters": false,
806
- "training": false,
807
- "dsa": true, // GLM-5.2 only; false elsewhere
808
- "mtp": true, // checkpoint-native GLM MTP is mounted
809
- "capabilities": { // explicit discoverable surface
810
- "chat_completions": true, "text_completions": true,
811
- "anthropic_messages": true, "responses": true,
812
- "streaming": true, "tools": true, "structured_output": true,
813
- "logprobs": true, "embeddings": false,
814
- "vision": false, "audio": false,
815
- "adapters": false, "training": false
816
- },
817
- "gen_defaults": { // model-author sampling defaults
818
- "temperature": 0.7, // (generation_config.json, with any
819
- "top_p": 0.95, // --temperature/--top-p/--top-k server
820
- "top_k": 0 // override applied); null when neither
821
- // the model nor the server set one
822
- }
823
- }
824
- ```
825
-
826
- Followed by every other servable model the local registry knows (mlx-lm
827
- scans the HF cache here; the registry is that scan, filtered to supported
828
- architectures, one row per repo via `listCanonical()` — duplicate snapshots
829
- from upstream re-pushes are not separate entries):
830
-
831
- ```jsonc
832
- { "id": "<repo id>", "object": "model", "created": 1760000000,
833
- "vision": false, // visionCapable(): sidecar OR encoder-free
834
- // unified vision config
835
- "tier": "targeted" | "generic" // supportTier(): dedicated/generated forward
836
- // ("targeted") vs the Tier-0 universal
837
- // module ("generic"); entries with no tier
838
- // (unsupported) are filtered out entirely
839
- }
840
- ```
841
-
842
- `GET /v1/models/<id>` filters the list to that id — same list shape,
843
- matching `mlx_lm.server`.
844
-
845
- `vision` / `audio` say whether the served model accepts `image_url` /
846
- `input_audio` content parts (tower loaded or lazily loadable — see
847
- *Audio input* above for what audio requires), so clients can discover
848
- capabilities instead of probing for a `400`.
849
- The served-model row also includes `batch_mode: "off" | "serial" | "batch"`:
850
- the configured cap and the loaded model's cache capability together. Per-turn
851
- `usage.lane` remains authoritative for a particular request; for example,
852
- native-MTP requests route serial even when the served GLM model reports
853
- `batch_mode: "batch"` for ordinary requests.
854
- For GLM-5.2, the non-generative fields are deliberately false: embeddings,
855
- vision/audio, adapters, and training are not emulated by the serving port.
856
-
857
- ## GET /health
858
-
859
- `{"status": "ok"}` — byte-for-byte what `mlx_lm.server` returns.
860
-
861
- ## GET /stats
862
-
863
- ```jsonc
864
- {
865
- "server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
866
- "prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
867
- "response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
868
- "kv_quant": { "mode": "bf16" | "uniform-kv8" | "turbo k8v3" | "mixed (kv_config.json)",
869
- "layers": { "kv4": 8, "bf16": 40 }, // turbo: { "turbo-k8v3": 8, "bf16": 40 }
870
- "attention": { "global": 10, "sliding_window": 38 } },
871
- // present only when --ssd-cache is on:
872
- "ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
873
- "restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
874
- // bounded write-behind queue (MLX_BUN_SSD_SPILL_QUEUE_GB):
875
- "pending_snapshots": 0, "pending_spills": 0,
876
- "pending_spill_bytes": 0, "dropped_spills": 0,
877
- "failed_spills": 0, "longest_durable_prefix_tokens": 0 },
878
- "admission": {
879
- "max_safe_context": 0, // advisory fit estimate, tokens
880
- "enforced_context_tokens": null, // explicit cap or fixed layout; null = none
881
- "memory_budget_bytes": null, // explicit budget, or null
882
- "usable_bytes": 0,
883
- "weights_bytes": 0
884
- },
885
- // present only for the direct Colibri GLM-5.2 runtime:
886
- "glm52": {
887
- "preset": "g5-32gb-quality",
888
- "planned_process_bytes": 0,
889
- "process_limit_bytes": 0,
890
- "context_tokens": 4096,
891
- "max_generation_tokens": 128,
892
- "batch_size": 1,
893
- "dsa": true,
894
- "mtp": true,
895
- "mtp_draft_tokens": 3,
896
- "resident_weight_bytes": 0,
897
- "main_expert_slab_bytes": 0,
898
- "mtp_expert_slab_bytes": 0,
899
- "expert_runtime": { // null when the expert runtime is off
900
- "main_residency": { "resident": 0, "pinned": 0, "working": 0,
901
- "hits": 0, "misses": 0, "evictions": 0 },
902
- "mtp_residency": { /* same shape */ } | null,
903
- "last_turn": { "main": { "demand": { "hits": 0, "misses": 0, "readBytes": 0 } },
904
- "mtp": { "demand": { "hits": 0, "misses": 0, "readBytes": 0 } },
905
- "pilot": null },
906
- "last_repin": []
907
- }
908
- },
909
- "batch": {
910
- "configured": 1, // the --batch N value
911
- "mode": "off", // "off" | "serial" | "batch": configured
912
- // and model-cache capability together
913
- "batched": false, // true only when mode == "batch"
914
- "active_rows": 0, // rows currently decoding in the batch
915
- "pending_rows": 0, // queued + mid-prefill rows waiting
916
- "submitted_rows": 0, // cumulative rows routed to the batch lane since start
917
- "kv_bytes": 0, // projected aggregate KV bytes of admitted rows
918
- "kv_budget_bytes": null // --kv-budget cap, or null (uncapped)
919
- }
920
- }
921
- ```
922
-
923
- `kv_quant.mode` reflects the `--kv-quant` flag the server was started
924
- with; the default is `bf16` (no KV quantization). `mixed (kv_config.json)`
925
- appears only when `--kv-quant config` was passed explicitly and the
926
- checkpoint ships a per-layer `kv_config.json` — per-layer KV quantization
927
- is never selected automatically.
928
-
929
- `pending_snapshots` counts prompt-cache entries scheduled for persistence but
930
- not yet confirmed by the atomic SSD store. `longest_durable_prefix_tokens` is
931
- the longest prefix already present in the SSD index, not merely queued.
932
-
933
- ## GET /fit
934
-
935
- Fit assessment for the loaded model on this machine, plus a capability
936
- matrix across Apple SKUs. Used by the status page. Experts bytes come
937
- from the registry so MoE active-parameter predictions match the serve
938
- banner and `mlx-bun fit`. When the eval DB has a real measured decode
939
- rate for this model snapshot, it is included and takes precedence over
940
- the prediction.
941
-
942
- For the direct Colibri GLM-5.2 runtime this endpoint switches to the exact
943
- serve-time memory plan. The response separates the full streamed artifact on
944
- disk from resident weights, main/MTP expert slabs, KV, and other reserves.
945
- `measured_decode_tps` is the G5 warm quality-preserving measurement;
946
- `glm52.direct_oracle_warm_decode_tps` is the same-machine direct Colibri
947
- control, while `glm52.aspirational_decode_tps` is a target, not a release
948
- gate. The single-row SKU result describes the current machine because the
949
- generic all-resident SKU estimator is inapplicable
950
- (`report.predicted_decode_tps` is `null` there).
951
-
952
- ```jsonc
953
- {
954
- "machine": { "chip": "M4 Pro", "ram_bytes": 0, "bandwidth_gbs": 0.0 },
955
- "context_tokens": 8192, // estimated context capacity
956
- "typical_context_tokens": 8192, // min(8192, context_tokens)
957
- "typical_decode_tps": 0.0, // predicted at typical_context_tokens
958
- "measured_decode_tps": null, // real number from eval DB, or null
959
- "measured_at": null, // unix ms of measurement, or null
960
- "report": {
961
- "fits": true,
962
- "weights_bytes": 0,
963
- "kv_bytes": 0,
964
- "transient_bytes": 0,
965
- "total_bytes": 0,
966
- "usable_bytes": 0,
967
- "max_safe_context": 8192,
968
- "predicted_decode_tps": 0.0
969
- },
970
- // GLM-5.2 only:
971
- "glm52": {
972
- "artifact_disk_bytes": 0, // exact registry size; null if unavailable
973
- "main_expert_slab_bytes": 0,
974
- "mtp_expert_slab_bytes": 0,
975
- "max_generation_tokens": 128,
976
- "direct_oracle_warm_decode_tps": 0.27,
977
- "aspirational_decode_tps": 2.0
978
- },
979
- "sku_matrix_ctx": 32768,
980
- "sku_matrix": [{
981
- "sku": "M4 Pro 24 GB", "ram_gb": 24,
982
- "fits": true, "max_context": 32768, "decode_tps": 0.0
983
- }]
984
- }
985
- ```
986
-
987
- ## Adapters (LoRA hot-swap)
988
-
989
- Routes in `src/serve/model-admin-routes.ts`; the manager is
990
- `AdapterManager` in `src/lora.ts`. Requests selecting the same ordered adapter
991
- set can share a continuous group. Different sets wait for the current group
992
- to finish and then use the same executor. Prefix-cache identity is resolved
993
- under the execution lease, so replacing an adapter while a request is queued
994
- cannot reuse state from its previous weights.
995
-
996
- - `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes,
997
- mounted_layers, ram_bytes }] }` — currently-mounted adapters only.
998
- `ram_bytes` is the actual resident size of the adapter's mounted
999
- `lora_a`/`lora_b` arrays (summed `MlxArray.nbytes` — real RAM cost while
1000
- mounted, not a guess from the on-disk file size).
1001
- - `GET /v1/adapters/available` — `{ adapters: [{ id, path, rank, scale,
1002
- base_model, mounted, compatible }] }` — every adapter found on disk
1003
- (`~/.cache/mlx-bun-finetunes`, `~/.cache/mlx-bun/adapters`), unfiltered.
1004
- `compatible` is true when the adapter's recorded base model matches the
1005
- currently-served model (compared by bare repo name) or the adapter
1006
- recorded no base at all; `mounted` is true when it's already loaded.
1007
- The web chat's adapter chip uses `compatible` to gray out entries it
1008
- won't let you select rather than hiding them.
1009
- - `POST /v1/adapters` — `{ "id": "...", "path": "/dir" }` (both required,
1010
- else `400 "id and path required"`); mounts through the generation queue
1011
- (never races a forward pass). Response:
1012
- `{ id, mounted_layers, rank, scale, ram_bytes }`. `400` on shape/compat
1013
- mismatch — validation is all-or-nothing.
1014
- - `DELETE /v1/adapters/<id>` — unmount; `{ id, removed_layers }` on
1015
- success, `404 {"error":{"message":"adapter <id> not mounted"}}` otherwise.
1016
-
1017
- Select per request with the `adapter` body field (chat, text completions;
1018
- the Anthropic and Responses surfaces pass it through as an extra), which
1019
- also accepts a composed spec — `"a+b"` (or `"a,b"`) stacks two mounted
1020
- adapters, their LoRA residuals summed in order
1021
- (`AdapterManager.resolveSpec` / `parseAdapterSpec` in `src/lora.ts`);
1022
- every named id must already be mounted or the request 400s with the
1023
- unknown id named. Prompt-cache entries are namespaced per adapter spec
1024
- (including composed ones), so switching adapters — or switching which
1025
- ones are stacked — never reuses another combination's KV.
1026
-
1027
- `serve --adapter <dir>` (alias `--adapter-path`, mlx_lm.server's
1028
- spelling) mounts an adapter at startup through this same machinery and
1029
- makes it the default for requests that send no `adapter` field; an
1030
- explicit `adapter` (including `"none"`) always wins, and hot-swap via
1031
- these endpoints is unchanged.
1032
-
1033
- ## POST /admin/cache/flush
1034
-
1035
- Forces every pending SSD prompt-cache snapshot through the serial write queue
1036
- and waits for the atomic temp-file/fsync/rename boundary. The response is `200`
1037
- only when no snapshot or spill remains pending and no write failed during this
1038
- flush; otherwise it is `503` with the counters that explain why.
1039
- A snapshot missing from both RAM and usable SSD coverage remains pending
1040
- across repeated flushes. Retrying does not turn a missing snapshot into a
1041
- successful durability result. Already-durable prefixes avoid another clone
1042
- and write.
1043
-
1044
- ```jsonc
1045
- {
1046
- "durable": true,
1047
- "flushedSnapshots": 1,
1048
- "missingSnapshots": 0,
1049
- "pendingSnapshots": 0,
1050
- "pendingSpills": 0,
1051
- "pendingSpillBytes": 0,
1052
- "droppedSpills": 0,
1053
- "failedSpills": 0,
1054
- "elapsedMs": 842,
1055
- "entries": 2,
1056
- "longest_durable_prefix_tokens": 16384
1057
- }
1058
- ```
1059
-
1060
- The standard benchmark calls this endpoint before its restart leg and records
1061
- the result. Normal `mlx-bun serve` shutdown on `SIGINT` or `SIGTERM` performs
1062
- the same flush after active requests drain, with a 120-second default timeout.
1063
- Without `--ssd-cache` the route still answers (`entries: 0`).
1064
-
1065
- ## GET /library
1066
-
1067
- `?refresh=1` bypasses the short response cache, including after a parent-managed job finishes.
1068
-
1069
- Returns all models found in the local HuggingFace hub cache (via the
1070
- registry scan), each annotated with a fit assessment for this machine.
1071
- Response is cached for 30 seconds (registry scan + config reads; no
1072
- tensor bytes are read). Handler: `src/serve/discovery-routes.ts`.
1073
-
1074
- ```jsonc
1075
- {
1076
- "models": [{
1077
- "repo_id": "…",
1078
- "model_type": "gemma3" | "minicpm5" | "qwen3" | …,
1079
- "size_bytes": 0,
1080
- "quant_bits": 4,
1081
- "vision": false, // visionCapable(): sidecar OR encoder-free
1082
- // unified vision config (not sidecar-only)
1083
- "audio": false, // audio-capable (audio_config + sidecar audio-tower tensors)
1084
- "supported": true, // supportTier() !== null (recognized family)
1085
- "support_tier": "targeted" | "generic" | null, // dedicated/generated
1086
- // forward vs the Tier-0 universal module;
1087
- // null only when supported is false
1088
- "serving": false, // currently loaded in this server
1089
- "assessment": { // null if config unreadable
1090
- "fits": true,
1091
- "max_safe_context": 8192,
1092
- "predicted_decode_tps": 0.0
1093
- }
1094
- }]
1095
- }
1096
- ```
1097
-
1098
- One row per repo (`listCanonical()`; duplicate snapshots from upstream
1099
- re-pushes surface only via the CLI's `mlx-bun ls --all-revisions`).
1100
-
1101
- ## GET /downloads
1102
-
1103
- Snapshot of the last 5 model downloads (active, completed, or errored)
1104
- initiated via `mlx-bun download`, the web library panel, or
1105
- `POST /api/hub/download`.
1106
-
1107
- ```jsonc
1108
- {
1109
- "downloads": [{
1110
- "repo_id": "…",
1111
- "state": "active" | "done" | "error",
1112
- "current_file": "model.safetensors" | null,
1113
- "received_bytes": 0,
1114
- "total_bytes": 0,
1115
- "files_done": 0,
1116
- "files_total": 0,
1117
- "bytes_per_sec": 0, // rolling ~5 s window
1118
- "started_at": 1760000000,
1119
- "finished_at": null,
1120
- "error": "…" // present on state "error"
1121
- }]
1122
- }
1123
- ```
1124
-
1125
- ## Jobs (`/api/jobs`)
1126
-
1127
- The web wizards (quantize, fine-tune, dataset) run as jobs: a SQLite row
1128
- plus an NDJSON event log (`src/jobs/`). Submit routes return a `job_id`;
1129
- these routes read progress. Loopback-served admin routes, no separate auth
1130
- (same posture as every other `/api/*` route). Handler:
1131
- `src/serve/admin-routes.ts`.
1132
-
1133
- `GET /api/jobs?limit=50&kind=quantize|finetune|dataset` — most recent
1134
- jobs, newest first: `{ "ok": true, "jobs": [JobRow, …] }`.
1135
-
1136
- `GET /api/jobs/<id>` — one row, or `404 { "ok": false, "error": "job not
1137
- found" }`:
1138
-
1139
- ```jsonc
1140
- { "ok": true, "job": {
1141
- "id": "…", "kind": "quantize" | "finetune" | "dataset",
1142
- "status": "queued" | "running" | "done" | "failed" | "zombie",
1143
- "config_json": "{…}", // the submit body as stored
1144
- "progress": 0.0, // 0..1, from the latest stage/metric event
1145
- "message": "…" | null,
1146
- "log_path": "…", "output_path": "…" | null, "error": "…" | null,
1147
- "started_at": "<ISO>", "ended_at": "<ISO>" | null
1148
- } }
1149
- ```
1150
-
1151
- `GET /api/jobs/<id>/stream` — EventSource-compatible SSE tail of the job's
1152
- log: a `retry: 1500` line first, then one `data: <JobEvent>` frame per
1153
- event, polling while the job is non-terminal, then `event: end` +
1154
- `data: {}` and close. An unknown id streams a single `failed` event then
1155
- `end`. Events:
1156
-
1157
- ```jsonc
1158
- { "type": "started", "ts": 0 }
1159
- { "type": "log", "line": "…" }
1160
- { "type": "stage", "stage": "…", "progress": 0.5, "message": "…" }
1161
- { "type": "metric", "kind": "train" | "val", "step": 0, "loss": 0.0 } // + grad_norm, learning_rate, tokens_per_sec, …
1162
- { "type": "done", "ts": 0, "output_dir": "…", "summary": { } }
1163
- { "type": "failed", "error": "…", "ts": 0 }
1164
- ```
1165
-
1166
- ## Fine-tune (`/api/finetune/*`)
1167
-
1168
- The HTTP entry point to LoRA training (`src/serve/lab-routes.ts` →
1169
- `src/train/job.ts`). It runs the same runner the CLI's `mlx-bun train`
1170
- drives, as a subprocess job (crash isolation + one GPU lease across
1171
- concurrent server requests). Data formats, every config knob, and the
1172
- methodology are in [training.md](./training.md); this section is the wire
1173
- shape.
1174
-
1175
- `POST /api/finetune/submit` — body is `FinetuneSubmit` (`src/train/job.ts`).
1176
- `model_dir` and `data_dir` are required at the route (`400 "model_dir and
1177
- data_dir required"`); `adapter_path` defaults to
1178
- `~/.cache/mlx-bun/adapters/adapter-<timestamp>` when omitted. Everything
1179
- else is optional and falls back to `DEFAULT_TRAIN_CONFIG`:
1180
-
1181
- ```jsonc
1182
- {
1183
- "model_dir": "/path/to/snapshot", // required
1184
- "data_dir": "/path/to/dataset", // required: train.jsonl (+ optional valid.jsonl)
1185
- "adapter_path": "/path/to/output", // optional (default above)
1186
- "method": "sft" | "dpo" | "orpo",
1187
- "rank": 16, "scale": 20, "rank_scaling": "…", "target_modules": ["…"], "num_layers": 0,
1188
- "iters": 300, "learning_rate": 2e-4, "max_seq_length": 2048, "batch_size": 1,
1189
- "grad_accumulation_steps": 1, "seed": 0, "steps_per_report": 1, "steps_per_eval": 0,
1190
- "weight_decay": 0.0, "lora_dropout": 0.0, "rs_lora": false, "lora_plus_ratio": 1,
1191
- "grad_checkpoint": false, "mlp_split": false, "segment_size": 0,
1192
- "save_checkpoints": true, "grad_clip_norm": 0, "val_max_examples": 0,
1193
- "dpo_beta": 0.1, "dpo_warmup_iters": 0, "dpo_lr_schedule": "constant" | "cosine",
1194
- "orpo_lambda": 0.1, "orpo_warmup_iters": 0, "orpo_lr_schedule": "constant" | "cosine",
1195
- "orpo_chunk_size": 0, "orpo_fused_ce": true, "orpo_flash_ce": true, "orpo_prefix_shared": true,
1196
- "sft_scope": "full" | "response",
1197
- "warm_start_adapter": "/path/to/adapter"
1198
- }
1199
- ```
1200
-
1201
- Response: `{ "ok": true, "job_id": "…", "adapter_path": "…" }`. Then
1202
- follow `GET /api/jobs/<job_id>/stream` for `metric` events (`kind:
1203
- "train"|"val"`, `step`, `loss`, …) and the terminal `done`/`failed`.
1204
- Config validation errors (an invalid `sft_scope`, a missing `adapter_path`
1205
- at the runner) surface as the job's `failed` event, not as a `4xx`.
1206
-
1207
- ```bash
1208
- curl -s localhost:8080/api/finetune/submit -X POST -H 'content-type: application/json' -d '{
1209
- "model_dir": "/path/to/snapshot", "data_dir": "/path/to/dataset",
1210
- "adapter_path": "/path/to/output-adapter",
1211
- "method": "sft", "rank": 16, "iters": 300, "learning_rate": 2e-4, "max_seq_length": 2048
1212
- }'
1213
- # → {"ok":true,"job_id":"…","adapter_path":"/path/to/output-adapter"}
1214
- curl -N localhost:8080/api/jobs/<job_id>/stream
1215
- ```
1216
-
1217
- `POST /api/finetune/inspect-dataset` with `{"path": "<data_dir>"}` — probe
1218
- a dataset directory before submitting: `{ "ok": true, "n_train": N,
1219
- "n_valid": N, "format": "messages" | "prompt-completion" | "text" | "dpo" |
1220
- "unknown" }`, or `{ "ok": false, …, "error": "<train.jsonl> not found" }`
1221
- (HTTP 200 either way).
1222
-
1223
- `POST /api/finetune/merge` with `{"adapter_a": "/dir", "adapter_b":
1224
- "/dir", "scales"?: [a, b]}` — fold two adapters into one (`mergeAdapters`
1225
- in `src/train/merge.ts`; per-source scales default to each adapter's own
1226
- config). Output goes to `~/.cache/mlx-bun/adapters/merged-<timestamp>`:
1227
- `{ "ok": true, "merged_path": "…", "stats": { "layersMerged", "layersOnlyInOne",
1228
- "totalKeysOut", "sources", "scales" } }`; `400 { "ok": false, "error" }` on
1229
- a missing field or merge failure.
1230
-
1231
- `POST /api/finetune/export` with `{"base_model": "…", "adapter_path":
1232
- "/dir", "method"?: "…"}` — writes a `manifest.json` describing the adapter
1233
- into `~/.cache/mlx-bun/exports/export-<timestamp>`:
1234
- `{ "ok": true, "export_path": "…", "manifest": { "version": 1, "base_model",
1235
- "adapter_path", "method"?, "created_at" } }`; `400` on a missing field.
1236
-
1237
- ## Quantize (`/api/quantize/*`)
1238
-
1239
- `POST /api/quantize/inspect` with `{"model_id": "<repo id | path>"}` —
1240
- resolve a model through the registry and report whether the quantizer can
1241
- walk it (`inspectModel`, `src/quantize/job.ts`): `{ "ok": true, "model_id",
1242
- "arch": "<model_type>" | null, "support": true, "size_gb": 0.0 }`, or
1243
- `{ "ok": false, …, "error" }` (HTTP 200 either way).
1244
-
1245
- `POST /api/quantize/submit` — starts a quantize job whose output lands in
1246
- the HF hub cache as a synthetic repo `<org>/<name>-OptiQ-<suffix>`
1247
- (`refs/main` written so the registry picks it up; `<suffix>` is
1248
- `<bits>bit` or `mixed-<target_bpw>bpw`, plus `-rot<seed>` when rotating):
1249
-
1250
- ```jsonc
1251
- {
1252
- "model_id": "org/name | /path", // required (400 "model_id required")
1253
- "bits": 4, "group_size": 64,
1254
- "target_bpw": 3.5, // mixed-precision target; wins over bits in the name
1255
- "candidate_bits": [2, 3, 4, 8],
1256
- "reference": "…", "calibration_mix": "…", "n_calibration": 0,
1257
- "rotate_weights": false, "rotation_seed": 42
1258
- }
1259
- ```
1260
-
1261
- Response: `{ "ok": true, "job_id": "…", "output_dir": "…" }`. The library
1262
- cache is invalidated when the job completes.
1263
-
1264
- `POST /api/quantize/resolve-folder` and `POST /api/model/resolve-folder`
1265
- (same handler) with `{"folder_name"?: "…", "rel_path"?: "…"}` — turn a
1266
- folder the browser picked (a `models--org--name` hub dir, a snapshot hash
1267
- dir, or a bare model dir under the hub cache / `~/.cache/mlx-bun`) into a
1268
- servable path: `{ "ok": true, "path": "…", "repo_id"?: "…", "model_type"?:
1269
- "…" }`, or `{ "ok": false, "error": "Couldn't locate this folder on disk —
1270
- paste the path instead." }` (HTTP 200).
1271
-
1272
- ## Dataset (`/api/dataset/*`)
1273
-
1274
- `GET /api/dataset/templates` — the dataset-builder template registry
1275
- (`src/dataset/registry.ts`): `{ "templates": [{ "id", "label",
1276
- "description", "output_format": "messages" | "prompt_completion" | "dpo" |
1277
- "text", "needs_llm": bool, "fields": [{ "name", "type": "text" | "textarea" |
1278
- "number", "label", "hint"?, "required"?, "default"? }] }] }`.
1279
-
1280
- `POST /api/dataset/submit` with `{"template_id": "…", "inputs": {…},
1281
- "model_name"?: "…"}` — runs the template in-process as a job; LLM-backed
1282
- templates call back into this server's own `/v1` (`api_url` =
1283
- `http://127.0.0.1:<port>`). Output dir is
1284
- `~/.cache/mlx-bun/datasets/dataset-<template>-<timestamp>`. Response:
1285
- `{ "ok": true, "job_id": "…", "output_dir": "…" }`; `400 { "ok": false,
1286
- "error": "unknown template …" }` for an unknown id.
1287
-
1288
- ## POST /api/{quantize,finetune,dataset}/push
1289
-
1290
- Upload a job's output (or any folder) to the Hugging Face Hub with the
1291
- saved write token (`src/hf-push.ts`). Body: `{"repo_id": "org/name",
1292
- "job_id"?: "…", "source_path"?: "/dir", "private"?: false}` — `repo_id` is
1293
- required; the source is `source_path`, else the job's `output_path`. The
1294
- `dataset` variant pushes as a dataset repo, the others as model repos.
1295
- Response `{ "ok": true, "url": "…" }`; `400 { "ok": false, "error" }` when
1296
- no token is saved, no source directory resolves, or the upload fails.
1297
-
1298
- ## GET /api/gc/plan · POST /api/gc/execute
1299
-
1300
- Reclaim disk from superseded snapshots + orphaned blobs in the local HF
1301
- cache — the web-UI equivalent of `mlx-bun gc` (thin wrappers over
1302
- `src/registry.ts`'s `planGc`/`executeGc`; same planner the CLI uses). Every
1303
- downloaded revision the HF cache keeps a `snapshots/<commit>` dir for; a
1304
- snapshot no `refs/*` points at is superseded and, once no surviving snapshot
1305
- symlinks to a blob, that blob is dead too. Loopback-served admin routes, no
1306
- separate auth (same bind as every other `/api/*` route).
1307
-
1308
- `GET /api/gc/plan` — read-only (config.json + safetensors index reads only,
1309
- no tensor bytes, no deletion):
1310
-
1311
- ```jsonc
1312
- {
1313
- "ok": true,
1314
- "superseded": [{
1315
- "repo_id": "…",
1316
- "prune_snapshots": 1, // unreferenced, safe to delete
1317
- "skipped_snapshots": 0, // unreferenced but hold files the kept
1318
- // revision lacks — needs --force via the
1319
- // CLI to actually prune; not deletable here
1320
- "dead_blobs": 3,
1321
- "reclaim_bytes": 0
1322
- }],
1323
- "reclaim_bytes": 0 // sum across every repo
1324
- }
1325
- ```
1326
-
1327
- `POST /api/gc/execute` with body `{"yes": true}` — deletes exactly what a
1328
- fresh plan would show (recomputed at execute time, not the caller's stale
1329
- plan); `{"yes": true}` is required or the request 400s, mirroring the CLI's
1330
- `--yes` gate so a stray call can't delete anything by accident:
1331
-
1332
- ```jsonc
1333
- { "ok": true, "snapshots": 1, "blobs": 3, "reclaimed_bytes": 0 }
1334
- ```
1335
-
1336
- Errors follow the same `{ "ok": false, "error": "…" }` shape as the other
1337
- `/api/*` job routes.
1338
-
1339
- ## GET /api/memory/\* · POST /api/memory/init
1340
-
1341
- Thin loopback JSON wrappers over `src/memory/vault.ts` for the web chat's
1342
- Memory panel — same loopback-only admin-route posture as `/api/gc/*`, no
1343
- separate auth. Handlers live in `src/memory/rest.ts`; they never touch the
1344
- agent-tool surface (`src/memory/tools.ts`), which stays read-only. Every
1345
- route below degrades gracefully — a missing vault, an unknown article, or
1346
- a bad param returns `{ "ok": false, ... }` (with an appropriate HTTP
1347
- status), never a 500.
1348
-
1349
- When no vault exists yet, every `GET` route (except `/init`) returns:
1350
-
1351
- ```jsonc
1352
- { "ok": false, "enabled": false, "error": "no memory vault yet", "root": "…" }
1353
- ```
1354
-
1355
- `GET /api/memory/status` — `vaultStatus()` as JSON:
1356
-
1357
- ```jsonc
1358
- { "ok": true, "enabled": true, "status": { "root": "…", "exists": true, "articleCount": 12, "referenceCount": 9, "isGitRepo": true, "recentArticles": [{ "article": "…", "mtimeMs": 0 }] } }
1359
- ```
1360
-
1361
- `GET /api/memory/list` — article stems plus `Reference/*` doc ids, kept
1362
- separate (mirrors `memory_list`):
1363
-
1364
- ```jsonc
1365
- { "ok": true, "articles": ["Alpha", "Beta"], "reference": ["Reference/mlx-bun_README"] }
1366
- ```
1367
-
1368
- `GET /api/memory/search?q=<query>&scope=all|articles|reference&limit=50` —
1369
- wraps `searchArticles` (`limit` capped at 200; `400` without `q`); same
1370
- summaries/hits shape the `memory_search` tool formats from:
1371
-
1372
- ```jsonc
1373
- { "ok": true, "summaries": [{ "article": "…", "occurrences": 3, "matched_terms": ["…"] }], "hits": [{ "article": "…", "anchor": "…", "line": 10, "excerpt": "…" }] }
1374
- ```
1375
-
1376
- `GET /api/memory/article?name=<article>` — rendered source plus the
1377
- deterministic metadata the panel needs (infobox, lead, series banner,
1378
- section skeleton); `404 { "ok": false }` for an unknown name:
1379
-
1380
- ```jsonc
1381
- { "ok": true, "name": "…", "path": "…", "content": "# …", "infobox": { "type": "…", "entityKind": "thing", "fields": [] }, "lead": "…", "series": null, "structure": [] }
1382
- ```
1383
-
1384
- `GET /api/memory/links?name=<article>` — inbound/outbound wikilinks (same
1385
- data `memory_links` resolves); articles only, not `Reference/*`:
1386
-
1387
- ```jsonc
1388
- { "ok": true, "name": "…", "outbound": ["…"], "inbound": ["…"] }
1389
- ```
1390
-
1391
- `GET /api/memory/history?name=<article>&limit=50` — the article's git log
1392
- (read-only `git log` plumbing on the vault's own repo, via `Bun.spawn` —
1393
- never a shell string; `limit` capped at 200):
1394
-
1395
- ```jsonc
1396
- { "ok": true, "name": "…", "isGitRepo": true, "entries": [{ "hash": "…", "date": "YYYY-MM-DD", "subject": "…" }] }
1397
- ```
1398
-
1399
- `GET /api/memory/diff?name=<article>&rev=<hash>` — that commit's diff for
1400
- the article (`git show <rev> -- <path>`). `rev` must match
1401
- `/^[0-9a-f]{4,40}$/` (a bare commit hash — never a ref expression like
1402
- `HEAD~1`) or the request 400s before touching git; an unknown `name` or a
1403
- revision with no diff for it 404s:
1404
-
1405
- ```jsonc
1406
- { "ok": true, "name": "…", "rev": "…", "diff": "diff --git a/articles/…" }
1407
- ```
1408
-
1409
- `POST /api/memory/init` with optional body `{"path"?: string}` — the
1410
- first-run consent-card backend. Delegates to the same `setupVault()` the
1411
- CLI's `mlx-bun memory init` calls (idempotent: create dirs, write README +
1412
- Meta pages only if missing, git init + initial commit); omits the CLI's
1413
- interactive extras. `path`, if given, must resolve under the default vault
1414
- root or the OS temp directory, else `400`:
1415
-
1416
- ```jsonc
1417
- { "ok": true, "result": { "root": "…", "created": ["…"], "gitInitialized": true, "alreadySetUp": false }, "status": { "root": "…", "exists": true, "articleCount": 0, "referenceCount": 9, "isGitRepo": true, "recentArticles": [] } }
1418
- ```
1419
-
1420
- ## GET /v1/memory/synthesize
1421
-
1422
- Runs the memory synthesis pipeline (`runSynthesis`, `src/memory/pipeline.ts`
1423
- — the same job `mlx-bun memory synthesize` runs) and streams its progress
1424
- as SSE. `?dry=1` plans the stages without model calls or writes. This is a
1425
- long GPU job over the whole store on a real run; it is registered under
1426
- `/v1/` for historical reasons and is not part of the OpenAI surface.
1427
-
1428
- ```
1429
- data: {"type":"stage","stage":"ingest","message":"…"}
1430
- data: {"type":"log","message":"…"}
1431
- data: {"type":"done","message":"synthesis done — …"}
1432
- data: {"type":"summary","implemented":true,"stages":["ingest","segment","extract","route","create","section-route","patch","link","wikify"],"note":"…"}
1433
- data: [DONE]
1434
- ```
1435
-
1436
- A failure emits `data: {"type":"error","message":"…"}` and closes without
1437
- `[DONE]`.
1438
-
1439
- ## Settings (`/api/settings/*`)
1440
-
1441
- `GET /api/settings/hf-token` → `{ "ok": true, "hasToken": bool }`;
1442
- `POST /api/settings/hf-token` with `{"token": "hf_…"}` saves the Hugging
1443
- Face write token to `~/.mlx-bun/hf.json` (mode 0600) →
1444
- `{ "ok": true }`; `400 "token required"` when empty. The token drives the
1445
- push routes above and the `upload` CLI command ([cli.md](./cli.md)).
1446
-
1447
- `GET /api/settings/tool-approvals` / `DELETE /api/settings/tool-approvals`
1448
- — the web chat's durable "always allow this tool" list for gated tools
1449
- (`bash`/`edit`/`write`), backed by `~/.mlx-bun/tool-approvals.json`
1450
- (`src/tool-approvals.ts`), keyed by stable tool NAME, not per-call
1451
- arguments. These two routes are read/forget only: granting an always-allow
1452
- happens through the approval card itself (the `approval` WS frame on
1453
- `/ws/chat` with `alwaysAllow: true`), never a bare settings POST.
1454
-
1455
- ```jsonc
1456
- // GET
1457
- { "ok": true, "alwaysAllow": ["bash", "edit"] }
1458
- // DELETE with body {"tool": "bash"} — idempotent; returns the updated set
1459
- { "ok": true, "alwaysAllow": ["edit"] }
1460
- ```
1461
-
1462
- `DELETE` without `tool` is `400 "tool required"`.
1463
-
1464
- ## Model Hub (`/api/hub/*`)
1465
-
1466
- The web chat's Model Hub panel: browse downloaded models, search Hugging
1467
- Face, and kick off downloads — all loopback-served, no separate auth.
1468
- Handlers live in `src/hub-rest.ts` (pure functions, no loaded-model
1469
- dependency).
1470
-
1471
- `GET /api/hub/local` — every model in the local registry (same source
1472
- `/library` reads), each with a `/fit`-computed hardware verdict at a fixed
1473
- 8k context:
1474
-
1475
- ```jsonc
1476
- {
1477
- "ok": true,
1478
- "models": [{
1479
- "repo_id": "…",
1480
- "model_type": "gemma3" | "minicpm5" | "qwen3" | …,
1481
- "size_bytes": 0,
1482
- "quant_bits": 4,
1483
- "quant_group_size": 64,
1484
- "vision": false,
1485
- "supported": true,
1486
- "support_tier": "targeted" | "generic" | null,
1487
- "assessment": { "fits": true, "max_safe_context": 8192, "predicted_decode_tps": 0.0 } | null
1488
- }]
1489
- }
1490
- ```
1491
-
1492
- `GET /api/hub/search?q=<query>` — server-side Hugging Face model search,
1493
- filtered to the `mlx` library tag so results are MLX-compatible by
1494
- construction. Never downloads anything. `400` without `q`. Degrades on any
1495
- network failure (DNS, timeout, non-2xx) to an explicit offline state
1496
- rather than a 500:
1497
-
1498
- ```jsonc
1499
- { "ok": true, "offline": false, "results": [{ "id": "mlx-community/…", "downloads": 0, "likes": 0, "size_estimate": null }] }
1500
- ```
1501
-
1502
- ```jsonc
1503
- { "ok": true, "offline": true, "error": "…", "results": [] }
1504
- ```
1505
-
1506
- `size_estimate` is always `null` — the search endpoint doesn't return
1507
- per-repo file sizes; the fit badge lands once a model is actually
1508
- downloaded (`/api/hub/local`).
1509
-
1510
- `POST /api/hub/download` with body `{"repo": "org/name"}` — starts a
1511
- background download via `downloadModel()` and returns immediately;
1512
- progress is visible via [`GET /downloads`](#get-downloads). Refuses a
1513
- duplicate kick-off for a repo that's already downloading:
1514
-
1515
- ```jsonc
1516
- { "ok": true, "repo": "org/name", "started": true }
1517
- ```
1518
-
1519
- ```jsonc
1520
- { "ok": false, "error": "a download for org/name is already in progress" } // 409
1521
- ```
1522
-
1523
- `POST /api/hub/serve` with body `{"model": "org/name"}` — **always answers
1524
- `restart_required`, never performs a live in-process swap.** The process a
1525
- web chat session is attached to has exactly one model loaded and no
1526
- drop-weights-and-reload seam reachable from `/ws/chat` (the `--isolate`
1527
- proxy's `ModelPool` is a real spawn-overlap swap, but it lives on the
1528
- isolated `/v1/*` surface only — `src/serve/isolate.ts`). The honest answer
1529
- is the restart command (HTTP 200, `ok: false`):
1530
-
1531
- ```jsonc
1532
- { "ok": false, "restart_required": true, "command": "mlx-bun serve org/name" }
1533
- ```
1534
-
1535
- ## GET /api/sessions/search · GET /api/sessions/export
1536
-
1537
- Full-text search across web-chat session message **bodies** and chat
1538
- export. Same loopback-only, no-separate-auth posture as `/api/memory/*`.
1539
- Handlers live in `src/serve/session-search.ts` (pure, read-only, no
1540
- loaded-model dependency). Both routes read the same session directory the
1541
- web chat's `PiWebSession` writes to (`~/.mlx-bun/sessions`, pi's own
1542
- JSONL-per-session format) and are JSONL-tolerant — a corrupt or mid-write
1543
- line is skipped, never thrown.
1544
-
1545
- `GET /api/sessions/search?q=<query>` — case-insensitive substring scan (no
1546
- index — personal-chat-corpus scale) over every session file's
1547
- user/assistant message text, capped at 50 matches total (10 per session).
1548
- Each match reports a plain-text ±60-char snippet plus the match's own
1549
- `[start,end)` offsets **into that snippet** — offsets, not pre-rendered
1550
- HTML, so the frontend escapes the snippet then inserts its own `<mark>`:
1551
-
1552
- ```jsonc
1553
- {
1554
- "ok": true,
1555
- "results": [{
1556
- "sessionPath": "/Users/…/.mlx-bun/sessions/20260706_…jsonl",
1557
- "sessionTitle": "Chicken recipes",
1558
- "matches": [{ "snippet": "…tell me about rosemary and thyme…", "ranges": [[14, 22]], "role": "user" }]
1559
- }]
1560
- }
1561
- ```
1562
-
1563
- 400s `{"ok": false, "error": "q is required"}` without a query.
1564
-
1565
- `GET /api/sessions/export?path=<session file>` — the raw session JSONL,
1566
- parsed into an array of entries (one per line, in file order). `path` must
1567
- resolve under the session directory:
1568
-
1569
- ```jsonc
1570
- { "ok": true, "path": "…", "entries": [{ "type": "session", "id": "…", "cwd": "…" }, { "type": "message", "message": { "role": "user", "content": "…" } }] }
1571
- ```
1572
-
1573
- 400s without `path`; 403s a `path` outside the session directory; 404s a
1574
- path inside the directory that doesn't exist on disk.
1575
-
1576
- ## Web app static routes + PWA installability
1577
-
1578
- The web chat (`GET /`) and everything it loads same-origin, no CDN, ever
1579
- (`src/serve/static-routes.ts`; GET only):
1580
-
1581
- | Route | Content | Notes |
1582
- | --- | --- | --- |
1583
- | `GET /` | `src/web/app.html` | The unified SPA shell. |
1584
- | `GET /assets/app.js` | `src/web/app.js` | GENERATED from `src/web/src/*.ts` by `bun scripts/build-web.ts` — see `tests/using/web-build.test.ts`'s freshness gate. |
1585
- | `GET /assets/hljs.js`, `GET /assets/hljs.css` | `src/web/vendor/hljs*` | Vendored syntax highlighting, no CDN. |
1586
- | `GET /manifest.webmanifest` | `src/web/manifest.webmanifest` | PWA manifest. |
1587
- | `GET /assets/icon.svg` | `src/web/icon.svg` | A single inline SVG app icon; no binary PNGs are shipped. |
1588
- | `GET /sw.js` | `src/web/sw.js` | Service worker, served `cache-control: no-store` at the root scope. |
1589
- | `GET /curves` | `src/lab/curve/curve-designer.html` | The HLG Curve Designer page (drives `/signal` + `/generate` below); read from disk when available, embedded copy in compiled binaries. `no-store`. |
1590
- | `GET /curve-terrain` | `src/serve/assets/curve-terrain.html` | Curve terrain visualization; `404` when the artifact isn't on disk. |
1591
- | `GET /dag` | `src/serve/assets/training-inference-map.html` | Training/inference DAG map; `404` when the artifact isn't on disk. |
1592
- | `GET /status`, `/chat`, `/quantize`, `/finetune`, `/dataset` | — | Legacy page paths: `302` to `/#<path>` (the SPA's hash routes). |
1593
-
1594
- `/assets/*` and the manifest are served with `cache-control: public,
1595
- max-age=3600`; `/sw.js`, `/curves`, `/curve-terrain`, `/dag` are `no-store`.
1596
-
1597
- **PWA scope:** the manifest + service worker exist for *installability and
1598
- an instant static-shell paint* — "Add to Home Screen" / a browser "Install
1599
- app" prompt, and the next visit's HTML/JS/CSS loading from cache while the
1600
- WebSocket connects. This is explicitly **not** offline chat:
1601
- `src/web/sw.js`'s fetch handler only intercepts the exact shell files
1602
- (`/`, `/assets/app.js`, `/assets/hljs.js`, `/assets/hljs.css`) and lets
1603
- every dynamic route (`/api/*`, `/v1/*`, `/ws/chat`, `/downloads`, …) fall
1604
- straight through to the network unconditionally. Registration is guarded
1605
- to secure contexts (`https:` or `localhost`/`127.0.0.1`).
1606
-
1607
- ## Curve Designer: POST /signal · POST /generate
1608
-
1609
- Backends for the `/curves` page (HLG tone-curve sampling research tool,
1610
- `src/lab/curve/curve-sampler.ts`). Both answer `OPTIONS` and send permissive CORS
1611
- headers (`access-control-allow-origin: *`) so the page can be opened from
1612
- a file or another origin. Both take the GPU lock (`gateway.runExclusive`)
1613
- — they never interleave with a serving request.
1614
-
1615
- `POST /signal` with `{"prompt": "…"}` — one prefill forward of the prompt
1616
- rendered as a single user turn; bins the next-token log-prob distribution
1617
- into 80 buckets over the curve's x-axis so the editor can draw the signal
1618
- under the curve: `{ "bins": [80 ints], "vocab": V }`. `400 {"error":
1619
- "invalid JSON"}`; `500 {"error": "signal failed: …"}`.
1620
-
1621
- `POST /generate` with `{"prompt", "curve"?: {points: [...]}, "n"?: 3,
1622
- "max_tokens"?: 80, "seed"?: 0, "default"?: false}` — samples `n` (1–8)
1623
- completions of up to `max_tokens` (1–256) tokens. When `curve` has ≥2
1624
- points and `default` is not `true`, the drawn log-prob transfer curve
1625
- replaces temperature+softmax entirely; otherwise the model's default chat
1626
- recipe (temperature/top-p/top-k) is used. A non-monotone curve is a `400`.
1627
-
1628
- ```jsonc
1629
- { "mode": "curve" | "default",
1630
- "recipe": { "temperature": 0.7, "topP": 0, "topK": 0 }, // only in default mode
1631
- "n": 3, "seed": 0,
1632
- "samples": [{ "text": "…", "junk": false }] } // junk = coherence heuristic tripped
1633
- ```
1634
-
1635
- ## Web chat WebSocket (`/ws/chat`)
1636
-
1637
- `GET /ws/chat` upgrades to a WebSocket (`426` when the upgrade header is
1638
- missing; `501` under `--isolate`). It carries the web chat's pi
1639
- `AgentSession` — the model is driven through pi, which calls back into
1640
- this server's own loopback `/v1`. Frames are JSON objects with a `type`
1641
- field; the exact TypeScript unions are `ClientMessage` and `ServerMessage`
1642
- in `src/pi-web.ts` (the web frontend imports them type-only, so both ends
1643
- are compile-checked).
1644
-
1645
- Client → server: `prompt {text, images?: [{data, mimeType}]}`, `abort`,
1646
- `approval {callId, decision, editedArgs?, alwaysAllow?}`, `set_thinking
1647
- {enabled}`, `set_coding_tools {enabled}`, `set_adapter {id | null}`,
1648
- `set_sampling {scope?: "session" | "next_turn", temperature?, top_p?,
1649
- top_k?, min_p?, xtc_probability?, xtc_threshold?, repetition_penalty?,
1650
- repetition_context_size?, presence_penalty?, frequency_penalty?, seed?}`
1651
- (null = server default), `set_system_prompt {text | null}`, `new_session`,
1652
- `list_sessions`, `open_session {path}`, `fork_session {path}`,
1653
- `delete_session {path}`, `regenerate`, `edit_resend {text}`,
1654
- `switch_sibling {entryId}`, `context {context}` (app-aware assistant, below).
1655
-
1656
- Server → client: `ready {model, vision, audio, thinking, genDefaults}`,
1657
- `turn_start`, `text_delta {delta}`, `thinking_delta {delta}`, `tool_start
1658
- {callId, tool, args}`, `tool_approval_request {callId, tool, args}`,
1659
- `tool_update {callId, chunk}`, `tool_end {callId, ok, result}`, `turn_end
1660
- {lane?}` (the serving lane the turn ran on, from `usage.lane` via the lane
1661
- registry — never guessed client-side), `queue_update {steering, followUp}`,
1662
- `history {items}`, `sessions {items, activePath?}`, `siblings {entryId?,
1663
- index, count, siblingIds}`, `context {tokens, contextWindow, percent}`
1664
- (context-window usage — a different frame from the client→server
1665
- `context`), `coding_tools {active, pending}`, `tool_approvals
1666
- {alwaysAllow}`, `ui_navigate {route}`, `ui_spotlight {ref?, label?,
1667
- selector?, target?, message?, route?}`, `error {message}`.
1668
-
1669
- ### App-aware assistant
1670
-
1671
- The web chat agent can see and act on the app it lives in. No screenshots
1672
- and no vision model are involved: the browser sends a structured DOM
1673
- snapshot as a WS frame, and the model gets three tools that
1674
- read/navigate/highlight that snapshot. The three tools are always in the
1675
- web chat's tool allowlist (read-only-on-the-machine, never gated by the
1676
- approval card; `APP_AWARE_TOOL_NAMES` in `src/pi-web.ts`):
1677
-
1678
- - **`get_current_app_context`** — returns the last context the browser
1679
- pushed: `{ route, view?, step?, snapshot }`. `snapshot` is a capped
1680
- (~120) list of visible interactive elements as `{ref, label, kind,
1681
- role?, selector, spotlightId?}`, agent chrome excluded.
1682
- - **`navigate_app({ route | page })`** — validates against the route
1683
- catalog (`APP_ROUTE_IDS`: `chat`, `quantize`, `finetune`, `dataset`,
1684
- `status`; unknown routes are a tool error, never sent to the browser)
1685
- and emits a `ui_navigate` frame.
1686
- - **`spotlight_ui({ ref | label | selector | target, route?, message? })`**
1687
- — emits a `ui_spotlight` frame; the browser resolves the target (ref
1688
- from its last snapshot, then live selector, then fuzzy label match, then
1689
- a curated catalog id) and shows a brief non-blocking highlight.
1690
-
1691
- Client → server WS frame (pushed by the browser on every route change and
1692
- every wizard-step change, never on a timer):
1693
-
1694
- ```jsonc
1695
- { "type": "context", "context": { "route": "quantize", "step": { "index": 1, "count": 4, "label": "Configure" }, "snapshot": { "route": "quantize", "capturedAt": "…", "elements": [/* … */] } } }
1696
- ```
1697
-
1698
- Server → client frames the two action tools produce:
1699
-
1700
- ```jsonc
1701
- { "type": "ui_navigate", "route": "quantize" }
1702
- { "type": "ui_spotlight", "target": "quantize-source", "message": "Paste a Hugging Face repo id or local path here" }
1703
- ```
1704
-
1705
- The server also auto-prepends a compact one-line ambient context (e.g.
1706
- `[user is on: Quantize · step 2/4]`) to the system prompt on every turn —
1707
- never a full snapshot dump. Approval-gated form-fill / job-start actions
1708
- are not wired; only navigate/spotlight are.
1709
-
1710
- ## Client setup: pi
1711
-
1712
- The supported one-command path:
1713
-
1714
- ```bash
1715
- mlx-bun harness pi # install; then: pi --provider mlx-bun
1716
- mlx-bun harness pi --remove # undo
1717
- ```
1718
-
1719
- It writes a self-contained extension to
1720
- `~/.pi/agent/extensions/mlx-bun-provider.ts` that registers the `mlx-bun`
1721
- provider with live discovery: at pi startup it fetches `/v1/models` from the
1722
- running server, so the stable `mlx-bun/local` model handle always resolves to
1723
- whatever the server is actually serving (context window, reasoning, and
1724
- vision capability included), and never goes stale across model swaps. The
1725
- model list baked at install time is only the fallback for when the server is
1726
- down.
1727
-
1728
- Manual fallback — `~/.pi/agent/models.json`:
1729
-
1730
- ```jsonc
1731
- {
1732
- "providers": {
1733
- "mlx-bun": {
1734
- "baseUrl": "http://127.0.0.1:8080/v1",
1735
- "api": "openai-completions",
1736
- "apiKey": "sk-anything-nonempty",
1737
- "models": [{ "id": "<model id from /v1/models>" }]
1738
- }
1739
- }
1740
- }
1741
- ```
1742
-
1743
- Note the manual route bakes a concrete model id, which goes stale when you
1744
- serve a different model — the staleness `harness pi`'s `local` id exists to
1745
- solve.
1746
-
1747
- Any OpenAI SDK works the same way: `baseURL: "http://127.0.0.1:8080/v1"`,
1748
- any non-empty `apiKey`.
1749
-
1750
- Under `--isolate`, the parent owns Responses continuation history and exposes its
1751
- bounded store counters in `GET /engine` as `response_store`. Model-worker restart
1752
- or eviction preserves that history; restarting the parent clears it. Direct
1753
- serving retains the same process-local TTL and byte limits.
1754
-
1755
-
1756
- Supported grouped TurboQuant drafting retains `usage.speculation`, seeded
1757
- sampling, logprobs, structured output and generated RAM/SSD continuations.
1758
- The cache codec does not select the scheduler or sampler implementation.
1759
- Composition coverage: [batching design](../design/batching.md).
1760
-
1761
- With paged KV enabled, supported Gemma4 bf16 text requests report the batched lane when submitted through continuous scheduling. Seeded sampling, logprobs and grammar use the same interfaces. These requests bypass prompt-cache lookup/publication; paging does not provide SSD persistence.
1762
-
1763
- Ordinary adapter checkpoints are isolated by ordered adapter IDs and mounted
1764
- content revisions. Base-model requests cannot restore or remove an adapter checkpoint.
1765
- With generation checkpointing enabled, repeating an identical eligible request
1766
- replays its saved completion prefix and resumes from the saved pending token.
1767
- Cancellation retains queued snapshots; successful completion removes them.
1768
- `POST /admin/cache/flush` drains both prompt spills and generation checkpoints,
1769
- and its elapsed time and durability statistics include both queues. A hard
1770
- process exit can lose snapshots that have not drained.
1771
-
1772
- For resumed generation checkpoints, `usage.prompt_tokens_details.cached_tokens`
1773
- counts only the reused original prompt, bounded by `usage.prompt_tokens`. Saved
1774
- completion tokens are replayed as completion output and do not increase that
1775
- prompt-cache usage counter.