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,519 @@
|
|
|
1
|
+
// Token → text streaming stages shared by every completion surface:
|
|
2
|
+
// StreamDecoder (mlx-lm byte-parity incremental detokenizer), ToolAwareStream
|
|
3
|
+
// (tool-call / reasoning-channel routing), StopMatcher (decoded-text stop
|
|
4
|
+
// sequences), ThinkingTagSplitter (<think> markup → reasoning deltas).
|
|
5
|
+
// Extracted from src/server.ts (repo-taming Phase 4).
|
|
6
|
+
import type { ToolDefinition } from "../chat-template";
|
|
7
|
+
import type { LoadedTokenizer } from "../tokenizer";
|
|
8
|
+
import {
|
|
9
|
+
CHANNEL_END, CHANNEL_START, parseGeneratedToolCalls, parseToolCalls,
|
|
10
|
+
TOOL_CALL_END, TOOL_CALL_START,
|
|
11
|
+
} from "../tool-call";
|
|
12
|
+
|
|
13
|
+
export interface OpenAIToolCall {
|
|
14
|
+
id: string;
|
|
15
|
+
type: "function";
|
|
16
|
+
function: { name: string; arguments: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ToolStreamMode = "gemma-sentinel" | "plain" | "buffered-text";
|
|
20
|
+
|
|
21
|
+
/** Pick the stream router for a model family. The token-id sentinel router is
|
|
22
|
+
* Gemma-4-ONLY: ids 48/49 (<|tool_call>/<tool_call|>) and 100/101
|
|
23
|
+
* (<|channel>/<channel|>) are special tokens of that tokenizer family
|
|
24
|
+
* (src/tool-call.ts). On every other tokenizer — MiniCPM5, Qwen3/3.5, and
|
|
25
|
+
* the Tier-0 generics (llama, qwen2, phi3, …) — those ids are ordinary
|
|
26
|
+
* low-id vocab, so the sentinel router would silently swallow output into a
|
|
27
|
+
* phantom tool/reasoning segment. Everyone else parses tool calls from
|
|
28
|
+
* decoded text (buffered-text; parseGeneratedToolCalls covers the
|
|
29
|
+
* OpenAI-JSON <tool_call>, Qwen <function=…>, and MiniCPM5 <function name=…>
|
|
30
|
+
* shapes) when tools are present, and streams plain otherwise. Models whose
|
|
31
|
+
* markup isn't covered fail soft: the markup stays in content. */
|
|
32
|
+
export function selectToolStreamMode(modelType: string, hasTools: boolean): ToolStreamMode {
|
|
33
|
+
if (modelType.startsWith("gemma4")) return "gemma-sentinel";
|
|
34
|
+
return hasTools ? "buffered-text" : "plain";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Routes generated tokens. Gemma uses family-specific sentinel token ids;
|
|
38
|
+
* MiniCPM5 and other text-template models use decoded-text parsing so
|
|
39
|
+
* ordinary tokenizer ids like "<" are never swallowed globally. Exported for
|
|
40
|
+
* unit tests (gemma-channel reasoning split). */
|
|
41
|
+
export class ToolAwareStream {
|
|
42
|
+
readonly #decoder: StreamDecoder;
|
|
43
|
+
#inTool = false;
|
|
44
|
+
#toolTokens: number[] = [];
|
|
45
|
+
#text = "";
|
|
46
|
+
/** Chars of #text already returned as content. */
|
|
47
|
+
#sent = 0;
|
|
48
|
+
/** Index where tool markup starts; content emission stops there. */
|
|
49
|
+
#frozen = -1;
|
|
50
|
+
#textToolCalls: OpenAIToolCall[] | null = null;
|
|
51
|
+
#textToolParseFailed = false;
|
|
52
|
+
readonly toolSegments: number[][] = [];
|
|
53
|
+
|
|
54
|
+
/** Gemma reasoning-channel state (gemma-sentinel mode). The model wraps
|
|
55
|
+
* chain-of-thought as `<|channel>thought\n…<channel|>` using special tokens
|
|
56
|
+
* 100/101 that the content decoder strips, so reasoning is captured here at
|
|
57
|
+
* the token level. A SEPARATE decoder keeps the reasoning byte-stream's
|
|
58
|
+
* incremental state independent of content's. The `thought` channel-name
|
|
59
|
+
* word is stripped before the reasoning text (mlx-lm's think-start marker
|
|
60
|
+
* is `<|channel>thought`; the "\n" after it is reasoning content). */
|
|
61
|
+
readonly #channelDecoder: StreamDecoder;
|
|
62
|
+
#inChannel = false;
|
|
63
|
+
#channelNamePending = "";
|
|
64
|
+
#channelNameDone = false;
|
|
65
|
+
#reasoning = "";
|
|
66
|
+
|
|
67
|
+
/** Decoded-text markers that open tool markup (oracle: the streaming
|
|
68
|
+
* parser buffers from `<tool_call`/`<function` on, never the whole
|
|
69
|
+
* response — content before a tool call still streams live). */
|
|
70
|
+
static readonly TOOL_MARKERS = ["<tool_call>", "<function"];
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
readonly tokenizer: LoadedTokenizer,
|
|
74
|
+
readonly mode: ToolStreamMode,
|
|
75
|
+
readonly tools: ToolDefinition[] | null,
|
|
76
|
+
/** Fired when parseGeneratedToolCalls rejects the emitted markup. The
|
|
77
|
+
* fill table (src/fill/) subscribes: a request whose tool call the
|
|
78
|
+
* parser refuses is a request whose template rendering disagreed with
|
|
79
|
+
* what the model emits, so its strict rows are disarmed. */
|
|
80
|
+
private readonly onParseFailure?: () => void,
|
|
81
|
+
) {
|
|
82
|
+
this.#decoder = new StreamDecoder(tokenizer, mode !== "buffered-text");
|
|
83
|
+
this.#channelDecoder = new StreamDecoder(tokenizer, true);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Feed decoded channel text, stripping the leading `thought` channel-name
|
|
87
|
+
* word (which may arrive across tokens). Returns the reasoning delta.
|
|
88
|
+
* mlx-lm parity: its think-start marker is exactly `<|channel>thought`
|
|
89
|
+
* (tokenizer_utils.py `_infer_thinking`) and only the MARKER tokens get
|
|
90
|
+
* their text blanked (server.py `_process_control_tokens`) — the "\n"
|
|
91
|
+
* after the name is an ordinary generated token, so it is the FIRST byte
|
|
92
|
+
* of the reasoning stream and must be kept, not swallowed with the name. */
|
|
93
|
+
#feedChannel(text: string): string {
|
|
94
|
+
if (this.#channelNameDone) return text;
|
|
95
|
+
this.#channelNamePending += text;
|
|
96
|
+
const nl = this.#channelNamePending.indexOf("\n");
|
|
97
|
+
if (nl === -1) return ""; // still inside the channel-name word
|
|
98
|
+
this.#channelNameDone = true;
|
|
99
|
+
const rest = this.#channelNamePending.slice(nl); // keep the "\n" (mlx-lm does)
|
|
100
|
+
this.#channelNamePending = "";
|
|
101
|
+
return rest;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Drain reasoning captured since the last call (gemma-channel thinking). */
|
|
105
|
+
takeReasoning(): string {
|
|
106
|
+
const r = this.#reasoning;
|
|
107
|
+
this.#reasoning = "";
|
|
108
|
+
return r;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Emit the longest #text prefix that cannot be (the start of) tool
|
|
112
|
+
* markup; hold back ambiguous tails until disambiguated. */
|
|
113
|
+
#textDelta(): string {
|
|
114
|
+
if (this.#frozen >= 0) return "";
|
|
115
|
+
let markerAt = -1;
|
|
116
|
+
for (const mk of ToolAwareStream.TOOL_MARKERS) {
|
|
117
|
+
const i = this.#text.indexOf(mk, this.#sent);
|
|
118
|
+
if (i !== -1 && (markerAt === -1 || i < markerAt)) markerAt = i;
|
|
119
|
+
}
|
|
120
|
+
if (markerAt !== -1) {
|
|
121
|
+
this.#frozen = markerAt;
|
|
122
|
+
const out = this.#text.slice(this.#sent, markerAt);
|
|
123
|
+
this.#sent = markerAt;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
let hold = 0;
|
|
127
|
+
for (const mk of ToolAwareStream.TOOL_MARKERS) {
|
|
128
|
+
const max = Math.min(mk.length - 1, this.#text.length - this.#sent);
|
|
129
|
+
for (let k = max; k > hold; k--) {
|
|
130
|
+
if (this.#text.endsWith(mk.slice(0, k))) { hold = k; break; }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const limit = this.#text.length - hold;
|
|
134
|
+
if (limit <= this.#sent) return "";
|
|
135
|
+
const out = this.#text.slice(this.#sent, limit);
|
|
136
|
+
this.#sent = limit;
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Returns the content text delta for this token ("" while capturing). */
|
|
141
|
+
push(token: number): string {
|
|
142
|
+
if (this.mode !== "gemma-sentinel") {
|
|
143
|
+
this.#text += this.#decoder.push(token);
|
|
144
|
+
if (this.mode === "plain") {
|
|
145
|
+
const out = this.#text.slice(this.#sent);
|
|
146
|
+
this.#sent = this.#text.length;
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
return this.#textDelta();
|
|
150
|
+
}
|
|
151
|
+
if (this.#inTool) {
|
|
152
|
+
if (token === TOOL_CALL_END) {
|
|
153
|
+
this.#inTool = false;
|
|
154
|
+
this.toolSegments.push(this.#toolTokens);
|
|
155
|
+
this.#toolTokens = [];
|
|
156
|
+
} else {
|
|
157
|
+
this.#toolTokens.push(token);
|
|
158
|
+
}
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
// Reasoning channel: tokens between <|channel> and <channel|> are thought,
|
|
162
|
+
// captured to #reasoning (drained via takeReasoning), never content. An
|
|
163
|
+
// empty block (<|channel>thought\n<channel|>, emitted by larger Gemmas even
|
|
164
|
+
// with thinking off) yields only the "\n" as reasoning (mlx-lm parity) and
|
|
165
|
+
// leaks nothing into content.
|
|
166
|
+
if (this.#inChannel) {
|
|
167
|
+
if (token === CHANNEL_END) {
|
|
168
|
+
this.#inChannel = false;
|
|
169
|
+
this.#reasoning += this.#feedChannel(this.#channelDecoder.flush());
|
|
170
|
+
} else {
|
|
171
|
+
this.#reasoning += this.#feedChannel(this.#channelDecoder.push(token));
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
if (token === CHANNEL_START) {
|
|
176
|
+
this.#inChannel = true;
|
|
177
|
+
this.#channelNameDone = false;
|
|
178
|
+
this.#channelNamePending = "";
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
if (token === TOOL_CALL_START) {
|
|
182
|
+
this.#inTool = true;
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
return this.#decoder.push(token);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
flush(): string {
|
|
189
|
+
if (this.mode !== "gemma-sentinel") {
|
|
190
|
+
this.#text += this.#decoder.flush();
|
|
191
|
+
if (this.mode === "buffered-text") {
|
|
192
|
+
const calls = this.toolCalls();
|
|
193
|
+
if (calls.length && !this.#textToolParseFailed && this.#frozen >= 0) {
|
|
194
|
+
// markup parsed into tool_calls — emit any prose still held
|
|
195
|
+
// before it; the markup itself never reaches content
|
|
196
|
+
const out = this.#text.slice(this.#sent, this.#frozen);
|
|
197
|
+
this.#sent = this.#text.length;
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
// no tool call (or parse fallback): release everything withheld
|
|
201
|
+
const out = this.#text.slice(this.#sent);
|
|
202
|
+
this.#sent = this.#text.length;
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
const out = this.#text.slice(this.#sent);
|
|
206
|
+
this.#sent = this.#text.length;
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
if (this.#inChannel) {
|
|
210
|
+
// truncated mid-reasoning (hit max_tokens); surface the partial thought
|
|
211
|
+
this.#reasoning += this.#feedChannel(this.#channelDecoder.flush());
|
|
212
|
+
this.#inChannel = false;
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
if (this.#inTool && this.#toolTokens.length) {
|
|
216
|
+
// truncated mid-tool-call (hit max_tokens); surface what we have
|
|
217
|
+
this.toolSegments.push(this.#toolTokens);
|
|
218
|
+
this.#toolTokens = [];
|
|
219
|
+
}
|
|
220
|
+
return this.#decoder.flush();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
toolCalls(): OpenAIToolCall[] {
|
|
224
|
+
if (this.mode !== "gemma-sentinel") {
|
|
225
|
+
if (this.#textToolCalls) return this.#textToolCalls;
|
|
226
|
+
try {
|
|
227
|
+
this.#textToolCalls = parseGeneratedToolCalls(this.#text, this.tools ?? []).map((c) => ({
|
|
228
|
+
id: `call_${crypto.randomUUID().slice(0, 8)}`,
|
|
229
|
+
type: "function",
|
|
230
|
+
function: { name: c.name, arguments: JSON.stringify(c.arguments) },
|
|
231
|
+
}));
|
|
232
|
+
} catch {
|
|
233
|
+
this.#textToolParseFailed = true;
|
|
234
|
+
this.#textToolCalls = [];
|
|
235
|
+
this.onParseFailure?.();
|
|
236
|
+
}
|
|
237
|
+
return this.#textToolCalls;
|
|
238
|
+
}
|
|
239
|
+
const out: OpenAIToolCall[] = [];
|
|
240
|
+
for (const seg of this.toolSegments) {
|
|
241
|
+
const text = this.tokenizer.decode(seg, false); // keep <|"|> markers
|
|
242
|
+
for (const c of parseToolCalls(text)) {
|
|
243
|
+
out.push({
|
|
244
|
+
id: `call_${crypto.randomUUID().slice(0, 8)}`,
|
|
245
|
+
type: "function",
|
|
246
|
+
function: { name: c.name, arguments: JSON.stringify(c.arguments) },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Decoded-text stop-sequence matcher with streaming hold-back. Matching
|
|
255
|
+
* on text (not token ids) catches sequences that span token boundaries
|
|
256
|
+
* or tokenize differently in context; current mlx-lm uses token-id
|
|
257
|
+
* state machines and misses those. Text that could be the start of a
|
|
258
|
+
* stop sequence is withheld until disambiguated, so SSE clients never
|
|
259
|
+
* see any part of the stop sequence itself. */
|
|
260
|
+
export class StopMatcher {
|
|
261
|
+
#pending = "";
|
|
262
|
+
stopped = false;
|
|
263
|
+
|
|
264
|
+
constructor(readonly sequences: string[]) {}
|
|
265
|
+
|
|
266
|
+
/** Feed a text delta; returns the prefix that is now safe to emit.
|
|
267
|
+
* After a match fires (`stopped`), text before the match is returned
|
|
268
|
+
* and everything from the match on is discarded. */
|
|
269
|
+
push(text: string): string {
|
|
270
|
+
if (this.stopped) return "";
|
|
271
|
+
if (this.sequences.length === 0) return text;
|
|
272
|
+
this.#pending += text;
|
|
273
|
+
// earliest full match wins
|
|
274
|
+
let cut = -1;
|
|
275
|
+
for (const seq of this.sequences) {
|
|
276
|
+
const i = this.#pending.indexOf(seq);
|
|
277
|
+
if (i !== -1 && (cut === -1 || i < cut)) cut = i;
|
|
278
|
+
}
|
|
279
|
+
if (cut !== -1) {
|
|
280
|
+
this.stopped = true;
|
|
281
|
+
const out = this.#pending.slice(0, cut);
|
|
282
|
+
this.#pending = "";
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
// hold back the longest tail that is a proper prefix of any sequence
|
|
286
|
+
let hold = 0;
|
|
287
|
+
for (const seq of this.sequences) {
|
|
288
|
+
const max = Math.min(seq.length - 1, this.#pending.length);
|
|
289
|
+
for (let k = max; k > hold; k--) {
|
|
290
|
+
if (this.#pending.endsWith(seq.slice(0, k))) {
|
|
291
|
+
hold = k;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (hold === 0) {
|
|
297
|
+
const out = this.#pending;
|
|
298
|
+
this.#pending = "";
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
const out = this.#pending.slice(0, -hold);
|
|
302
|
+
this.#pending = this.#pending.slice(-hold);
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Generation ended without a match — release any held-back text. */
|
|
307
|
+
flush(): string {
|
|
308
|
+
const out = this.#pending;
|
|
309
|
+
this.#pending = "";
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Split Qwen-style inline <think>...</think> markup into OpenAI reasoning
|
|
315
|
+
* deltas/content. This keeps raw tags out of normal chat text while giving
|
|
316
|
+
* pi (TUI + web) proper thinking_delta events. It is streaming-safe: partial
|
|
317
|
+
* tag prefixes are held until disambiguated. */
|
|
318
|
+
export class ThinkingTagSplitter {
|
|
319
|
+
#pending = "";
|
|
320
|
+
#inThinking: boolean;
|
|
321
|
+
reasoning = "";
|
|
322
|
+
content = "";
|
|
323
|
+
|
|
324
|
+
/** `startInThinking` seeds the parser INSIDE a <think> block. Needed for
|
|
325
|
+
* templates (Qwen3.5, MiniCPM5) that prime an OPEN `<think>` in the
|
|
326
|
+
* generation prompt when thinking is enabled: the model's output then
|
|
327
|
+
* starts mid-reasoning and emits only the closing `</think>`, never an
|
|
328
|
+
* opening tag. Without this seed the whole chain-of-thought leaks into
|
|
329
|
+
* `content` and the `reasoning` field stays empty. */
|
|
330
|
+
constructor(private readonly enabled: boolean, startInThinking = false) {
|
|
331
|
+
this.#inThinking = startInThinking;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#safePrefixUntilTag(tag: string): string {
|
|
335
|
+
const i = this.#pending.indexOf(tag);
|
|
336
|
+
if (i !== -1) return this.#pending.slice(0, i);
|
|
337
|
+
let hold = 0;
|
|
338
|
+
for (let k = Math.min(tag.length - 1, this.#pending.length); k > 0; k--) {
|
|
339
|
+
if (this.#pending.endsWith(tag.slice(0, k))) { hold = k; break; }
|
|
340
|
+
}
|
|
341
|
+
return this.#pending.slice(0, this.#pending.length - hold);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
push(text: string): { content: string; reasoning: string } {
|
|
345
|
+
if (!this.enabled) {
|
|
346
|
+
this.content += text;
|
|
347
|
+
return { content: text, reasoning: "" };
|
|
348
|
+
}
|
|
349
|
+
this.#pending += text;
|
|
350
|
+
let content = "";
|
|
351
|
+
let reasoning = "";
|
|
352
|
+
while (this.#pending) {
|
|
353
|
+
const tag = this.#inThinking ? "</think>" : "<think>";
|
|
354
|
+
const i = this.#pending.indexOf(tag);
|
|
355
|
+
const emit = i === -1 ? this.#safePrefixUntilTag(tag) : this.#pending.slice(0, i);
|
|
356
|
+
if (!emit && i === -1) break;
|
|
357
|
+
if (emit) {
|
|
358
|
+
if (this.#inThinking) reasoning += emit;
|
|
359
|
+
else content += emit;
|
|
360
|
+
this.#pending = this.#pending.slice(emit.length);
|
|
361
|
+
}
|
|
362
|
+
if (i !== -1 && this.#pending.startsWith(tag)) {
|
|
363
|
+
this.#pending = this.#pending.slice(tag.length);
|
|
364
|
+
this.#inThinking = !this.#inThinking;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (i === -1) break;
|
|
368
|
+
}
|
|
369
|
+
this.content += content;
|
|
370
|
+
this.reasoning += reasoning;
|
|
371
|
+
return { content, reasoning };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
flush(): { content: string; reasoning: string } {
|
|
375
|
+
if (!this.enabled) return { content: "", reasoning: "" };
|
|
376
|
+
const out = this.#inThinking
|
|
377
|
+
? { content: "", reasoning: this.#pending }
|
|
378
|
+
: { content: this.#pending, reasoning: "" };
|
|
379
|
+
this.#pending = "";
|
|
380
|
+
this.content += out.content;
|
|
381
|
+
this.reasoning += out.reasoning;
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Incremental detokenizer: emits the longest stable decoded prefix.
|
|
387
|
+
*
|
|
388
|
+
* Byte parity with mlx-lm's streaming detokenizers (the drop-in contract is
|
|
389
|
+
* rendered BYTES, not just token ids). For BPE/ByteLevel tokenizers, two
|
|
390
|
+
* mlx-lm 0.31.3 BPEStreamingDetokenizer behaviors our full-sequence decode
|
|
391
|
+
* lacks (tokenizer_utils.py:195-226):
|
|
392
|
+
*
|
|
393
|
+
* 1. `trimsLeadingSpace` — mlx-lm drops ONE leading " " at the start of the
|
|
394
|
+
* generated sequence (`_maybe_trim_space`); trim it here. SPM decode
|
|
395
|
+
* already matches (see LoadedTokenizer.trimsLeadingSpace).
|
|
396
|
+
* 2. `bareSpaceTokenId` — add_token WITHHOLDS a single-char byte-32 token
|
|
397
|
+
* ("Ġ") in `_unflushed` ("For single spaces wait until the next token"),
|
|
398
|
+
* flushing it together with the NEXT token — and mlx_lm.server NEVER
|
|
399
|
+
* calls detokenizer.finalize() (zero hits in server.py 0.31.3), so a
|
|
400
|
+
* generation ENDING on bare-space token(s) silently drops their spaces
|
|
401
|
+
* from the served bytes. push() withholds those spaces; flush() drops a
|
|
402
|
+
* trailing bare-space run (the held text dies with the request, exactly
|
|
403
|
+
* like mlx-lm serve). Re-check both if upstream ever adds a finalize()
|
|
404
|
+
* call. LATENT HAZARD (deliberately not emulated): models with
|
|
405
|
+
* clean_up_tokenization_spaces=true get an ADDITIONAL mid-stream rule
|
|
406
|
+
* (`_space_matches`: held space dropped before "." "," "'s" …) — both
|
|
407
|
+
* current BPE targets have it false (MiniCPM5), so it never fires here.
|
|
408
|
+
*
|
|
409
|
+
* Exported for unit tests (serve-detok mlx-lm byte parity). */
|
|
410
|
+
export class StreamDecoder {
|
|
411
|
+
static readonly #WINDOW_TOKENS = 32;
|
|
412
|
+
#ids: number[] = [];
|
|
413
|
+
#emitted = "";
|
|
414
|
+
#warnedRevision = false;
|
|
415
|
+
/** Token index where the bounded decode suffix begins. */
|
|
416
|
+
#windowStart = 0;
|
|
417
|
+
/** Exact decoded text before #windowStart, established by suffix matching. */
|
|
418
|
+
#windowPrefix = "";
|
|
419
|
+
readonly #trimLeadingSpace: boolean;
|
|
420
|
+
readonly #bareSpaceId: number | undefined;
|
|
421
|
+
|
|
422
|
+
constructor(
|
|
423
|
+
readonly tokenizer: LoadedTokenizer,
|
|
424
|
+
readonly skipSpecialTokens = true,
|
|
425
|
+
) {
|
|
426
|
+
this.#trimLeadingSpace = tokenizer.trimsLeadingSpace === true;
|
|
427
|
+
this.#bareSpaceId = tokenizer.bareSpaceTokenId;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#decode(ids: number[]): string {
|
|
431
|
+
const full = this.#decodeRaw(ids);
|
|
432
|
+
return this.#trimLeadingSpace && full.startsWith(" ") ? full.slice(1) : full;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
#decodeRaw(ids: number[]): string {
|
|
436
|
+
return this.tokenizer.decode(ids, this.skipSpecialTokens);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Decode only a bounded suffix once an exact text anchor can be proven.
|
|
440
|
+
*
|
|
441
|
+
* At each rebase, decode a candidate suffix independently and accept it only
|
|
442
|
+
* when it is literally the suffix of the exact text decoded so far. This
|
|
443
|
+
* preserves ByteLevel/SPM boundary behavior while reducing steady-state
|
|
444
|
+
* decode work from the whole generation to 32–64 tokens. If no suffix can be
|
|
445
|
+
* anchored (an unusual/global decoder), leave #windowStart at zero and keep
|
|
446
|
+
* the full-history correctness path. */
|
|
447
|
+
#decodeIncremental(): string {
|
|
448
|
+
const full = this.#windowStart === 0
|
|
449
|
+
? this.#decode(this.#ids)
|
|
450
|
+
: this.#windowPrefix + this.#decodeRaw(this.#ids.slice(this.#windowStart));
|
|
451
|
+
|
|
452
|
+
if (this.#ids.length - this.#windowStart < StreamDecoder.#WINDOW_TOKENS * 2)
|
|
453
|
+
return full;
|
|
454
|
+
|
|
455
|
+
const preferred = this.#ids.length - StreamDecoder.#WINDOW_TOKENS;
|
|
456
|
+
const oldest = Math.max(this.#windowStart, preferred - StreamDecoder.#WINDOW_TOKENS);
|
|
457
|
+
for (let start = preferred; start >= oldest; start--) {
|
|
458
|
+
const tail = this.#decodeRaw(this.#ids.slice(start));
|
|
459
|
+
if ((tail.length > 0 || full.length === 0) && full.endsWith(tail)) {
|
|
460
|
+
this.#windowStart = start;
|
|
461
|
+
this.#windowPrefix = full.slice(0, full.length - tail.length);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return full;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
push(token: number): string {
|
|
469
|
+
this.#ids.push(token);
|
|
470
|
+
// Bare-space hold-back (mlx-lm add_token keeps "Ġ" in _unflushed): don't
|
|
471
|
+
// advance #emitted; the held space(s) flush as part of the next
|
|
472
|
+
// non-bare-space token's delta — consecutive bare spaces accumulate.
|
|
473
|
+
if (token === this.#bareSpaceId) return "";
|
|
474
|
+
const full = this.#decodeIncremental();
|
|
475
|
+
// hold back a trailing replacement char (partial multi-byte sequence)
|
|
476
|
+
const stable = full.endsWith("�") ? full.slice(0, -1) : full;
|
|
477
|
+
if (!stable.startsWith(this.#emitted)) {
|
|
478
|
+
// The decoder revised already-streamed text (cleanup rules — never
|
|
479
|
+
// fires for the shipped tokenizers, which all have
|
|
480
|
+
// clean_up_tokenization_spaces=false; see the LATENT HAZARD note in
|
|
481
|
+
// the class doc). An SSE client cannot un-receive bytes, so the old
|
|
482
|
+
// "re-emit from scratch" answer DUPLICATED the whole stream
|
|
483
|
+
// (2026-07-07 review). Truncate-safe resync instead: emit only the
|
|
484
|
+
// length-extension and keep the emitted watermark monotone — byte
|
|
485
|
+
// drift is confined to the revised span, and the once-per-stream
|
|
486
|
+
// warning makes the tokenizer that needs real _space_matches
|
|
487
|
+
// emulation loud instead of silently wrong.
|
|
488
|
+
if (!this.#warnedRevision) {
|
|
489
|
+
this.#warnedRevision = true;
|
|
490
|
+
console.warn(
|
|
491
|
+
"[detok] decoder revised already-streamed text (clean_up_tokenization_spaces?) — " +
|
|
492
|
+
"resyncing without re-emit; add mlx-lm's cleanup-rule emulation for this tokenizer",
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
if (stable.length <= this.#emitted.length) return "";
|
|
496
|
+
const out = stable.slice(this.#emitted.length);
|
|
497
|
+
this.#emitted = stable;
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
const delta = stable.slice(this.#emitted.length);
|
|
501
|
+
this.#emitted = stable;
|
|
502
|
+
return delta;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
flush(): string {
|
|
506
|
+
// mlx_lm.server never finalize()s: a trailing bare-space run stays
|
|
507
|
+
// withheld forever, so its text is dropped from the served bytes.
|
|
508
|
+
let ids = this.#ids;
|
|
509
|
+
if (this.#bareSpaceId !== undefined) {
|
|
510
|
+
let n = ids.length;
|
|
511
|
+
while (n > 0 && ids[n - 1] === this.#bareSpaceId) n--;
|
|
512
|
+
if (n < ids.length) ids = ids.slice(0, n);
|
|
513
|
+
}
|
|
514
|
+
const full = this.#decode(ids);
|
|
515
|
+
const delta = full.slice(this.#emitted.length);
|
|
516
|
+
this.#emitted = full;
|
|
517
|
+
return delta;
|
|
518
|
+
}
|
|
519
|
+
}
|