mlx-bun 0.0.12 → 0.2.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 +40 -24
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/archive/planning/release-notes-v0.0.13.md +77 -0
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +8 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -1
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +86 -19
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/chat-template.ts +34 -1
- package/src/cli.ts +131 -36
- package/src/config.ts +10 -0
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +768 -1367
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
|
@@ -26,7 +26,10 @@ import { cpuStream, MlxArray } from "../mlx/array";
|
|
|
26
26
|
import * as ops from "../mlx/ops";
|
|
27
27
|
import { trainForward } from "../train/forward";
|
|
28
28
|
import type { RuntimeModel } from "../model/factory";
|
|
29
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
QuantizedLinear,
|
|
31
|
+
QuantizedLinearState,
|
|
32
|
+
} from "../model/gemma4-base";
|
|
30
33
|
|
|
31
34
|
/** Per-layer KL sensitivity at each candidate bit-width (port of
|
|
32
35
|
* optiq SensitivityResult). */
|
|
@@ -43,46 +46,23 @@ export interface AnalyzeSensitivityOptions {
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// --------------------------------------------------------------------------
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// gemma4-base.ts declares QuantizedLinear's w/scales/biases/spec as `readonly`
|
|
49
|
-
// (they never change at inference time). The sensitivity sweep needs to swap a
|
|
50
|
-
// layer's quantized weight transiently. Rather than edit the model file, we
|
|
51
|
-
// poke the public fields here via a typed cast — a quantize-only mutation
|
|
52
|
-
// helper. The fields are public, so this is a write to public state, not a
|
|
53
|
-
// reach into private internals. Flagged in the port report.
|
|
49
|
+
// Scoped state exchange for sensitivity probes. QuantizedLinear owns the only
|
|
50
|
+
// mutation capability; inference consumers retain a read-only getter surface.
|
|
54
51
|
// --------------------------------------------------------------------------
|
|
55
52
|
|
|
56
|
-
/** Mutable view of the public QuantizedLinear fields we swap. */
|
|
57
|
-
type MutableQuantizedLinear = {
|
|
58
|
-
w: MlxArray;
|
|
59
|
-
scales: MlxArray;
|
|
60
|
-
biases: MlxArray | null;
|
|
61
|
-
spec: ops.QuantSpec;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
53
|
/** Snapshot of a QuantizedLinear's quantized state (for restore). */
|
|
65
|
-
|
|
66
|
-
w: MlxArray;
|
|
67
|
-
scales: MlxArray;
|
|
68
|
-
biases: MlxArray | null;
|
|
69
|
-
spec: ops.QuantSpec;
|
|
70
|
-
}
|
|
54
|
+
type LayerState = QuantizedLinearState;
|
|
71
55
|
|
|
72
56
|
function captureState(layer: QuantizedLinear): LayerState {
|
|
73
57
|
return { w: layer.w, scales: layer.scales, biases: layer.biases, spec: layer.spec };
|
|
74
58
|
}
|
|
75
59
|
|
|
76
60
|
function restoreState(layer: QuantizedLinear, state: LayerState): void {
|
|
77
|
-
const
|
|
61
|
+
const transient = layer.exchangeQuantizedState(state);
|
|
78
62
|
// Dispose the transient swapped-in tensors before restoring the originals.
|
|
79
|
-
if (
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
82
|
-
m.w = state.w;
|
|
83
|
-
m.scales = state.scales;
|
|
84
|
-
m.biases = state.biases;
|
|
85
|
-
m.spec = state.spec;
|
|
63
|
+
if (transient.w !== state.w) transient.w.dispose();
|
|
64
|
+
if (transient.scales !== state.scales) transient.scales.dispose();
|
|
65
|
+
if (transient.biases && transient.biases !== state.biases) transient.biases.dispose();
|
|
86
66
|
}
|
|
87
67
|
|
|
88
68
|
/** Quantize `bf16Weight` at `newBits`/`groupSize` and swap it into `layer`
|
|
@@ -97,15 +77,16 @@ function mutateLayerToBits(
|
|
|
97
77
|
original: LayerState,
|
|
98
78
|
): void {
|
|
99
79
|
const q = ops.quantize(bf16Weight, groupSize, newBits, "affine", cpuStream);
|
|
100
|
-
const
|
|
80
|
+
const previous = layer.exchangeQuantizedState({
|
|
81
|
+
w: q.packed,
|
|
82
|
+
scales: q.scales,
|
|
83
|
+
biases: q.biases,
|
|
84
|
+
spec: { bits: newBits, groupSize, mode: "affine" },
|
|
85
|
+
});
|
|
101
86
|
// Free any previously swapped-in transient tensors (not the original).
|
|
102
|
-
if (
|
|
103
|
-
if (
|
|
104
|
-
if (
|
|
105
|
-
m.w = q.packed;
|
|
106
|
-
m.scales = q.scales;
|
|
107
|
-
m.biases = q.biases;
|
|
108
|
-
m.spec = { bits: newBits, groupSize, mode: "affine" };
|
|
87
|
+
if (previous.w !== original.w) previous.w.dispose();
|
|
88
|
+
if (previous.scales !== original.scales) previous.scales.dispose();
|
|
89
|
+
if (previous.biases && previous.biases !== original.biases) previous.biases.dispose();
|
|
109
90
|
ops.evalAll([q.packed, q.scales, q.biases]);
|
|
110
91
|
}
|
|
111
92
|
|
|
@@ -193,29 +174,30 @@ export function analyzeSensitivityExact(
|
|
|
193
174
|
ops.evalAll([bf16Source]);
|
|
194
175
|
|
|
195
176
|
const sensitivities: Record<number, number> = {};
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
177
|
+
try {
|
|
178
|
+
for (const bits of candidateBits) {
|
|
179
|
+
if (bits === baselineBits) {
|
|
180
|
+
// KL is 0 by construction (re-quantizing to the baseline bit-width
|
|
181
|
+
// reproduces the running model). Skip the forwards.
|
|
182
|
+
sensitivities[bits] = 0.0;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
mutateLayerToBits(layer, bf16Source, bits, groupSize, original);
|
|
186
|
+
|
|
187
|
+
let klSum = 0;
|
|
188
|
+
for (let i = 0; i < calArrays.length; i++) {
|
|
189
|
+
const cur = trainForward(model, calArrays[i]!);
|
|
190
|
+
klSum += klFromRef(cur, refLogits[i]!);
|
|
191
|
+
cur.dispose();
|
|
192
|
+
}
|
|
193
|
+
sensitivities[bits] = klSum / Math.max(calArrays.length, 1);
|
|
203
194
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const cur = trainForward(model, calArrays[i]!);
|
|
209
|
-
klSum += klFromRef(cur, refLogits[i]!);
|
|
210
|
-
cur.dispose();
|
|
211
|
-
}
|
|
212
|
-
sensitivities[bits] = klSum / Math.max(calArrays.length, 1);
|
|
195
|
+
} finally {
|
|
196
|
+
// Restore even when quantization or a calibration forward throws.
|
|
197
|
+
restoreState(layer, original);
|
|
198
|
+
bf16Source.dispose();
|
|
213
199
|
}
|
|
214
200
|
|
|
215
|
-
// Restore the original quantized weight; release the bf16 source.
|
|
216
|
-
restoreState(layer, original);
|
|
217
|
-
bf16Source.dispose();
|
|
218
|
-
|
|
219
201
|
// param count = product of the dequantized (full) weight shape.
|
|
220
202
|
const fullShape = [layer.outFeatures, layer.inFeatures];
|
|
221
203
|
const paramCount = fullShape.reduce((a, b) => a * b, 1);
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Quantizer-facing offline weight-transform seam. Recipe math lives in
|
|
2
|
+
// rotate.ts; this module owns planning, family adaptation, and tensor ownership
|
|
3
|
+
// so the artifact writer has one contract for every fold implementation.
|
|
4
|
+
|
|
5
|
+
import type { ModelConfig } from "../config";
|
|
6
|
+
import type { MlxArray } from "../mlx/array";
|
|
7
|
+
import type { Weights } from "../weights";
|
|
8
|
+
import {
|
|
9
|
+
foldLlamaWeights,
|
|
10
|
+
planQwen35Fold,
|
|
11
|
+
planQwenMtpFold,
|
|
12
|
+
QwenFoldContext,
|
|
13
|
+
type FoldGeometry,
|
|
14
|
+
type FoldOptions,
|
|
15
|
+
type QwenFoldOptions,
|
|
16
|
+
type QwenFoldPlan,
|
|
17
|
+
} from "./rotate";
|
|
18
|
+
|
|
19
|
+
/** Serialized alongside a quantized artifact so a folded basis is a property
|
|
20
|
+
* of the artifact, not an out-of-band experiment-script convention. */
|
|
21
|
+
export interface WeightTransformMetadata {
|
|
22
|
+
id: string;
|
|
23
|
+
seed: number;
|
|
24
|
+
family: string;
|
|
25
|
+
deviations: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Pure output plan. `sourceByOutput` makes cloning/untie operations explicit:
|
|
29
|
+
* most outputs map to themselves; a synthesized head maps to its embedding. */
|
|
30
|
+
export interface WeightTransformPlan {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly outputNames: readonly string[];
|
|
33
|
+
readonly sourceByOutput: ReadonlyMap<string, string>;
|
|
34
|
+
readonly untieWordEmbeddings: boolean;
|
|
35
|
+
readonly metadata: WeightTransformMetadata;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Per-run executor. `apply` returns a lazy, owned array and never disposes
|
|
39
|
+
* `source`; the quantizer owns the result and the Weights object owns source. */
|
|
40
|
+
export interface WeightTransformContext {
|
|
41
|
+
apply(outputName: string, source: MlxArray): MlxArray;
|
|
42
|
+
dispose(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Common seam for the Llama, Qwen3.5 trunk, and Qwen MTP fold recipes. */
|
|
46
|
+
export interface WeightTransform {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
/** Name/config analysis only: no mlx arrays, model load, or device work. */
|
|
49
|
+
plan(names: readonly string[], config: ModelConfig): WeightTransformPlan;
|
|
50
|
+
createContext(weights: Weights, plan: WeightTransformPlan): WeightTransformContext;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LlamaWeightTransformPlan extends WeightTransformPlan {
|
|
54
|
+
readonly kind: "llama";
|
|
55
|
+
readonly geometry: FoldGeometry;
|
|
56
|
+
readonly options: FoldOptions;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface QwenWeightTransformPlan extends WeightTransformPlan {
|
|
60
|
+
readonly kind: "qwen3_5" | "qwen3_5_mtp";
|
|
61
|
+
readonly hiddenSize: number;
|
|
62
|
+
readonly seed: number;
|
|
63
|
+
readonly fold: QwenFoldPlan;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sourceMap(names: readonly string[]): Map<string, string> {
|
|
67
|
+
return new Map(names.map((name) => [name, name]));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertPow2(n: number, what: string): void {
|
|
71
|
+
if (n < 2 || (n & (n - 1)) !== 0)
|
|
72
|
+
throw new Error(
|
|
73
|
+
`rotation fold: ${what}=${n} is not a power of two — the Kronecker ` +
|
|
74
|
+
"Hadamard path is not implemented",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Pure Llama fold plan. Array work remains inside createContext/apply. */
|
|
79
|
+
export function planLlamaWeightTransform(
|
|
80
|
+
names: readonly string[],
|
|
81
|
+
config: ModelConfig,
|
|
82
|
+
options: FoldOptions,
|
|
83
|
+
): LlamaWeightTransformPlan {
|
|
84
|
+
const has = new Set(names);
|
|
85
|
+
for (const required of ["model.embed_tokens.weight", "model.norm.weight"])
|
|
86
|
+
if (!has.has(required)) throw new Error(`rotation fold: missing tensor ${required}`);
|
|
87
|
+
if (has.has("lm_head.weight"))
|
|
88
|
+
throw new Error("rotation fold: llama adapter currently requires tied embeddings");
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < config.text.numHiddenLayers; i++) {
|
|
91
|
+
const layer = `model.layers.${i}`;
|
|
92
|
+
for (const required of [
|
|
93
|
+
`${layer}.input_layernorm.weight`,
|
|
94
|
+
`${layer}.post_attention_layernorm.weight`,
|
|
95
|
+
`${layer}.self_attn.q_proj.weight`,
|
|
96
|
+
`${layer}.self_attn.k_proj.weight`,
|
|
97
|
+
`${layer}.self_attn.v_proj.weight`,
|
|
98
|
+
`${layer}.self_attn.o_proj.weight`,
|
|
99
|
+
`${layer}.mlp.gate_proj.weight`,
|
|
100
|
+
`${layer}.mlp.up_proj.weight`,
|
|
101
|
+
`${layer}.mlp.down_proj.weight`,
|
|
102
|
+
]) {
|
|
103
|
+
if (!has.has(required)) throw new Error(`rotation fold: missing tensor ${required}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const geometry: FoldGeometry = {
|
|
108
|
+
numLayers: config.text.numHiddenLayers,
|
|
109
|
+
hiddenSize: config.text.hiddenSize,
|
|
110
|
+
numHeads: config.text.numAttentionHeads,
|
|
111
|
+
numKvHeads: config.text.numKeyValueHeads,
|
|
112
|
+
headDim: config.text.headDim,
|
|
113
|
+
};
|
|
114
|
+
if (options.r1 ?? true) assertPow2(geometry.hiddenSize, "hidden_size");
|
|
115
|
+
if (options.r2 ?? true) assertPow2(geometry.headDim, "head_dim");
|
|
116
|
+
|
|
117
|
+
const outputNames = [...names, "lm_head.weight"];
|
|
118
|
+
const sourceByOutput = sourceMap(names);
|
|
119
|
+
sourceByOutput.set("lm_head.weight", "model.embed_tokens.weight");
|
|
120
|
+
return {
|
|
121
|
+
kind: "llama",
|
|
122
|
+
id: "rotation.llama",
|
|
123
|
+
outputNames,
|
|
124
|
+
sourceByOutput,
|
|
125
|
+
untieWordEmbeddings: true,
|
|
126
|
+
geometry,
|
|
127
|
+
options: { ...options },
|
|
128
|
+
metadata: {
|
|
129
|
+
id: "rotation.llama",
|
|
130
|
+
seed: options.seed,
|
|
131
|
+
family: "llama",
|
|
132
|
+
deviations: [
|
|
133
|
+
"no-embedding-mean-centering",
|
|
134
|
+
"no-R4-downproj-input-fold",
|
|
135
|
+
"no-R3",
|
|
136
|
+
"gamma-kept-in-module-as-ones (eps preserved)",
|
|
137
|
+
"fold-precision-f32",
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createLlamaContext(
|
|
144
|
+
weights: Weights,
|
|
145
|
+
plan: LlamaWeightTransformPlan,
|
|
146
|
+
): WeightTransformContext {
|
|
147
|
+
const folded = foldLlamaWeights(weights, plan.geometry, plan.options);
|
|
148
|
+
const pending = new Map(folded.tensors.map((tensor) => [tensor.name, tensor.array]));
|
|
149
|
+
return {
|
|
150
|
+
apply(outputName, source) {
|
|
151
|
+
const transformed = pending.get(outputName);
|
|
152
|
+
if (transformed) {
|
|
153
|
+
pending.delete(outputName);
|
|
154
|
+
return transformed;
|
|
155
|
+
}
|
|
156
|
+
// Preserve family-specific tensors outside the fold corridor.
|
|
157
|
+
return source.astype(source.dtype);
|
|
158
|
+
},
|
|
159
|
+
dispose() {
|
|
160
|
+
for (const array of pending.values()) array.dispose();
|
|
161
|
+
pending.clear();
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function qwenTransformPlan(
|
|
167
|
+
names: readonly string[],
|
|
168
|
+
config: ModelConfig,
|
|
169
|
+
seed: number,
|
|
170
|
+
companion: boolean,
|
|
171
|
+
prefix?: string,
|
|
172
|
+
): QwenWeightTransformPlan {
|
|
173
|
+
const fold = companion
|
|
174
|
+
? planQwenMtpFold(names)
|
|
175
|
+
: planQwen35Fold(
|
|
176
|
+
names,
|
|
177
|
+
prefix ?? (names.includes("language_model.model.norm.weight") ? "language_model." : ""),
|
|
178
|
+
);
|
|
179
|
+
const outputNames = [...names];
|
|
180
|
+
const sourceByOutput = sourceMap(names);
|
|
181
|
+
if (fold.extraHead) {
|
|
182
|
+
outputNames.push(fold.extraHead.name);
|
|
183
|
+
sourceByOutput.set(fold.extraHead.name, fold.extraHead.from);
|
|
184
|
+
}
|
|
185
|
+
const family = companion ? "qwen3_5_mtp" : "qwen3_5";
|
|
186
|
+
return {
|
|
187
|
+
kind: family,
|
|
188
|
+
id: `rotation.${family}`,
|
|
189
|
+
outputNames,
|
|
190
|
+
sourceByOutput,
|
|
191
|
+
untieWordEmbeddings: fold.extraHead !== null,
|
|
192
|
+
hiddenSize: config.text.hiddenSize,
|
|
193
|
+
seed,
|
|
194
|
+
fold,
|
|
195
|
+
metadata: {
|
|
196
|
+
id: `rotation.${family}`,
|
|
197
|
+
seed,
|
|
198
|
+
family,
|
|
199
|
+
deviations: [...fold.deviations],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createQwenContext(
|
|
205
|
+
weights: Weights,
|
|
206
|
+
plan: QwenWeightTransformPlan,
|
|
207
|
+
): WeightTransformContext {
|
|
208
|
+
// Lazy mode composes into the quantizer's existing writer. The experiment
|
|
209
|
+
// script keeps QwenFoldContext's eager default because it releases shards.
|
|
210
|
+
const context = new QwenFoldContext(
|
|
211
|
+
weights,
|
|
212
|
+
plan.hiddenSize,
|
|
213
|
+
plan.seed,
|
|
214
|
+
plan.fold.gammaNames,
|
|
215
|
+
false,
|
|
216
|
+
);
|
|
217
|
+
return {
|
|
218
|
+
apply(outputName, source) {
|
|
219
|
+
const extra = plan.fold.extraHead;
|
|
220
|
+
if (extra && outputName === extra.name)
|
|
221
|
+
return context.apply({ kind: "input", gamma: extra.gamma }, source);
|
|
222
|
+
const op = plan.fold.ops.get(outputName);
|
|
223
|
+
if (!op) throw new Error(`weight transform ${plan.id}: unplanned output ${outputName}`);
|
|
224
|
+
return context.apply(op, source);
|
|
225
|
+
},
|
|
226
|
+
dispose: () => context.dispose(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Llama-family γ+R1+R2 adapter. */
|
|
231
|
+
export function llamaWeightTransform(options: FoldOptions): WeightTransform {
|
|
232
|
+
return {
|
|
233
|
+
id: "rotation.llama",
|
|
234
|
+
plan: (names, config) => planLlamaWeightTransform(names, config, options),
|
|
235
|
+
createContext(weights, plan) {
|
|
236
|
+
if (!("kind" in plan) || plan.kind !== "llama")
|
|
237
|
+
throw new Error(`weight transform rotation.llama received plan ${plan.id}`);
|
|
238
|
+
return createLlamaContext(weights, plan as LlamaWeightTransformPlan);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Qwen3.5 trunk/VL adapter (R1 only because of the attention output gate). */
|
|
244
|
+
export function qwen35WeightTransform(options: QwenFoldOptions): WeightTransform {
|
|
245
|
+
return {
|
|
246
|
+
id: "rotation.qwen3_5",
|
|
247
|
+
plan: (names, config) =>
|
|
248
|
+
qwenTransformPlan(names, config, options.seed, false, options.prefix),
|
|
249
|
+
createContext(weights, plan) {
|
|
250
|
+
if (!("kind" in plan) || plan.kind !== "qwen3_5")
|
|
251
|
+
throw new Error(`weight transform rotation.qwen3_5 received plan ${plan.id}`);
|
|
252
|
+
return createQwenContext(weights, plan as QwenWeightTransformPlan);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Qwen3.5 MTP companion adapter. Must use the same seed as its trunk. */
|
|
258
|
+
export function qwenMtpWeightTransform(seed: number): WeightTransform {
|
|
259
|
+
return {
|
|
260
|
+
id: "rotation.qwen3_5_mtp",
|
|
261
|
+
plan: (names, config) => qwenTransformPlan(names, config, seed, true),
|
|
262
|
+
createContext(weights, plan) {
|
|
263
|
+
if (!("kind" in plan) || plan.kind !== "qwen3_5_mtp")
|
|
264
|
+
throw new Error(`weight transform rotation.qwen3_5_mtp received plan ${plan.id}`);
|
|
265
|
+
return createQwenContext(weights, plan as QwenWeightTransformPlan);
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Select one of the three adapters from the source model and tensor schema. */
|
|
271
|
+
export function automaticRotationWeightTransform(
|
|
272
|
+
options: FoldOptions & { prefix?: string },
|
|
273
|
+
): WeightTransform {
|
|
274
|
+
return {
|
|
275
|
+
id: "rotation.auto",
|
|
276
|
+
plan(names, config) {
|
|
277
|
+
if (names.includes("fc.weight") && names.includes("pre_fc_norm_hidden.weight"))
|
|
278
|
+
return qwenTransformPlan(names, config, options.seed, true);
|
|
279
|
+
if (config.modelType === "qwen3_5")
|
|
280
|
+
return qwenTransformPlan(names, config, options.seed, false, options.prefix);
|
|
281
|
+
if (config.modelType === "llama")
|
|
282
|
+
return planLlamaWeightTransform(names, config, options);
|
|
283
|
+
throw new Error(`rotation transform: unsupported model_type ${config.modelType}`);
|
|
284
|
+
},
|
|
285
|
+
createContext(weights, plan) {
|
|
286
|
+
if (!("kind" in plan)) throw new Error(`rotation transform: invalid plan ${plan.id}`);
|
|
287
|
+
return plan.kind === "llama"
|
|
288
|
+
? createLlamaContext(weights, plan as LlamaWeightTransformPlan)
|
|
289
|
+
: createQwenContext(weights, plan as QwenWeightTransformPlan);
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
package/src/responses.ts
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
// - previous_response_id is echoed in the response when the request
|
|
18
18
|
// carried one (oracle always emits null).
|
|
19
19
|
|
|
20
|
+
import type {
|
|
21
|
+
CompletionEvent,
|
|
22
|
+
CompletionStreamProtocol,
|
|
23
|
+
} from "./serve/completion-sink";
|
|
24
|
+
|
|
20
25
|
// ---------------------------------------------------------------------
|
|
21
26
|
// Response store (previous_response_id resumption)
|
|
22
27
|
// ---------------------------------------------------------------------
|
|
@@ -167,6 +172,55 @@ function coerceText(content: unknown): string {
|
|
|
167
172
|
return parts.join("");
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
/** Convert a Responses message content array to chat-shaped content: text
|
|
176
|
+
* flattens, media TRANSLATES (`input_image` → `image_url`, incl. the
|
|
177
|
+
* Responses-standard string `image_url` field; video symmetrically), and
|
|
178
|
+
* unknown non-text parts REJECT loudly — a silent media drop answers
|
|
179
|
+
* confidently about content the model never saw (the anthropic.ts audio
|
|
180
|
+
* doctrine). Text-only arrays keep the legacy flattened-string shape. */
|
|
181
|
+
function coerceContent(content: unknown): string | Record<string, unknown>[] {
|
|
182
|
+
if (content == null) return "";
|
|
183
|
+
if (typeof content === "string") return content;
|
|
184
|
+
if (!Array.isArray(content)) return String(content);
|
|
185
|
+
const parts: Record<string, unknown>[] = [];
|
|
186
|
+
let hasMedia = false;
|
|
187
|
+
for (const part of content) {
|
|
188
|
+
if (typeof part === "string") {
|
|
189
|
+
parts.push({ type: "text", text: part });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!part || typeof part !== "object") continue;
|
|
193
|
+
const p = part as Record<string, unknown>;
|
|
194
|
+
if (p.type === "input_image" || p.type === "image_url") {
|
|
195
|
+
const url = typeof p.image_url === "string"
|
|
196
|
+
? p.image_url
|
|
197
|
+
: (p.image_url as { url?: string } | undefined)?.url;
|
|
198
|
+
if (typeof url !== "string")
|
|
199
|
+
throw new Error("input_image part missing image_url");
|
|
200
|
+
parts.push({ type: "image_url", image_url: { url } });
|
|
201
|
+
hasMedia = true;
|
|
202
|
+
} else if (p.type === "input_video" || p.type === "video_url") {
|
|
203
|
+
const url = typeof p.video_url === "string"
|
|
204
|
+
? p.video_url
|
|
205
|
+
: (p.video_url as { url?: string } | undefined)?.url;
|
|
206
|
+
if (typeof url !== "string")
|
|
207
|
+
throw new Error("input_video part missing video_url");
|
|
208
|
+
parts.push({ type: "video_url", video_url: { url } });
|
|
209
|
+
hasMedia = true;
|
|
210
|
+
} else if (p.text != null) {
|
|
211
|
+
parts.push({ type: "text", text: String(p.text) });
|
|
212
|
+
} else {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`unsupported Responses content part type "${String(p.type)}" — ` +
|
|
215
|
+
`use input_text, input_image, or input_video`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!hasMedia)
|
|
220
|
+
return parts.map((p) => String((p as { text?: unknown }).text ?? "")).join("");
|
|
221
|
+
return parts;
|
|
222
|
+
}
|
|
223
|
+
|
|
170
224
|
/** Convert a prior response's output array into input-shaped items for
|
|
171
225
|
* previous_response_id resumption (reasoning items dropped — replaying
|
|
172
226
|
* them invites the model to repeat itself). */
|
|
@@ -232,11 +286,14 @@ export function responsesToChatBody(body: ResponsesRequest): Record<string, unkn
|
|
|
232
286
|
}
|
|
233
287
|
// regular message item (explicit type or implicit by having role)
|
|
234
288
|
let role = String(item.role ?? "user");
|
|
235
|
-
const content = coerceText(item.content);
|
|
236
289
|
if (role === "system" || role === "developer") {
|
|
237
|
-
|
|
290
|
+
// System prompts are text by contract — flatten (media in a system
|
|
291
|
+
// item has no defined semantics on any surface we mirror).
|
|
292
|
+
const text = coerceText(item.content);
|
|
293
|
+
if (text) systemParts.push(text);
|
|
238
294
|
continue;
|
|
239
295
|
}
|
|
296
|
+
const content = coerceContent(item.content);
|
|
240
297
|
if (!["user", "assistant", "tool"].includes(role)) role = "user";
|
|
241
298
|
messages.push({ role, content });
|
|
242
299
|
}
|
|
@@ -722,6 +779,49 @@ export class ResponsesStreamTranslator {
|
|
|
722
779
|
}
|
|
723
780
|
}
|
|
724
781
|
|
|
782
|
+
/** Direct semantic-event adapter used by the server. `onComplete` receives
|
|
783
|
+
* the same response.completed payload stored for previous_response_id. */
|
|
784
|
+
export function createResponsesStreamProtocol(
|
|
785
|
+
model: string,
|
|
786
|
+
previousResponseId: string | null,
|
|
787
|
+
onComplete: (final: Record<string, unknown>) => void,
|
|
788
|
+
): CompletionStreamProtocol {
|
|
789
|
+
const translator = new ResponsesStreamTranslator(model, previousResponseId);
|
|
790
|
+
const chunk = (delta: Record<string, unknown>) => ({ choices: [{ delta }] });
|
|
791
|
+
return {
|
|
792
|
+
start: () => translator.addChunk(chunk({ role: "assistant", content: "" })),
|
|
793
|
+
addEvents(events: CompletionEvent[]) {
|
|
794
|
+
return events.flatMap((event) => {
|
|
795
|
+
if (event.type === "reasoning") {
|
|
796
|
+
return translator.addChunk(chunk({ reasoning: event.text }));
|
|
797
|
+
}
|
|
798
|
+
if (event.type === "content") {
|
|
799
|
+
return translator.addChunk(chunk({ content: event.text }));
|
|
800
|
+
}
|
|
801
|
+
return translator.addChunk(chunk({
|
|
802
|
+
tool_calls: event.calls.map((call, index) => ({ index, ...call })),
|
|
803
|
+
}));
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
finish(reason, usage) {
|
|
807
|
+
const frames = [
|
|
808
|
+
...translator.addChunk({ choices: [{ delta: {}, finish_reason: reason }], usage }),
|
|
809
|
+
...translator.finalize(),
|
|
810
|
+
];
|
|
811
|
+
onComplete(translator.finalResponse());
|
|
812
|
+
return frames;
|
|
813
|
+
},
|
|
814
|
+
error(message) {
|
|
815
|
+
return [sse("error", {
|
|
816
|
+
type: "error",
|
|
817
|
+
code: "server_error",
|
|
818
|
+
message,
|
|
819
|
+
param: null,
|
|
820
|
+
})];
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
725
825
|
/** Wrap our OpenAI SSE byte stream as a Responses SSE byte stream.
|
|
726
826
|
* onComplete fires after response.completed with the final payload so
|
|
727
827
|
* the route can store it for previous_response_id resumption. */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type RuntimeKey = `MLX_BUN_${string}`;
|
|
2
|
+
export type RuntimeOverrides = Readonly<Partial<Record<RuntimeKey, string | undefined>>>;
|
|
3
|
+
|
|
4
|
+
export interface RuntimeConfig {
|
|
5
|
+
readonly values: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
value(name: RuntimeKey): string | undefined;
|
|
7
|
+
flag(name: RuntimeKey, defaultOn: boolean): boolean;
|
|
8
|
+
number(name: RuntimeKey, fallback: number): number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Build one immutable runtime snapshot. Only mlx-bun-owned keys cross this
|
|
12
|
+
* boundary; feature code never reads or mutates process.env. */
|
|
13
|
+
export function createRuntimeConfig(
|
|
14
|
+
source: Readonly<Record<string, string | undefined>>,
|
|
15
|
+
): RuntimeConfig {
|
|
16
|
+
const values = Object.freeze(Object.fromEntries(
|
|
17
|
+
Object.entries(source).filter(([key, value]) =>
|
|
18
|
+
key.startsWith("MLX_BUN_") && value !== undefined),
|
|
19
|
+
));
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
values,
|
|
22
|
+
value: (name: RuntimeKey) => values[name],
|
|
23
|
+
flag: (name: RuntimeKey, defaultOn: boolean) => {
|
|
24
|
+
const value = values[name];
|
|
25
|
+
return value === "1" ? true : value === "0" ? false : defaultOn;
|
|
26
|
+
},
|
|
27
|
+
number: (name: RuntimeKey, fallback: number) => {
|
|
28
|
+
const value = values[name];
|
|
29
|
+
if (value === undefined) return fallback;
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let active = createRuntimeConfig(process.env);
|
|
37
|
+
|
|
38
|
+
export function runtimeConfig(): RuntimeConfig {
|
|
39
|
+
return active;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runtimeValue(name: RuntimeKey): string | undefined {
|
|
43
|
+
return active.value(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runtimeFlag(name: RuntimeKey, defaultOn: boolean): boolean {
|
|
47
|
+
return active.flag(name, defaultOn);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function runtimeNumber(name: RuntimeKey, fallback: number): number {
|
|
51
|
+
return active.number(name, fallback);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runtimeKey(name: string): RuntimeKey {
|
|
55
|
+
if (!name.startsWith("MLX_BUN_"))
|
|
56
|
+
throw new Error(`runtime flag must start with MLX_BUN_ (got ${name})`);
|
|
57
|
+
return name as RuntimeKey;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Replace selected values by installing a new frozen snapshot. The CLI uses
|
|
61
|
+
* this once while resolving flags; tests may use the returned restore hook.
|
|
62
|
+
* No environment mutation is involved. */
|
|
63
|
+
export function configureRuntime(overrides: RuntimeOverrides): () => void {
|
|
64
|
+
const previous = active;
|
|
65
|
+
active = createRuntimeConfig({ ...previous.values, ...overrides });
|
|
66
|
+
return () => { active = previous; };
|
|
67
|
+
}
|