mlx-bun 0.2.0 → 0.4.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.
- package/README.md +71 -588
- package/docs/README.md +94 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/archive/planning/release-notes-v0.3.0.md +35 -0
- package/docs/design/batching.md +1841 -0
- package/docs/design/decode-speed-program.md +2346 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +655 -334
- package/docs/design/kv-cache.md +824 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1990 -0
- package/docs/design/turboquant.md +2212 -0
- package/docs/design/unified-engine-frontier-plan.md +1833 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +50 -1
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +3613 -0
- package/docs/reference/cli.md +434 -160
- package/docs/reference/distribution.md +328 -182
- package/docs/reference/environment.md +398 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +113 -6
- package/docs/reference/server-api.md +944 -429
- package/docs/reference/server-config.md +740 -338
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +158 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1790 -0
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +32 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +130 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +89 -0
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +242 -0
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +46 -0
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +66 -58
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +60 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +25 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +45 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +53 -0
- package/src/engine/resources.ts +53 -0
- package/src/engine/scheduler.ts +72 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +30 -16
- package/src/generate.ts +709 -417
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +12 -12
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +177 -63
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/{model → lab/paged-kv}/paged-kv.ts +32 -15
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +1 -3
- package/src/mlx/array.ts +11 -11
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +20 -6
- package/src/model/batched-quant.ts +9 -3
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +78 -17
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +65 -23
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +8 -2
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +179 -80
- package/src/model/gemma4.ts +40 -32
- package/src/model/generated/gemma4-12b.ts +6 -3
- package/src/model/generated/gemma4-26b.ts +6 -3
- package/src/model/generated/gemma4-e4b.ts +6 -3
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/implementation.ts +69 -0
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +17 -15
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +202 -78
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +15 -14
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +110 -69
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +40 -29
- package/src/serve/batch-scheduler.ts +8 -1671
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +183 -213
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +511 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +23 -18
- package/src/serve/request-prep.ts +344 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +335 -2711
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +31 -55
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +16 -14
- package/src/spec/dflash-source.ts +36 -12
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +97 -122
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +129 -172
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -137
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +93 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +147 -195
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +201 -201
- package/src/spec/source.ts +110 -13
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +44 -102
- package/src/ssd-cache.ts +99 -19
- package/src/ssd-durability.ts +36 -20
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +62 -60
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// Wire-level chat/completion request shapes and the pure per-request
|
|
2
|
+
// helpers: message normalization, sampling-field coercion, grammar-degrade
|
|
3
|
+
// prompt injection, and the mlx_lm.server-parity validators. No model, no
|
|
4
|
+
// server state. Extracted from src/server.ts (repo-taming Phase 4).
|
|
5
|
+
import type { ChatMessage, ToolDefinition } from "../chat-template";
|
|
6
|
+
import type { HlgConfig } from "../sampler";
|
|
7
|
+
import { RequestError } from "./pipeline";
|
|
8
|
+
|
|
9
|
+
export interface ChatRequestParams {
|
|
10
|
+
messages: ChatMessage[];
|
|
11
|
+
stream?: boolean;
|
|
12
|
+
max_tokens?: number;
|
|
13
|
+
max_completion_tokens?: number;
|
|
14
|
+
temperature?: number;
|
|
15
|
+
top_p?: number;
|
|
16
|
+
top_k?: number;
|
|
17
|
+
seed?: number;
|
|
18
|
+
repetition_penalty?: number;
|
|
19
|
+
/** mlx-lm extension: recent-token window for repetition_penalty
|
|
20
|
+
* (default 20; 0 = whole history, Python `[-0:]` semantics). */
|
|
21
|
+
repetition_context_size?: number;
|
|
22
|
+
/** min-p sampling (mlx_lm.server's `min_p`): keep tokens whose probability
|
|
23
|
+
* is ≥ min_p · p(top token). 0 = off. */
|
|
24
|
+
min_p?: number;
|
|
25
|
+
/** XTC sampling (mlx_lm.server names): with probability `xtc_probability`
|
|
26
|
+
* per step, remove every token above `xtc_threshold` except the least
|
|
27
|
+
* likely of them. EOS + the newline token are always exempt (the server
|
|
28
|
+
* injects them as xtc special tokens, matching mlx_lm.server). */
|
|
29
|
+
xtc_probability?: number;
|
|
30
|
+
xtc_threshold?: number;
|
|
31
|
+
/** OpenAI logit_bias: {tokenId: additive bias}. JSON object keys arrive as
|
|
32
|
+
* strings; coerced to int keys / float values like mlx-lm (400 on failure). */
|
|
33
|
+
logit_bias?: Record<string, number>;
|
|
34
|
+
/** OpenAI presence/frequency penalties + mlx-lm's context-size extensions
|
|
35
|
+
* (window of recent tokens the penalty looks at; default 20). */
|
|
36
|
+
presence_penalty?: number;
|
|
37
|
+
presence_context_size?: number;
|
|
38
|
+
frequency_penalty?: number;
|
|
39
|
+
frequency_context_size?: number;
|
|
40
|
+
/** mlx_lm.server logprobs: `logprobs` is a BOOL (even on /v1/completions —
|
|
41
|
+
* not OpenAI's legacy int), `top_logprobs` an int in [0, 11] or the -1
|
|
42
|
+
* "unset" sentinel (server.py validates exactly that; OpenAI's cap is 20,
|
|
43
|
+
* mlx-lm's is 11 — we copy the reference). Non-stream responses carry
|
|
44
|
+
* mlx-lm's logprobs block; stream chunks never do (reference behavior). */
|
|
45
|
+
logprobs?: boolean;
|
|
46
|
+
top_logprobs?: number;
|
|
47
|
+
/** OpenAI stop sequences: plain string or array (spec allows up to 4).
|
|
48
|
+
* Matched on DECODED text, not token ids — see StopMatcher. */
|
|
49
|
+
stop?: string | string[];
|
|
50
|
+
tools?: ToolDefinition[];
|
|
51
|
+
tool_choice?: "auto" | "none" | { type: string; function?: { name: string } };
|
|
52
|
+
/** Forwarded to HF chat templates, matching optiq serve. MiniCPM5 uses
|
|
53
|
+
* enable_thinking to select direct answers vs the <think> channel; Qwen3.8
|
|
54
|
+
* adds preserve_thinking (keep think blocks from history; template default
|
|
55
|
+
* true — better prompt-cache reuse in agent loops). */
|
|
56
|
+
chat_template_kwargs?: {
|
|
57
|
+
enable_thinking?: boolean;
|
|
58
|
+
preserve_thinking?: boolean;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
};
|
|
61
|
+
/** OpenAI reasoning control. For models with a switchable <think> channel
|
|
62
|
+
* (Qwen3.5/MiniCPM5) it gates enable_thinking: "none" → off, any level → on.
|
|
63
|
+
* This is what Pi sends when the provider advertises reasoning. For
|
|
64
|
+
* templates that read a reasoning-depth variable (Qwen3.8: xhigh/medium/
|
|
65
|
+
* low), the level ALSO maps into the template via qwenReasoningEffort —
|
|
66
|
+
* "xhigh" accepted as a first-class value (Qwen3.8's default/deepest). */
|
|
67
|
+
reasoning_effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
68
|
+
/** Mounted LoRA adapter selection: "id", "a+b" (stacked), or "none". */
|
|
69
|
+
adapter?: string;
|
|
70
|
+
/** HLG tone-curve sampling override (per request). Snake_case wire fields,
|
|
71
|
+
* merged over the server's --hlg-sampling config. docs/archive/hlg-sampling.md. */
|
|
72
|
+
hlg?: {
|
|
73
|
+
enabled?: boolean;
|
|
74
|
+
width?: number;
|
|
75
|
+
shoulder?: number;
|
|
76
|
+
toe?: number;
|
|
77
|
+
pivot_offset?: number;
|
|
78
|
+
};
|
|
79
|
+
/** OpenAI structured output: {type:"json_object"} | {type:"json_schema",
|
|
80
|
+
* json_schema:{name,schema,strict?}} | {type:"text"}. Enforced at the
|
|
81
|
+
* sampler via xgrammar token-bitmasks (src/grammar.ts). L2-class (oMLX
|
|
82
|
+
* oracle). On compile failure, degrades to a system-prompt injection +
|
|
83
|
+
* Warning header (oMLX parity), never 500. */
|
|
84
|
+
response_format?: unknown;
|
|
85
|
+
/** vLLM/oMLX grammar aliases (all compiled via xgrammar): raw EBNF/LARK
|
|
86
|
+
* grammar, regex, enum choice, and a bare JSON-schema object. Precedence
|
|
87
|
+
* (guided_grammar > json_schema > json_object > structured_outputs >
|
|
88
|
+
* guided_regex > guided_choice) mirrors oMLX _effective_guided_grammar. */
|
|
89
|
+
guided_grammar?: string;
|
|
90
|
+
guided_regex?: string;
|
|
91
|
+
guided_choice?: string[];
|
|
92
|
+
structured_outputs?: unknown;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** POST /v1/completions body (mlx_lm.server's raw text completion — no chat
|
|
96
|
+
* template). Sampling/penalty/stop fields are the same names as ChatRequestParams;
|
|
97
|
+
* `prompt` replaces `messages`. mlx_lm.server accepts only a STRING prompt
|
|
98
|
+
* (it calls `tokenizer.encode(request.prompt)` directly) — token-array
|
|
99
|
+
* prompts are rejected there too, so we match. No `echo` (mlx-lm has none). */
|
|
100
|
+
export type TextCompletionParams = Omit<ChatRequestParams, "messages" | "tools" | "tool_choice"> & {
|
|
101
|
+
prompt?: unknown;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/** Per-field default HLG knobs when enabling without specifying them. */
|
|
106
|
+
export const HLG_DEFAULTS = { width: 4, shoulder: 4, toe: 6, pivotOffset: 6 } as const;
|
|
107
|
+
|
|
108
|
+
/** Resolve the effective HLG config: a per-request `hlg` object overrides the
|
|
109
|
+
* server's --hlg-sampling default field-by-field. Returns undefined (HLG off)
|
|
110
|
+
* unless enabled by the request or the server. */
|
|
111
|
+
export function resolveHlg(
|
|
112
|
+
reqHlg: ChatRequestParams["hlg"],
|
|
113
|
+
serverHlg: HlgConfig | undefined,
|
|
114
|
+
): HlgConfig | undefined {
|
|
115
|
+
const enabled = reqHlg?.enabled ?? serverHlg?.enabled ?? false;
|
|
116
|
+
if (!enabled) return undefined;
|
|
117
|
+
const base = serverHlg ?? HLG_DEFAULTS;
|
|
118
|
+
return {
|
|
119
|
+
enabled: true,
|
|
120
|
+
width: reqHlg?.width ?? base.width,
|
|
121
|
+
shoulder: reqHlg?.shoulder ?? base.shoulder,
|
|
122
|
+
toe: reqHlg?.toe ?? base.toe,
|
|
123
|
+
pivotOffset: reqHlg?.pivot_offset ?? base.pivotOffset,
|
|
124
|
+
pivot: "top",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** True when a rendered prompt ends INSIDE an unclosed `<think>` block — the
|
|
129
|
+
* generation prompt primed reasoning (Qwen3.5 / MiniCPM5 with thinking on),
|
|
130
|
+
* so the model continues the chain-of-thought and emits only the closing
|
|
131
|
+
* `</think>`. Seeds ThinkingTagSplitter so reasoning is split out correctly.
|
|
132
|
+
* Thinking-off primes a CLOSED empty block (`<think>\n\n</think>`), and
|
|
133
|
+
* no-thinking templates have no `<think>` at all — both return false. */
|
|
134
|
+
export function promptEndsInOpenThink(rendered: string): boolean {
|
|
135
|
+
const open = rendered.lastIndexOf("<think>");
|
|
136
|
+
return open !== -1 && open > rendered.lastIndexOf("</think>");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Concatenate the text of an OpenAI content-part array, ignoring non-text
|
|
140
|
+
* parts. Tolerant of the part shapes clients actually send: `{type:"text",
|
|
141
|
+
* text}` (OpenAI/pi), `{type:"input_text", text}` (Responses-style), or any
|
|
142
|
+
* part carrying a string `text`. */
|
|
143
|
+
export function contentPartsToText(parts: Array<Record<string, unknown>>): string {
|
|
144
|
+
return parts
|
|
145
|
+
.map((p) => (p && typeof p.text === "string" ? p.text : ""))
|
|
146
|
+
.join("");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** True if any content part is media — image OR audio — so the multimodal
|
|
150
|
+
* path must keep the array form for extractImages/extractAudio. */
|
|
151
|
+
export function hasMediaPart(parts: Array<Record<string, unknown>>): boolean {
|
|
152
|
+
return parts.some(
|
|
153
|
+
(p) =>
|
|
154
|
+
p &&
|
|
155
|
+
(p.type === "image" || p.type === "image_url" ||
|
|
156
|
+
p.type === "audio" || p.type === "input_audio" || p.type === "audio_url" ||
|
|
157
|
+
p.type === "video" || p.type === "video_url"),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** OpenAI sends assistant tool_call arguments as JSON strings; the
|
|
162
|
+
* template renders the object form natively — normalize before render.
|
|
163
|
+
*
|
|
164
|
+
* Two more wire-format → template-format fixes, both "match the format the
|
|
165
|
+
* model's chat template expects":
|
|
166
|
+
*
|
|
167
|
+
* 1. Map the OpenAI reasoning-model "developer" role to "system": pi-ai (and
|
|
168
|
+
* OpenAI's own SDKs) rename the system prompt to `developer` whenever a
|
|
169
|
+
* model advertises reasoning, but our chat templates only know
|
|
170
|
+
* system/user/assistant/tool and raise "Unexpected message role." This is
|
|
171
|
+
* why Qwen3.5/MiniCPM5 chat got no messages while Gemma (non-reasoning, so
|
|
172
|
+
* pi keeps `system`) worked. `developer` IS the system prompt, so the remap
|
|
173
|
+
* is semantics-preserving.
|
|
174
|
+
*
|
|
175
|
+
* 2. Flatten text-only content-part arrays to a plain string. pi (and any
|
|
176
|
+
* OpenAI multimodal client) sends user content as `[{type:"text",text}]`,
|
|
177
|
+
* but non-vision chat templates expect `content` to be a STRING and render
|
|
178
|
+
* nothing for an array — so the user's turn silently vanishes and the model
|
|
179
|
+
* replies "I don't see any message." Arrays that carry a media part
|
|
180
|
+
* (image or audio) are left intact for the multimodal path
|
|
181
|
+
* (extractImages/extractAudio); only text-only arrays are collapsed
|
|
182
|
+
* here, which is a no-op for the vision templates too. */
|
|
183
|
+
export function normalizeMessages(messages: ChatMessage[]): ChatMessage[] {
|
|
184
|
+
return messages.map((raw) => {
|
|
185
|
+
let m = raw.role === "developer" ? { ...raw, role: "system" } : raw;
|
|
186
|
+
if (Array.isArray(m.content) && !hasMediaPart(m.content)) {
|
|
187
|
+
m = { ...m, content: contentPartsToText(m.content) };
|
|
188
|
+
}
|
|
189
|
+
// Pi and mlx-lm return `reasoning`; Qwen templates read
|
|
190
|
+
// `reasoning_content`. Keep the original field and immutable history,
|
|
191
|
+
// and let an explicitly supplied canonical string (including "") win.
|
|
192
|
+
if (typeof m.reasoning_content !== "string" && typeof m.reasoning === "string")
|
|
193
|
+
m = { ...m, reasoning_content: m.reasoning };
|
|
194
|
+
if (!m.tool_calls) return m;
|
|
195
|
+
return {
|
|
196
|
+
...m,
|
|
197
|
+
tool_calls: m.tool_calls.map((tc) => ({
|
|
198
|
+
...tc,
|
|
199
|
+
function: {
|
|
200
|
+
...tc.function,
|
|
201
|
+
arguments:
|
|
202
|
+
typeof tc.function.arguments === "string"
|
|
203
|
+
? (JSON.parse(tc.function.arguments) as Record<string, unknown>)
|
|
204
|
+
: tc.function.arguments,
|
|
205
|
+
},
|
|
206
|
+
})),
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Coerce a wire `logit_bias` ({"tokenId": bias}) to numeric keys/values —
|
|
212
|
+
* mlx_lm.server's `{int(k): float(v)}` coercion; throws its exact error
|
|
213
|
+
* message on anything non-numeric (surfaced as a 400). JSON object keys
|
|
214
|
+
* always arrive as strings, hence the coercion. */
|
|
215
|
+
export function parseLogitBias(
|
|
216
|
+
raw: Record<string, number> | undefined | null,
|
|
217
|
+
): Record<number, number> | undefined {
|
|
218
|
+
if (raw == null) return undefined;
|
|
219
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
220
|
+
throw new Error("logit_bias must be a dict of int to float");
|
|
221
|
+
const out: Record<number, number> = {};
|
|
222
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
223
|
+
const id = Number(k);
|
|
224
|
+
const bias = Number(v);
|
|
225
|
+
if (!Number.isInteger(id) || !Number.isFinite(bias))
|
|
226
|
+
throw new Error("logit_bias must be a dict of int to float");
|
|
227
|
+
out[id] = bias;
|
|
228
|
+
}
|
|
229
|
+
return Object.keys(out).length ? out : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Default seed for a request that didn't pin one. `Date.now()` alone is NOT
|
|
233
|
+
* request-unique: under `--batch N` the batch lane serves ONLY default-seed
|
|
234
|
+
* requests (explicit-seed requests use the serial mechanism, see
|
|
235
|
+
* GenerationGateway.place),
|
|
236
|
+
* so two identical prompts arriving in the same millisecond would share a seed
|
|
237
|
+
* and — with per-row RNG keyed as stepKey(seed, generatedCount) — produce
|
|
238
|
+
* byte-identical completions, silently collapsing best-of-N diversity. Mix a
|
|
239
|
+
* per-process Weyl counter (golden-ratio increment, period 2^32) into the
|
|
240
|
+
* timestamp so every call yields a distinct uint32 within any given ms.
|
|
241
|
+
* Determinism contract unchanged: reproducibility is only promised for an
|
|
242
|
+
* EXPLICIT request seed (`req.seed ?? nextDefaultSeed()` — explicit wins,
|
|
243
|
+
* byte-identical to before); a default seed is fresh entropy per request. */
|
|
244
|
+
let seedWeyl = 0;
|
|
245
|
+
export function nextDefaultSeed(): number {
|
|
246
|
+
seedWeyl = (seedWeyl + 0x9e3779b9) >>> 0;
|
|
247
|
+
return ((Date.now() & 0xffffffff) ^ seedWeyl) >>> 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** mlx_lm.server's logprobs request validation, copied exactly (server.py
|
|
251
|
+
* APIHandler.validate_model_parameters: `_validate("logprobs", bool)` and
|
|
252
|
+
* `_validate("top_logprobs", int, min_val=0, max_val=11, whitelist=[-1])`
|
|
253
|
+
* with defaults logprobs=False / top_logprobs=-1). Returns the reference's
|
|
254
|
+
* error message (→ 400) or null when valid. Note mlx-lm caps top_logprobs
|
|
255
|
+
* at 11, not OpenAI's 20 — we mirror the reference. */
|
|
256
|
+
/** Build a degrade-path system-prompt instruction for JSON output, mirroring
|
|
257
|
+
* oMLX's api.tool_calling.build_json_system_prompt (used when xgrammar
|
|
258
|
+
* compile fails — the response_format degrades to prompt injection rather
|
|
259
|
+
* than a 500, oMLX parity). Returns null for {type:"text"} / unset. */
|
|
260
|
+
export function degradeJsonSystemPrompt(body: ChatRequestParams): string | null {
|
|
261
|
+
if (body.guided_grammar) {
|
|
262
|
+
return "You must respond with text matching this grammar:\n\n" + body.guided_grammar;
|
|
263
|
+
}
|
|
264
|
+
const rf = body.response_format as { type?: string; json_schema?: { name?: string; description?: string; schema?: unknown } } | undefined;
|
|
265
|
+
if (rf && typeof rf === "object" && rf.type === "json_object") {
|
|
266
|
+
return "You must respond with valid JSON only. " +
|
|
267
|
+
"Do not include any explanation or text outside the JSON object.";
|
|
268
|
+
}
|
|
269
|
+
const schemaSpec = rf && typeof rf === "object" && rf.type === "json_schema"
|
|
270
|
+
? rf.json_schema
|
|
271
|
+
: body.structured_outputs && typeof body.structured_outputs === "object"
|
|
272
|
+
? { schema: body.structured_outputs }
|
|
273
|
+
: null;
|
|
274
|
+
if (schemaSpec) {
|
|
275
|
+
const spec = schemaSpec;
|
|
276
|
+
const schema = spec.schema ?? body.structured_outputs ?? {};
|
|
277
|
+
const name = spec.name ?? "response";
|
|
278
|
+
const description = spec.description ?? "";
|
|
279
|
+
let prompt = `You must respond with valid JSON matching the '${name}' schema.`;
|
|
280
|
+
if (description) prompt += ` ${description}`;
|
|
281
|
+
prompt += `\n\nJSON Schema:\n\`\`\`json\n${JSON.stringify(schema, null, 2)}\n\`\`\`\n\n` +
|
|
282
|
+
"Respond with only the JSON object, no additional text or explanation.";
|
|
283
|
+
return prompt;
|
|
284
|
+
}
|
|
285
|
+
if (body.guided_regex) {
|
|
286
|
+
return "You must respond with text matching this regular expression:\n\n" +
|
|
287
|
+
body.guided_regex;
|
|
288
|
+
}
|
|
289
|
+
if (body.guided_choice?.length) {
|
|
290
|
+
return "You must respond with exactly one of these choices:\n\n" +
|
|
291
|
+
body.guided_choice.map((choice) => JSON.stringify(choice)).join("\n");
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function applyGrammarDegrade(
|
|
297
|
+
body: ChatRequestParams,
|
|
298
|
+
degradeHint: string,
|
|
299
|
+
): { body: ChatRequestParams; warning: string } {
|
|
300
|
+
const content = degradeJsonSystemPrompt(body) ??
|
|
301
|
+
"Follow the requested output constraint exactly.";
|
|
302
|
+
return {
|
|
303
|
+
warning:
|
|
304
|
+
`grammar not enforced: ${degradeHint} - falling back to prompt injection`,
|
|
305
|
+
body: {
|
|
306
|
+
...body,
|
|
307
|
+
messages: [{ role: "system", content }, ...body.messages],
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function validateLogprobsParams(body: {
|
|
313
|
+
logprobs?: unknown;
|
|
314
|
+
top_logprobs?: unknown;
|
|
315
|
+
}): string | null {
|
|
316
|
+
const lp = body.logprobs ?? false;
|
|
317
|
+
if (typeof lp !== "boolean") return "logprobs must be of type bool";
|
|
318
|
+
const tl = body.top_logprobs ?? -1;
|
|
319
|
+
if (typeof tl !== "number" || !Number.isInteger(tl))
|
|
320
|
+
return "top_logprobs must be of type int";
|
|
321
|
+
if (tl === -1) return null; // the "unset" whitelist sentinel
|
|
322
|
+
if (tl < 0) return "top_logprobs must be at least 0";
|
|
323
|
+
if (tl > 11) return "top_logprobs must be at most 11";
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const REASONING_EFFORT_LEVELS =
|
|
328
|
+
["none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
329
|
+
|
|
330
|
+
/** Validate `reasoning_effort` against the accepted level names. An invalid
|
|
331
|
+
* string must NOT pass through: resolveEnableThinking treats any defined
|
|
332
|
+
* value ≠ "none" as thinking-ON, so a typo like "hihg" silently flipped the
|
|
333
|
+
* thinking channel (and its hotter temperature) on thinking-off models and
|
|
334
|
+
* burned a full default-depth reasoning budget (2026-08-18 review). Same
|
|
335
|
+
* contract style as validateLogprobsParams above. */
|
|
336
|
+
export function validateReasoningEffort(body: {
|
|
337
|
+
reasoning_effort?: unknown;
|
|
338
|
+
}): string | null {
|
|
339
|
+
const v = body.reasoning_effort;
|
|
340
|
+
if (v === undefined || v === null) return null;
|
|
341
|
+
if (typeof v !== "string" || !(REASONING_EFFORT_LEVELS as readonly string[]).includes(v))
|
|
342
|
+
return "reasoning_effort must be one of " +
|
|
343
|
+
"'none', 'minimal', 'low', 'medium', 'high', 'xhigh'";
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// The request objects a caller constructs and hands to the pipeline. The
|
|
349
|
+
// constructor is the validation boundary: structural checks that need no
|
|
350
|
+
// model (mlx_lm.server parity messages) throw RequestError(400) here, so a
|
|
351
|
+
// stage never sees a malformed request.
|
|
352
|
+
|
|
353
|
+
export class ChatRequest {
|
|
354
|
+
readonly params: ChatRequestParams;
|
|
355
|
+
|
|
356
|
+
constructor(params: ChatRequestParams) {
|
|
357
|
+
if (!Array.isArray(params.messages) || params.messages.length === 0)
|
|
358
|
+
throw new RequestError(400, "messages required");
|
|
359
|
+
const lpParamError = validateLogprobsParams(params);
|
|
360
|
+
if (lpParamError) throw new RequestError(400, lpParamError);
|
|
361
|
+
const effortError = validateReasoningEffort(params);
|
|
362
|
+
if (effortError) throw new RequestError(400, effortError);
|
|
363
|
+
this.params = params;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
get stream(): boolean {
|
|
367
|
+
return this.params.stream === true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** POST /v1/completions — raw text, no chat template (mlx_lm.server's
|
|
372
|
+
* request_type "text"). Only a non-empty STRING prompt is accepted. */
|
|
373
|
+
export class TextCompletionRequest {
|
|
374
|
+
readonly params: TextCompletionParams & { prompt: string };
|
|
375
|
+
|
|
376
|
+
constructor(params: TextCompletionParams) {
|
|
377
|
+
if (typeof params.prompt !== "string" || params.prompt.length === 0)
|
|
378
|
+
throw new RequestError(
|
|
379
|
+
400,
|
|
380
|
+
"prompt (a non-empty string) is required " +
|
|
381
|
+
"(token-array prompts are not accepted, matching mlx_lm.server)",
|
|
382
|
+
);
|
|
383
|
+
const lpParamError = validateLogprobsParams(params);
|
|
384
|
+
if (lpParamError) throw new RequestError(400, lpParamError);
|
|
385
|
+
this.params = params as TextCompletionParams & { prompt: string };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
get stream(): boolean {
|
|
389
|
+
return this.params.stream === true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// ChatStage: ChatRequest in, InferenceRequest out. Turns a chat conversation
|
|
2
|
+
// (messages, tools, media parts, sampling fields) into the prompt token ids,
|
|
3
|
+
// media embeddings, resolved generation options, adapter selection, grammar
|
|
4
|
+
// controller, and the token→text pipeline the model needs. Every chat-shaped
|
|
5
|
+
// surface (OpenAI chat, Anthropic Messages, Responses) runs this stage; the
|
|
6
|
+
// wire format is not its business.
|
|
7
|
+
import type { PromptCache } from "../prompt-cache";
|
|
8
|
+
import {
|
|
9
|
+
applyGrammarDegrade,
|
|
10
|
+
type ChatRequest,
|
|
11
|
+
type ChatRequestParams,
|
|
12
|
+
} from "./chat-request";
|
|
13
|
+
import type { InferenceRequest } from "./inference-request";
|
|
14
|
+
import type { ServingContext } from "./model-host";
|
|
15
|
+
import type { BuiltPrompt, ModelPromptBuilder } from "./model-binding";
|
|
16
|
+
import { modelPromptBuilder } from "../backends/mlx/model-serving";
|
|
17
|
+
import { RequestError } from "./pipeline";
|
|
18
|
+
import { RequestOwnership } from "./request-plan";
|
|
19
|
+
import type { RequestPrep } from "./request-prep";
|
|
20
|
+
import { StopMatcher, ThinkingTagSplitter } from "./token-streams";
|
|
21
|
+
|
|
22
|
+
export class ChatStage {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly ctx: ServingContext,
|
|
25
|
+
private readonly prep: RequestPrep,
|
|
26
|
+
private readonly promptCache: Pick<PromptCache, "peekPrefixLen">,
|
|
27
|
+
/** Admission ceiling (fit() / the GLM memory plan), resolved once. */
|
|
28
|
+
private readonly contextLimit: number | null,
|
|
29
|
+
/** `serve --adapter <dir>` startup default; a request's explicit
|
|
30
|
+
* `adapter` (incl. "none") wins. */
|
|
31
|
+
private readonly defaultAdapter?: string,
|
|
32
|
+
private readonly preparation?: import("./preparation").PreparationExecutor,
|
|
33
|
+
private readonly buildPrompt: ModelPromptBuilder = modelPromptBuilder(ctx),
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
async run(request: ChatRequest, requestId?: string, signal?: AbortSignal): Promise<InferenceRequest> {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
const body = request.params;
|
|
39
|
+
const media = body.messages.some((message) => Array.isArray(message.content) &&
|
|
40
|
+
message.content.some((part) => part.type !== "text"));
|
|
41
|
+
const native = media || body.response_format != null || !!body.guided_grammar ||
|
|
42
|
+
!!body.guided_regex || !!body.guided_choice?.length || body.structured_outputs != null;
|
|
43
|
+
const build = async () => {
|
|
44
|
+
signal?.throwIfAborted();
|
|
45
|
+
const result = await this.runPrepared(request, requestId);
|
|
46
|
+
if (signal?.aborted) {
|
|
47
|
+
result.plan.ownership.dispose();
|
|
48
|
+
signal.throwIfAborted();
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
if (!native || !this.preparation) return build();
|
|
53
|
+
let reservation = await this.preparation.reserve?.(media ? "media" : "constraint", signal);
|
|
54
|
+
try {
|
|
55
|
+
const result = await this.preparation.run(build, signal);
|
|
56
|
+
if (reservation) result.plan.ownership.retain(reservation);
|
|
57
|
+
reservation = undefined;
|
|
58
|
+
return result;
|
|
59
|
+
} finally { reservation?.dispose(); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async runPrepared(request: ChatRequest, requestId?: string): Promise<InferenceRequest> {
|
|
63
|
+
const { ctx, prep } = this;
|
|
64
|
+
let body = request.params;
|
|
65
|
+
const id = requestId ?? `chatcmpl-${crypto.randomUUID()}`;
|
|
66
|
+
const tools =
|
|
67
|
+
body.tool_choice === "none" ? null : (body.tools?.length ? body.tools : null);
|
|
68
|
+
const ownership = new RequestOwnership();
|
|
69
|
+
const warnings: string[] = [];
|
|
70
|
+
const reject = (status: number, message: string): never => {
|
|
71
|
+
ownership.dispose();
|
|
72
|
+
throw new RequestError(status, message);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Grammar-constrained decoding (src/grammar.ts). Compile BEFORE prompt
|
|
76
|
+
// rendering: on the degrade path (compile failed but a constraint was
|
|
77
|
+
// requested) inject a system message instructing valid JSON so the
|
|
78
|
+
// model still best-efforts schema-conformant output (oMLX parity —
|
|
79
|
+
// _compile_grammar_for_request returning None + build_json_system_prompt).
|
|
80
|
+
let grammarCtrl: import("../grammar").GrammarController | null = null;
|
|
81
|
+
const grammarReq =
|
|
82
|
+
body.response_format != null || !!body.guided_grammar ||
|
|
83
|
+
!!body.guided_regex || !!body.guided_choice?.length ||
|
|
84
|
+
body.structured_outputs != null;
|
|
85
|
+
if (grammarReq) {
|
|
86
|
+
const g = await prep.compileGrammarForRequest(body);
|
|
87
|
+
grammarCtrl = ownership.own(g.controller);
|
|
88
|
+
if (!g.controller && g.degradeHint) {
|
|
89
|
+
const degraded = applyGrammarDegrade(body, g.degradeHint);
|
|
90
|
+
warnings.push(degraded.warning);
|
|
91
|
+
body = degraded.body;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let built: BuiltPrompt;
|
|
96
|
+
try {
|
|
97
|
+
built = await this.buildPrompt(body, tools, ownership, prep);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
if (e instanceof RequestError) return reject(e.status, e.message);
|
|
100
|
+
return reject(400, `prompt build failed: ${(e as Error).message}`);
|
|
101
|
+
}
|
|
102
|
+
const { promptIds, vision, startInThinking, diffusionPixels } = built;
|
|
103
|
+
ownership.own(vision?.embeddings);
|
|
104
|
+
ownership.own(vision?.imageMask);
|
|
105
|
+
ownership.own(vision?.multimodalMask);
|
|
106
|
+
ownership.own(diffusionPixels);
|
|
107
|
+
|
|
108
|
+
let options: ReturnType<RequestPrep["toOptions"]>;
|
|
109
|
+
try {
|
|
110
|
+
options = prep.toOptions(body);
|
|
111
|
+
// Stable cache boundary for the prompt-boundary snapshot (chat
|
|
112
|
+
// prompts end in a generation primer the next turn won't contain).
|
|
113
|
+
// The probe costs a second full render+encode (~150 ms at 16k), so
|
|
114
|
+
// run it ONLY when a snapshot can actually be taken: the cache
|
|
115
|
+
// must not already hold the strict prefix (warm repeats peek at
|
|
116
|
+
// len-1 → skip; the snapshot gate needs boundary > cached prefix).
|
|
117
|
+
if (built.probeStableLen) {
|
|
118
|
+
const cachePeek = this.promptCache.peekPrefixLen(
|
|
119
|
+
promptIds, options.adapters?.length ? ctx.adapters.cacheNamespace(options.adapters) : "");
|
|
120
|
+
if (cachePeek < promptIds.length - 1)
|
|
121
|
+
options.snapshotAt = prep.stableLenFor(body, tools, promptIds);
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// bad logit_bias (non-numeric keys/values) — mlx-lm's coercion error
|
|
125
|
+
return reject(400, (e as Error).message);
|
|
126
|
+
}
|
|
127
|
+
if (diffusionPixels) options.visionPixels = diffusionPixels;
|
|
128
|
+
// Attach the compiled grammar controller (null when no constraint /
|
|
129
|
+
// degrade — generate() runs the unmasked fast pipelined loop).
|
|
130
|
+
if (grammarCtrl) options.grammar = grammarCtrl;
|
|
131
|
+
// Token fast-forwarding (K3, MLX_BUN_FILL=strict): the determined-span
|
|
132
|
+
// table for this request's tools. Refused for the shapes only this stage
|
|
133
|
+
// can see — a compiled grammar (forced tokens are its job), media prompts
|
|
134
|
+
// (embeddings prefill / mRoPE), a mounted draft model (the spec loop is a
|
|
135
|
+
// different executor and never reads options.fill), and quantized-KV
|
|
136
|
+
// schemes (post-conversion multi-token append is unvalidated). The
|
|
137
|
+
// body-level refusals live in prep.fillPlanFor.
|
|
138
|
+
let fillSession: import("../fill/fill-session").FillSession | null = null;
|
|
139
|
+
if (!grammarCtrl && !vision && !diffusionPixels && !ctx.draft &&
|
|
140
|
+
!options.kvBits && !options.kvConfig?.length && !options.turboQuant) {
|
|
141
|
+
fillSession = prep.fillPlanFor(body, tools, promptIds);
|
|
142
|
+
if (fillSession) options.fill = fillSession;
|
|
143
|
+
}
|
|
144
|
+
// Unset = no request-level ceiling; admission clamps to the admitted
|
|
145
|
+
// context (the program is the cap).
|
|
146
|
+
const requestedMaxTokens = options.maxTokens ?? Infinity;
|
|
147
|
+
let adapterIds: string[];
|
|
148
|
+
try {
|
|
149
|
+
// A request's explicit `adapter` (incl. "none") wins over the
|
|
150
|
+
// startup default from `serve --adapter <dir>`.
|
|
151
|
+
adapterIds = ctx.adapters.resolveSpec(body.adapter ?? this.defaultAdapter);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return reject(400, (e as Error).message);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const wantLogprobs = body.logprobs === true;
|
|
157
|
+
const topLogprobs =
|
|
158
|
+
typeof body.top_logprobs === "number" && body.top_logprobs > 0
|
|
159
|
+
? body.top_logprobs : 0;
|
|
160
|
+
return {
|
|
161
|
+
requestId: id,
|
|
162
|
+
stream: request.stream,
|
|
163
|
+
warnings,
|
|
164
|
+
plan: {
|
|
165
|
+
promptIds,
|
|
166
|
+
options,
|
|
167
|
+
requestedMaxTokens,
|
|
168
|
+
contextLimit: this.contextLimit,
|
|
169
|
+
stream: request.stream,
|
|
170
|
+
wantLogprobs,
|
|
171
|
+
topLogprobs,
|
|
172
|
+
adapterIds,
|
|
173
|
+
hasVision: !!vision,
|
|
174
|
+
userSeed: body.seed !== undefined,
|
|
175
|
+
hasGrammar: !!grammarCtrl,
|
|
176
|
+
hasDraft: !!ctx.draft,
|
|
177
|
+
ownership,
|
|
178
|
+
},
|
|
179
|
+
...(vision ? { vision } : {}),
|
|
180
|
+
pipeline: {
|
|
181
|
+
// The router's parse-failure hook disarms this request's strict fill
|
|
182
|
+
// rows (and records `usage.fill.parseFallback`).
|
|
183
|
+
router: prep.toolRouter(tools, fillSession ? () => fillSession!.noteParseFailure() : undefined),
|
|
184
|
+
stopper: new StopMatcher(options.stopSequences),
|
|
185
|
+
thinking: new ThinkingTagSplitter(
|
|
186
|
+
ctx.template.thinkingFormat === "think-tag",
|
|
187
|
+
startInThinking,
|
|
188
|
+
),
|
|
189
|
+
collectToolCalls: true,
|
|
190
|
+
},
|
|
191
|
+
idToToken: (tokenId) => ctx.tokenizer.idToToken(tokenId),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { GenerateOptions } from "../generate";
|
|
2
|
+
import type { ResolvedExecution } from "../contracts/execution";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
/** Sort object fields only; array order is meaningful for adapter composition,
|
|
6
|
+
* stop handling and quantization recipes. */
|
|
7
|
+
function canonical(value: unknown): unknown {
|
|
8
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
9
|
+
if (value && typeof value === "object")
|
|
10
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))
|
|
11
|
+
.map(([key, item]) => [key, canonical(item)]));
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Stable identity for an exactly resumable generation. The prompt already
|
|
16
|
+
* includes rendered messages and tool schemas; the remaining fields cover
|
|
17
|
+
* every sampling policy that can change the continuation. */
|
|
18
|
+
export function generationCheckpointKey(
|
|
19
|
+
promptIds: number[], options: GenerateOptions & { stopSequences?: readonly string[] }, cacheNs = "",
|
|
20
|
+
execution?: ResolvedExecution, bindingIdentity?: unknown,
|
|
21
|
+
): string {
|
|
22
|
+
const policy = {
|
|
23
|
+
version: 4,
|
|
24
|
+
execution: execution && { method: execution.method, mechanism: execution.mechanism,
|
|
25
|
+
pagedKv: execution.pagedKv, fill: execution.fill,
|
|
26
|
+
compiledDecode: execution.compiledDecode, grammarJump: execution.grammarJump },
|
|
27
|
+
bindingIdentity,
|
|
28
|
+
stopSequences: options.stopSequences ?? [],
|
|
29
|
+
cacheNs,
|
|
30
|
+
promptIds,
|
|
31
|
+
maxTokens: options.maxTokens,
|
|
32
|
+
eosTokenIds: options.eosTokenIds,
|
|
33
|
+
temperature: options.temperature,
|
|
34
|
+
topP: options.topP,
|
|
35
|
+
topK: options.topK,
|
|
36
|
+
minP: options.minP,
|
|
37
|
+
minTokensToKeep: options.minTokensToKeep,
|
|
38
|
+
xtcProbability: options.xtcProbability,
|
|
39
|
+
xtcThreshold: options.xtcThreshold,
|
|
40
|
+
xtcSpecialTokens: options.xtcSpecialTokens,
|
|
41
|
+
// A missing client seed means "fresh randomness" for a new request, but
|
|
42
|
+
// an identical RETRY must find the durable checkpoint and restore the
|
|
43
|
+
// original random stream. Keep the generated seed out of request identity;
|
|
44
|
+
// it travels in checkpoint metadata instead. Explicit seeds remain part of
|
|
45
|
+
// the policy, so changing one cannot select an incompatible checkpoint.
|
|
46
|
+
seedMode: options.seedWasExplicit ? "explicit" : "server-default",
|
|
47
|
+
seed: options.seedWasExplicit ? options.seed : undefined,
|
|
48
|
+
hlg: options.hlg,
|
|
49
|
+
curve: options.curve,
|
|
50
|
+
logitBias: options.logitBias,
|
|
51
|
+
repetitionPenalty: options.repetitionPenalty,
|
|
52
|
+
repetitionContextSize: options.repetitionContextSize,
|
|
53
|
+
presencePenalty: options.presencePenalty,
|
|
54
|
+
presenceContextSize: options.presenceContextSize,
|
|
55
|
+
frequencyPenalty: options.frequencyPenalty,
|
|
56
|
+
frequencyContextSize: options.frequencyContextSize,
|
|
57
|
+
kvBits: options.kvBits,
|
|
58
|
+
kvGroupSize: options.kvGroupSize,
|
|
59
|
+
quantizedKvStart: options.quantizedKvStart,
|
|
60
|
+
kvConfig: options.kvConfig,
|
|
61
|
+
turboQuant: options.turboQuant,
|
|
62
|
+
};
|
|
63
|
+
return createHash("sha256").update(JSON.stringify(canonical(policy))).digest("hex");
|
|
64
|
+
}
|