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