mlx-bun 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-v2-plan.md +515 -0
  3. package/docs/design/bucketing-stage.md +196 -0
  4. package/docs/design/compat-cli-surface-design.md +11 -11
  5. package/docs/design/diffusion-gemma-port.md +602 -0
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/orpo-dynamic-lambda.md +41 -0
  15. package/docs/design/orpo-training.md +35 -5
  16. package/docs/design/parallel-slots.md +12 -1
  17. package/docs/design/parity-tier-dag.md +13 -5
  18. package/docs/design/spec-decode-larger-targets.md +229 -0
  19. package/docs/design/tauri-desktop-app.md +2 -2
  20. package/docs/design/the-dreaming-handoff.md +141 -0
  21. package/docs/design/the-dreaming-master-plan.md +446 -0
  22. package/docs/design/write-pipeline-entity.md +468 -0
  23. package/docs/investigations/curve-bisector-routing.md +1 -1
  24. package/docs/investigations/decode-roofline-lookagain.md +262 -0
  25. package/docs/investigations/dspark-handoff.md +58 -0
  26. package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
  27. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  28. package/docs/planning/curve-sampler-research-plan.md +331 -0
  29. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  30. package/docs/planning/pi-integration-review.md +178 -0
  31. package/docs/planning/release-notes-v0.0.9.md +122 -0
  32. package/docs/planning/web-ui-pass-plan.md +213 -0
  33. package/docs/planning/website-readme-pass-plan.md +224 -0
  34. package/docs/reference/cli.md +255 -0
  35. package/docs/reference/embedding.md +7 -2
  36. package/docs/reference/library-api.md +22 -3
  37. package/docs/reference/memory.md +5 -5
  38. package/docs/reference/models.md +137 -0
  39. package/docs/reference/orpo-quickstart.md +6 -0
  40. package/docs/reference/server-api.md +187 -9
  41. package/docs/reference/server-config.md +40 -9
  42. package/docs/reference/training.md +25 -11
  43. package/package.json +1 -1
  44. package/src/anthropic.ts +22 -0
  45. package/src/assets/curve-designer.html +1 -1
  46. package/src/chat-template.ts +25 -7
  47. package/src/cli.ts +1029 -86
  48. package/src/config.ts +54 -3
  49. package/src/diffusion/diffusion-generate.ts +436 -0
  50. package/src/download.ts +99 -8
  51. package/src/embed.ts +77 -0
  52. package/src/eval/perplexity.ts +164 -0
  53. package/src/eval/runner.ts +19 -7
  54. package/src/eval/tasks/bfcl.ts +1 -1
  55. package/src/generate.ts +196 -19
  56. package/src/harness-pi.ts +43 -17
  57. package/src/index.ts +2 -0
  58. package/src/lora.ts +2 -2
  59. package/src/memory/article.ts +558 -0
  60. package/src/memory/chunk-validation.ts +150 -0
  61. package/src/memory/chunk.ts +305 -12
  62. package/src/memory/cluster.ts +344 -17
  63. package/src/memory/coverage.ts +134 -0
  64. package/src/memory/crosslink.ts +0 -0
  65. package/src/memory/db.ts +184 -45
  66. package/src/memory/entity.ts +203 -0
  67. package/src/memory/gate.ts +187 -0
  68. package/src/memory/history.ts +152 -0
  69. package/src/memory/ingest.ts +324 -11
  70. package/src/memory/model.ts +321 -0
  71. package/src/memory/normalize.ts +671 -0
  72. package/src/memory/parse.ts +22 -0
  73. package/src/memory/pipeline.ts +242 -30
  74. package/src/memory/prompts.ts +42 -0
  75. package/src/memory/query.ts +334 -0
  76. package/src/memory/reconcile.ts +734 -0
  77. package/src/memory/redirect.ts +108 -0
  78. package/src/memory/reindex.ts +325 -0
  79. package/src/memory/resolve.ts +307 -0
  80. package/src/memory/route.ts +272 -0
  81. package/src/memory/schedule.ts +6 -4
  82. package/src/memory/stages.ts +774 -0
  83. package/src/memory/synthesize.ts +1629 -20
  84. package/src/memory/tools.ts +446 -96
  85. package/src/memory/vault.ts +462 -35
  86. package/src/memory/wikify.ts +1057 -16
  87. package/src/mlx/ffi.ts +11 -0
  88. package/src/mlx/ops.ts +113 -0
  89. package/src/model/compiled-decode.ts +43 -8
  90. package/src/model/diffusion-gemma.ts +1026 -0
  91. package/src/model/factory.ts +35 -8
  92. package/src/model/flash-attention.ts +4 -2
  93. package/src/model/fused-decode-kernel.ts +14 -7
  94. package/src/model/fused-mlp-kernel.ts +169 -0
  95. package/src/model/fused-swiglu-kernel.ts +240 -0
  96. package/src/model/gemma4-base.ts +62 -6
  97. package/src/model/gemma4.ts +33 -2
  98. package/src/model/minicpm5.ts +106 -8
  99. package/src/model/qwen3.ts +276 -0
  100. package/src/model/steel-linear-kernel.ts +96 -0
  101. package/src/model/support.ts +45 -7
  102. package/src/model/universal/archs.ts +332 -0
  103. package/src/model/universal/dense.ts +576 -0
  104. package/src/model/universal/modules.ts +163 -0
  105. package/src/model/universal/rope.ts +366 -0
  106. package/src/pi-provider.ts +1 -1
  107. package/src/pi-terminal.ts +29 -8
  108. package/src/pi-web.ts +19 -2
  109. package/src/quantize/config-writer.ts +1 -1
  110. package/src/registry.ts +215 -7
  111. package/src/responses.ts +23 -0
  112. package/src/sampler.ts +220 -9
  113. package/src/serve/batch-scheduler.ts +272 -80
  114. package/src/serve/generation-gateway.ts +90 -18
  115. package/src/server.ts +643 -50
  116. package/src/spec/drafter.ts +81 -49
  117. package/src/spec/dspark/data-dflash.ts +156 -0
  118. package/src/spec/dspark/data.ts +201 -0
  119. package/src/spec/dspark/generate-dflash.ts +193 -0
  120. package/src/spec/dspark/generate.ts +290 -0
  121. package/src/spec/dspark/loss.ts +213 -0
  122. package/src/spec/dspark/module-dflash.ts +361 -0
  123. package/src/spec/dspark/module.ts +540 -0
  124. package/src/spec/dspark/sample.ts +112 -0
  125. package/src/spec/generate.ts +40 -19
  126. package/src/status-page.html +1 -1
  127. package/src/tokenizer.ts +7 -0
  128. package/src/train/diffusion-lora.ts +216 -0
  129. package/src/train/flash-cce.ts +70 -15
  130. package/src/train/fuse.ts +243 -0
  131. package/src/train/index.ts +3 -0
  132. package/src/train/job.ts +8 -0
  133. package/src/train/loss.ts +289 -30
  134. package/src/train/prefix-shared.ts +33 -5
  135. package/src/train/segmented.ts +67 -11
  136. package/src/train/trainer.ts +92 -28
  137. package/src/vision/diffusion-vision.ts +620 -0
  138. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,163 @@
1
+ // Universal-tier primitives: dense (unquantized) linear/embedding modules
2
+ // (Phase 1.5 — mlx nn.Linear / nn.Embedding semantics), quantized-or-dense
3
+ // loader helpers, norm loaders, and the load-time weight audit.
4
+ //
5
+ // The quantized paths delegate to the proven gemma4-base primitives; the
6
+ // dense paths are verbatim ports of mlx.nn.Linear (`mx.addmm(bias, x, W.T)`
7
+ // / `x @ W.T`) and mlx.nn.Embedding (`weight[ids]`, `x @ weight.T`).
8
+
9
+ import type { ModelConfig } from "../../config";
10
+ import type { Weights } from "../../weights";
11
+ import { MlxArray } from "../../mlx/array";
12
+ import * as ops from "../../mlx/ops";
13
+ import { QuantizedEmbedding, QuantizedLinear, RMSNorm } from "../gemma4-base";
14
+
15
+ /** Records every tensor a universal load consumes; `finish` diffs against
16
+ * the shard index so a descriptor mistake is a LOAD error (unconsumed /
17
+ * missing tensors named), never a silently-wrong model
18
+ * (docs/design/generic-model-support.md §3.4). */
19
+ export class WeightAudit {
20
+ readonly consumed = new Set<string>();
21
+
22
+ use(name: string): void {
23
+ this.consumed.add(name);
24
+ }
25
+
26
+ /** `drop` = the arch's sanitize rules (tensors mlx-lm discards on load,
27
+ * e.g. rotary_emb.inv_freq, or lm_head.weight under tied embeddings). */
28
+ finish(weights: Weights, drop: RegExp[]): void {
29
+ const unconsumed = weights.tensorNames.filter(
30
+ (n) => !this.consumed.has(n) && !drop.some((re) => re.test(n)),
31
+ );
32
+ if (unconsumed.length > 0)
33
+ throw new Error(
34
+ `weight audit: ${unconsumed.length} tensor(s) in the checkpoint were not consumed ` +
35
+ `by the universal module (descriptor mismatch?): ${unconsumed.slice(0, 12).join(", ")}` +
36
+ (unconsumed.length > 12 ? ", …" : ""),
37
+ );
38
+ }
39
+ }
40
+
41
+ /** Load a tensor and record it in the audit. */
42
+ function tensorUsed(weights: Weights, audit: WeightAudit, name: string): MlxArray {
43
+ audit.use(name);
44
+ return weights.tensor(name);
45
+ }
46
+
47
+ /** Plain unquantized linear — mlx nn.Linear:
48
+ * `mx.addmm(bias, x, weight.T)` when bias is present, else `x @ weight.T`. */
49
+ export class DenseLinear {
50
+ readonly wT: MlxArray;
51
+
52
+ constructor(readonly w: MlxArray, readonly bias: MlxArray | null) {
53
+ this.wT = ops.transposeAxes(w, [1, 0]); // lazy view, shared across calls
54
+ }
55
+
56
+ get inFeatures(): number {
57
+ return this.w.shape[1]!;
58
+ }
59
+ get outFeatures(): number {
60
+ return this.w.shape[0]!;
61
+ }
62
+
63
+ forward(x: MlxArray): MlxArray {
64
+ return this.bias ? ops.addmm(this.bias, x, this.wT) : ops.matmul(x, this.wT);
65
+ }
66
+ }
67
+
68
+ /** Either linear flavor behind one call surface. */
69
+ export type AnyLinear = QuantizedLinear | DenseLinear;
70
+
71
+ /** Quantized when `.scales` exists (MLX-quantized checkpoints), else dense
72
+ * bf16/f16/f32 (Phase 1.5). Both flavors carry the optional ADDITIVE
73
+ * `.bias` term (qwen2 qkv, starcoder2, …). */
74
+ export function loadLinear(
75
+ weights: Weights, path: string, config: ModelConfig, audit: WeightAudit,
76
+ ): AnyLinear {
77
+ if (weights.has(`${path}.scales`)) {
78
+ const lin = QuantizedLinear.load(weights, path, config);
79
+ audit.use(`${path}.weight`);
80
+ audit.use(`${path}.scales`);
81
+ if (weights.has(`${path}.biases`)) audit.use(`${path}.biases`);
82
+ if (weights.has(`${path}.bias`)) audit.use(`${path}.bias`);
83
+ return lin;
84
+ }
85
+ if (!weights.has(`${path}.weight`))
86
+ throw new Error(`${path}: no .weight tensor (nor .scales) in checkpoint`);
87
+ return new DenseLinear(
88
+ tensorUsed(weights, audit, `${path}.weight`),
89
+ weights.has(`${path}.bias`) ? tensorUsed(weights, audit, `${path}.bias`) : null,
90
+ );
91
+ }
92
+
93
+ /** Plain unquantized embedding — mlx nn.Embedding (+ as_linear tied head). */
94
+ export class DenseEmbedding {
95
+ readonly wT: MlxArray;
96
+
97
+ constructor(readonly w: MlxArray) {
98
+ this.wT = ops.transposeAxes(w, [1, 0]);
99
+ }
100
+
101
+ encode(ids: MlxArray): MlxArray {
102
+ return ops.takeAxis(this.w, ids, 0);
103
+ }
104
+
105
+ /** nn.Embedding.as_linear: `x @ weight.T`. */
106
+ asLinear(h: MlxArray): MlxArray {
107
+ return ops.matmul(h, this.wT);
108
+ }
109
+ }
110
+
111
+ export type AnyEmbedding = QuantizedEmbedding | DenseEmbedding;
112
+
113
+ export function loadEmbedding(
114
+ weights: Weights, path: string, config: ModelConfig, audit: WeightAudit,
115
+ ): AnyEmbedding {
116
+ if (weights.has(`${path}.scales`)) {
117
+ const emb = QuantizedEmbedding.load(weights, path, config);
118
+ audit.use(`${path}.weight`);
119
+ audit.use(`${path}.scales`);
120
+ if (weights.has(`${path}.biases`)) audit.use(`${path}.biases`);
121
+ return emb;
122
+ }
123
+ return new DenseEmbedding(tensorUsed(weights, audit, `${path}.weight`));
124
+ }
125
+
126
+ /** RMSNorm loader; `plusOne` = the gemma-family `1.0 + weight` variant
127
+ * (the add is folded once at load — identical value to mlx-lm's
128
+ * per-call `1.0 + self.weight`). */
129
+ export function loadRmsNorm(
130
+ weights: Weights, path: string, eps: number, plusOne: boolean, audit: WeightAudit,
131
+ ): RMSNorm {
132
+ const w = tensorUsed(weights, audit, `${path}.weight`);
133
+ if (!plusOne) return new RMSNorm(w, eps);
134
+ const one = ops.scalarLike(1, w);
135
+ const w1 = ops.add(one, w);
136
+ one.dispose();
137
+ return new RMSNorm(w1, eps);
138
+ }
139
+
140
+ /** mlx nn.LayerNorm (weight + bias affine — starcoder2's norm). */
141
+ export class LayerNorm {
142
+ constructor(
143
+ readonly weight: MlxArray,
144
+ readonly bias: MlxArray | null,
145
+ readonly eps: number,
146
+ ) {}
147
+
148
+ forward(x: MlxArray): MlxArray {
149
+ return ops.layerNorm(x, this.weight, this.bias, this.eps);
150
+ }
151
+ }
152
+
153
+ export type AnyNorm = RMSNorm | LayerNorm;
154
+
155
+ export function loadLayerNorm(
156
+ weights: Weights, path: string, eps: number, audit: WeightAudit,
157
+ ): LayerNorm {
158
+ return new LayerNorm(
159
+ tensorUsed(weights, audit, `${path}.weight`),
160
+ weights.has(`${path}.bias`) ? tensorUsed(weights, audit, `${path}.bias`) : null,
161
+ eps,
162
+ );
163
+ }
@@ -0,0 +1,366 @@
1
+ // Universal rope factory — faithful port of mlx_lm/models/rope_utils.py
2
+ // (oracle venv, mlx-lm 0.31.3): initialize_rope with the default / linear /
3
+ // llama3 (Llama3RoPE) / yarn (YarnRoPE) / longrope (SuScaledRoPE) /
4
+ // proportional (ProportionalRoPE) variants.
5
+ //
6
+ // Porting discipline (three-level fidelity tree, L1): every frequency
7
+ // table is computed with the SAME mlx float32 ops the oracle uses at
8
+ // module init — not host-side float64 math — so the freqs bytes match the
9
+ // oracle's bit-for-bit. Host math (Math.log/floor/ceil) is used only where
10
+ // the oracle uses python-float math (yarn correction range, mscale, su
11
+ // scale), which is IEEE double on both sides.
12
+ //
13
+ // The existing runtime paths (ops.rope default, gemma4's proportional
14
+ // freqs) are untouched; this factory serves the universal (Tier-0) models.
15
+
16
+ import { MlxArray } from "../../mlx/array";
17
+ import { Dtype } from "../../mlx/ffi";
18
+ import * as ops from "../../mlx/ops";
19
+
20
+ /** Raw HF `rope_scaling` dict (values read exactly like the oracle). */
21
+ export type RopeScalingConfig = Record<string, unknown>;
22
+
23
+ const num = (v: unknown, fallback: number): number =>
24
+ typeof v === "number" ? v : fallback;
25
+
26
+ const requireNum = (sc: RopeScalingConfig, key: string): number => {
27
+ const v = sc[key];
28
+ if (typeof v !== "number") throw new Error(`rope_scaling.${key} missing or not a number`);
29
+ return v;
30
+ };
31
+
32
+ /** One rope instance (shared across layers — identical params ⇒ identical
33
+ * values). `apply` returns a NEW array; the caller owns disposal of both. */
34
+ export class UniversalRope {
35
+ constructor(
36
+ readonly dims: number,
37
+ readonly traditional: boolean,
38
+ /** Base for native fast_rope; null when `freqs` supplies the table. */
39
+ readonly base: number | null,
40
+ /** fast_rope scale (1/factor for "linear", else 1). */
41
+ readonly scale: number,
42
+ /** Precomputed frequency table (llama3/yarn/longrope/proportional). */
43
+ readonly freqs: MlxArray | null,
44
+ /** Pre-rope input scale on x[..., :dims] (yarn mscale / su scale). */
45
+ readonly preScale: number,
46
+ /** Apply preScale even when it is exactly 1.0 (SuScaledRoPE does; Yarn
47
+ * skips when mscale == 1.0). */
48
+ readonly preScaleAlways: boolean,
49
+ ) {}
50
+
51
+ apply(x: MlxArray, offset: number): MlxArray {
52
+ let input = x;
53
+ let scaled: MlxArray | null = null;
54
+ if (this.preScaleAlways || this.preScale !== 1.0) {
55
+ // x[..., :dims] = preScale * x[..., :dims] (weak-scalar mul)
56
+ const D = x.shape[x.shape.length - 1]!;
57
+ if (this.dims === D) {
58
+ scaled = ops.mulScalar(x, this.preScale);
59
+ } else {
60
+ const start = x.shape.map(() => 0);
61
+ const stop = [...x.shape];
62
+ stop[stop.length - 1] = this.dims;
63
+ const head = x.slice(start, stop);
64
+ const headScaled = ops.mulScalar(head, this.preScale);
65
+ head.dispose();
66
+ scaled = ops.sliceUpdate(x, headScaled, start, stop);
67
+ headScaled.dispose();
68
+ }
69
+ input = scaled;
70
+ }
71
+ const out = ops.ropeScaled(
72
+ input, this.dims, this.traditional, this.base, this.scale, offset, this.freqs,
73
+ );
74
+ scaled?.dispose();
75
+ return out;
76
+ }
77
+
78
+ dispose(): void {
79
+ this.freqs?.dispose();
80
+ }
81
+ }
82
+
83
+ /** SmolLM3 NoPE: identity stand-in for disabled-rope layers. */
84
+ export class NoRope extends UniversalRope {
85
+ constructor() {
86
+ super(0, false, null, 1, null, 1, false);
87
+ }
88
+ override apply(x: MlxArray, _offset: number): MlxArray {
89
+ // Return an owned copy-view so callers can dispose uniformly.
90
+ return x.slice(x.shape.map(() => 0), [...x.shape]);
91
+ }
92
+ }
93
+
94
+ /** `base ** (arange(0, dims, 2) / dims)` with the oracle's dtypes:
95
+ * int32 arange for llama3 (no explicit dtype in the source), float32
96
+ * arange for yarn/longrope. Integer÷integer divides to float32 in mlx,
97
+ * so both spellings compute the pow in f32. */
98
+ function baseFreqs(dims: number, base: number, arangeDtype: Dtype): MlxArray {
99
+ const ar = ops.arange(0, dims, 2, arangeDtype);
100
+ const dimsScalar = arangeDtype === Dtype.int32
101
+ ? ops.fromInt32([dims], [])
102
+ : MlxArray.fromFloat32(new Float32Array([dims]), []);
103
+ const exps = ops.div(ar, dimsScalar); // → float32
104
+ ar.dispose();
105
+ dimsScalar.dispose();
106
+ const baseScalar = ops.scalarLike(base, exps);
107
+ const out = ops.pow(baseScalar, exps);
108
+ baseScalar.dispose();
109
+ exps.dispose();
110
+ return out; // float32 [dims/2]
111
+ }
112
+
113
+ /** Llama3RoPE frequency table (rope_utils.py:74-108, verbatim). */
114
+ function llama3Freqs(dims: number, base: number, sc: RopeScalingConfig): MlxArray {
115
+ const factor = requireNum(sc, "factor");
116
+ const lowFreqFactor = num(sc.low_freq_factor, 1.0);
117
+ const highFreqFactor = num(sc.high_freq_factor, 4.0);
118
+ const oldContextLen = num(sc.original_max_position_embeddings, 8192);
119
+
120
+ const lowFreqWavelen = oldContextLen / lowFreqFactor;
121
+ const highFreqWavelen = oldContextLen / highFreqFactor;
122
+
123
+ let freqs = baseFreqs(dims, base, Dtype.int32);
124
+ const wavelens = ops.mulScalar(freqs, 2 * Math.PI);
125
+
126
+ // freqs = where(wavelens > low_freq_wavelen, freqs * factor, freqs)
127
+ const lfw = ops.scalarLike(lowFreqWavelen, wavelens);
128
+ const gtLow = ops.less(lfw, wavelens);
129
+ const freqsTimesFactor = ops.mulScalar(freqs, factor);
130
+ const freqs1 = ops.where(gtLow, freqsTimesFactor, freqs);
131
+ for (const a of [gtLow, freqsTimesFactor, freqs]) a.dispose();
132
+ freqs = freqs1;
133
+
134
+ // is_medium = (wavelens > high_freq_wavelen) & (wavelens < low_freq_wavelen)
135
+ const hfw = ops.scalarLike(highFreqWavelen, wavelens);
136
+ const gtHigh = ops.less(hfw, wavelens);
137
+ const ltLow = ops.less(wavelens, lfw);
138
+ const isMedium = ops.logicalAnd(gtHigh, ltLow);
139
+ for (const a of [hfw, lfw, gtHigh, ltLow]) a.dispose();
140
+
141
+ // smooth_factors = (old_context_len / wavelens - low) / (high - low)
142
+ const oldScalar = ops.scalarLike(oldContextLen, wavelens);
143
+ const oldOverWav = ops.div(oldScalar, wavelens);
144
+ oldScalar.dispose();
145
+ wavelens.dispose();
146
+ const lowScalar = ops.scalarLike(lowFreqFactor, oldOverWav);
147
+ const numArr = ops.sub(oldOverWav, lowScalar);
148
+ oldOverWav.dispose();
149
+ lowScalar.dispose();
150
+ const denScalar = ops.scalarLike(highFreqFactor - lowFreqFactor, numArr);
151
+ const smooth = ops.div(numArr, denScalar);
152
+ numArr.dispose();
153
+ denScalar.dispose();
154
+
155
+ // smooth_freqs = freqs / ((1 - smooth) / factor + smooth)
156
+ const one = ops.scalarLike(1, smooth);
157
+ const oneMinus = ops.sub(one, smooth);
158
+ one.dispose();
159
+ const factorScalar = ops.scalarLike(factor, oneMinus);
160
+ const scaledDown = ops.div(oneMinus, factorScalar);
161
+ oneMinus.dispose();
162
+ factorScalar.dispose();
163
+ const denom = ops.add(scaledDown, smooth);
164
+ scaledDown.dispose();
165
+ smooth.dispose();
166
+ const smoothFreqs = ops.div(freqs, denom);
167
+ denom.dispose();
168
+
169
+ const out = ops.where(isMedium, smoothFreqs, freqs);
170
+ for (const a of [isMedium, smoothFreqs, freqs]) a.dispose();
171
+ return out;
172
+ }
173
+
174
+ /** YarnRoPE (rope_utils.py:128-181, verbatim): returns [freqs, mscale]. */
175
+ function yarnFreqs(
176
+ dims: number,
177
+ base: number,
178
+ scalingFactor: number,
179
+ originalMaxPositionEmbeddings: number,
180
+ betaFast: number,
181
+ betaSlow: number,
182
+ mscale: number,
183
+ mscaleAllDim: number,
184
+ ): [MlxArray, number] {
185
+ const findCorrectionDim = (numRotations: number): number =>
186
+ (dims * Math.log(originalMaxPositionEmbeddings / (numRotations * 2 * Math.PI))) /
187
+ (2 * Math.log(base));
188
+ let low = Math.floor(findCorrectionDim(betaFast));
189
+ let high = Math.ceil(findCorrectionDim(betaSlow));
190
+ low = Math.max(low, 0);
191
+ high = Math.min(high, dims - 1);
192
+ const getMscale = (scale: number, m: number): number =>
193
+ scale <= 1 ? 1.0 : 0.1 * m * Math.log(scale) + 1.0;
194
+ const outMscale =
195
+ getMscale(scalingFactor, mscale) / getMscale(scalingFactor, mscaleAllDim);
196
+
197
+ const freqExtra = baseFreqs(dims, base, Dtype.float32);
198
+ const freqInter = ops.mulScalar(freqExtra, scalingFactor);
199
+
200
+ // freq_mask = 1 - linear_ramp_mask(low, high, dims // 2)
201
+ let maxVal = high;
202
+ if (low === maxVal) maxVal += 0.001; // prevent singularity (oracle comment)
203
+ const arangeHalf = ops.arange(0, Math.floor(dims / 2), 1, Dtype.float32);
204
+ const minScalar = ops.scalarLike(low, arangeHalf);
205
+ const shifted = ops.sub(arangeHalf, minScalar);
206
+ arangeHalf.dispose();
207
+ minScalar.dispose();
208
+ const rangeScalar = ops.scalarLike(maxVal - low, shifted);
209
+ const linear = ops.div(shifted, rangeScalar);
210
+ shifted.dispose();
211
+ rangeScalar.dispose();
212
+ const lo = ops.scalarLike(0, linear);
213
+ const hi = ops.scalarLike(1, linear);
214
+ const ramp = ops.clip(linear, lo, hi);
215
+ for (const a of [linear, lo, hi]) a.dispose();
216
+ const one = ops.scalarLike(1, ramp);
217
+ const freqMask = ops.sub(one, ramp);
218
+ ramp.dispose();
219
+
220
+ // freqs = (inter * extra) / (inter * mask + extra * (1 - mask))
221
+ const numArr = ops.mul(freqInter, freqExtra);
222
+ const interMasked = ops.mul(freqInter, freqMask);
223
+ const oneMinusMask = ops.sub(one, freqMask);
224
+ one.dispose();
225
+ const extraMasked = ops.mul(freqExtra, oneMinusMask);
226
+ oneMinusMask.dispose();
227
+ const den = ops.add(interMasked, extraMasked);
228
+ const freqs = ops.div(numArr, den);
229
+ for (const a of [freqExtra, freqInter, freqMask, numArr, interMasked, extraMasked, den])
230
+ a.dispose();
231
+ return [freqs, outMscale];
232
+ }
233
+
234
+ /** SuScaledRoPE (rope_utils.py:10-71): [freqs, scale]. NOTE the oracle uses
235
+ * long_factor for ALL positions (short_factor is accepted and ignored). */
236
+ function suFreqsAndScale(
237
+ dims: number,
238
+ base: number,
239
+ maxPositionEmbeddings: number,
240
+ originalMaxPositionEmbeddings: number,
241
+ longFactor: number[] | number,
242
+ longMscale?: number,
243
+ ): [MlxArray, number] {
244
+ const freqs0 = baseFreqs(dims, base, Dtype.float32);
245
+ const lf = Array.isArray(longFactor)
246
+ ? MlxArray.fromFloat32(new Float32Array(longFactor), [longFactor.length])
247
+ : MlxArray.fromFloat32(new Float32Array([longFactor]), []);
248
+ const freqs = ops.mul(lf, freqs0);
249
+ lf.dispose();
250
+ freqs0.dispose();
251
+
252
+ const factor = maxPositionEmbeddings / originalMaxPositionEmbeddings;
253
+ const defaultScale =
254
+ factor <= 1.0
255
+ ? 1.0
256
+ : Math.sqrt(1 + Math.log(factor) / Math.log(originalMaxPositionEmbeddings));
257
+ // python: `long_mscale or default` — 0 is falsy there too.
258
+ const scale = longMscale ? longMscale : defaultScale;
259
+ return [freqs, scale];
260
+ }
261
+
262
+ /** ProportionalRoPE (rope_utils.py:199-232): rotate the first rotated_dims,
263
+ * ∞-freq (identity) padding for the rest. */
264
+ function proportionalFreqs(
265
+ dims: number, rotatedDims: number, base: number, factor: number,
266
+ ): MlxArray {
267
+ if (rotatedDims > dims) throw new Error("rotated_dims should be smaller than dims");
268
+ const ar = ops.arange(0, rotatedDims, 2, Dtype.float32);
269
+ const dimsScalar = MlxArray.fromFloat32(new Float32Array([dims]), []);
270
+ const exps = ops.div(ar, dimsScalar);
271
+ ar.dispose();
272
+ dimsScalar.dispose();
273
+ const baseScalar = ops.scalarLike(base, exps);
274
+ const powed = ops.pow(baseScalar, exps);
275
+ baseScalar.dispose();
276
+ exps.dispose();
277
+ const scaled = ops.mulScalar(powed, factor);
278
+ powed.dispose();
279
+ const padLen = Math.floor((dims - rotatedDims) / 2);
280
+ if (padLen === 0) return scaled;
281
+ const pad = MlxArray.fromFloat32(
282
+ new Float32Array(padLen).fill(Number.POSITIVE_INFINITY), [padLen],
283
+ );
284
+ const out = ops.concatAxis([scaled, pad], 0);
285
+ scaled.dispose();
286
+ pad.dispose();
287
+ return out;
288
+ }
289
+
290
+ /** Port of rope_utils.initialize_rope — the shared entry every
291
+ * initialize_rope-style arch (llama, qwen2/3, olmo2, granite, …) uses. */
292
+ export function initializeRope(
293
+ dims: number,
294
+ base: number,
295
+ traditional: boolean,
296
+ scalingConfig: RopeScalingConfig | null,
297
+ maxPositionEmbeddings: number | null,
298
+ ): UniversalRope {
299
+ const ropeType = scalingConfig
300
+ ? String(scalingConfig.type ?? scalingConfig.rope_type ?? "default")
301
+ : "default";
302
+
303
+ if (ropeType === "default" || ropeType === "linear") {
304
+ const scale = ropeType === "linear" ? 1 / requireNum(scalingConfig!, "factor") : 1.0;
305
+ return new UniversalRope(dims, traditional, base, scale, null, 1, false);
306
+ }
307
+
308
+ if (ropeType === "llama3") {
309
+ const freqs = llama3Freqs(dims, base, scalingConfig!);
310
+ return new UniversalRope(dims, traditional, null, 1.0, freqs, 1, false);
311
+ }
312
+
313
+ if (ropeType === "yarn" || ropeType === "deepseek_yarn" || ropeType === "telechat3-yarn") {
314
+ const sc = scalingConfig!;
315
+ const [freqs, mscale] = yarnFreqs(
316
+ dims,
317
+ base,
318
+ requireNum(sc, "factor"),
319
+ num(sc.original_max_position_embeddings, 4096),
320
+ num(sc.beta_fast, 32),
321
+ num(sc.beta_slow, 1),
322
+ num(sc.mscale, 1),
323
+ num(sc.mscale_all_dim, 0),
324
+ );
325
+ // YarnRoPE scales x only when mscale != 1.0 (preScaleAlways=false).
326
+ return new UniversalRope(dims, traditional, null, 1.0, freqs, mscale, false);
327
+ }
328
+
329
+ if (ropeType === "longrope") {
330
+ const sc = scalingConfig!;
331
+ const [freqs, scale] = suFreqsAndScale(
332
+ dims,
333
+ base,
334
+ maxPositionEmbeddings ?? 131072,
335
+ requireNum(sc, "original_max_position_embeddings"),
336
+ (sc.long_factor as number[] | number | undefined) ?? 1.0,
337
+ );
338
+ // SuScaledRoPE hardcodes traditional=False and ALWAYS pre-scales.
339
+ return new UniversalRope(dims, false, null, 1.0, freqs, scale, true);
340
+ }
341
+
342
+ if (ropeType === "proportional") {
343
+ const sc = scalingConfig!;
344
+ const rotatedDims = Math.floor(dims * num(sc.partial_rotary_factor, 1.0));
345
+ const freqs = proportionalFreqs(dims, rotatedDims, base, num(sc.factor, 1.0));
346
+ return new UniversalRope(dims, traditional, null, 1.0, freqs, 1, false);
347
+ }
348
+
349
+ throw new Error(`Unsupported RoPE type ${ropeType}`);
350
+ }
351
+
352
+ /** SuScaledRoPE constructed the phi3.py way (NOT via initialize_rope):
353
+ * original_max_position_embeddings comes from the TOP-LEVEL config, and
354
+ * rope_dim = head_dim * partial_rotary_factor. */
355
+ export function phi3SuRope(
356
+ ropeDim: number,
357
+ base: number,
358
+ maxPositionEmbeddings: number,
359
+ originalMaxPositionEmbeddings: number,
360
+ longFactor: number[] | number,
361
+ ): UniversalRope {
362
+ const [freqs, scale] = suFreqsAndScale(
363
+ ropeDim, base, maxPositionEmbeddings, originalMaxPositionEmbeddings, longFactor,
364
+ );
365
+ return new UniversalRope(ropeDim, false, null, 1.0, freqs, scale, true);
366
+ }
@@ -57,7 +57,7 @@ export interface PiProviderWiring {
57
57
 
58
58
  /**
59
59
  * Build the in-memory auth + registry and resolve the local model for a pi
60
- * session pointed at `baseUrl` (e.g. "http://127.0.0.1:8090/v1"). The runtime
60
+ * session pointed at `baseUrl` (e.g. "http://127.0.0.1:8080/v1"). The runtime
61
61
  * key satisfies pi's auth check for our zero-cost local provider; no disk
62
62
  * (no models.json, no ~/.pi cross-talk).
63
63
  */
@@ -77,7 +77,7 @@ Be honest about what you did and what you found, including mistakes, dead ends,
77
77
  }
78
78
 
79
79
  export interface RunEmbeddedPiOptions {
80
- /** mlx-bun server base URL, e.g. "http://localhost:8090/v1". */
80
+ /** mlx-bun server base URL, e.g. "http://localhost:8080/v1". */
81
81
  baseUrl: string;
82
82
  /** Real served model id, used to label the model in the prompt + registry. */
83
83
  modelLabel?: string;
@@ -85,6 +85,9 @@ export interface RunEmbeddedPiOptions {
85
85
  contextWindow?: number;
86
86
  /** Whether the served model has a switchable thinking channel. */
87
87
  reasoning?: boolean;
88
+ /** Whether the served model accepts images (server's vision capability —
89
+ * /v1/models `vision`). MUST match the real model: see PiProviderOptions. */
90
+ vision?: boolean;
88
91
  /** Run mode. Default: "interactive". */
89
92
  mode?: PiTerminalMode;
90
93
  /** Print mode output format ("text" final-only, "json" event stream). */
@@ -115,6 +118,7 @@ export async function runEmbeddedPi(opts: RunEmbeddedPiOptions): Promise<number>
115
118
  contextWindow: opts.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
116
119
  name: opts.modelLabel ? `${opts.modelLabel} (mlx-bun local)` : undefined,
117
120
  reasoning: opts.reasoning ?? false,
121
+ vision: opts.vision ?? false,
118
122
  });
119
123
 
120
124
  // Shared pi tool/skill surface. Memory is included only when the user has
@@ -202,6 +206,11 @@ export interface ParsedPiArgs {
202
206
  printFormat: "text" | "json";
203
207
  /** Joined free-text message (the trailing words / `-p <text>`). */
204
208
  message?: string;
209
+ /** pi's verbose startup banner (interactive only). */
210
+ verbose: boolean;
211
+ /** Unknown flags that were dropped (with their values) — callers should
212
+ * warn so `--resume`-style pi flags don't fail silently. */
213
+ ignored: string[];
205
214
  }
206
215
 
207
216
  /**
@@ -222,32 +231,44 @@ export function parsePiArgs(passthrough: string[], isStdinPiped: boolean): Parse
222
231
  let print = false;
223
232
  let rpc = false;
224
233
  let json = false;
234
+ let verbose = false;
225
235
  const words: string[] = [];
236
+ const ignored: string[] = [];
226
237
  for (let i = 0; i < passthrough.length; i++) {
227
238
  const a = passthrough[i]!;
228
239
  if (a === "-p" || a === "--print") {
229
240
  print = true;
230
241
  } else if (a === "--json") {
231
242
  json = true;
243
+ } else if (a === "--verbose") {
244
+ verbose = true;
232
245
  } else if (a === "--mode") {
233
246
  const m = passthrough[++i];
234
247
  if (m === "rpc") rpc = true;
235
248
  else if (m === "json") { print = true; json = true; }
236
249
  else if (m === "text") print = true;
237
250
  } else if (a.startsWith("-")) {
238
- // Unknown flag for the built-in agent: ignore it (the full pi flag
239
- // surface lives in the user's own pi via `mlx-bun harness pi`). Don't
240
- // treat it as message text.
241
- continue;
251
+ // Unknown flag for the built-in agent: drop it AND its value (the full
252
+ // pi flag surface lives in the user's own pi via `mlx-bun harness pi`).
253
+ // Without the value-swallow, `mlx-bun pi --resume abc123` started a
254
+ // chat with the message "abc123". A following token that looks like
255
+ // another flag is not a value, so it isn't consumed.
256
+ const next = passthrough[i + 1];
257
+ if (next !== undefined && !next.startsWith("-")) {
258
+ ignored.push(`${a} ${next}`);
259
+ i++;
260
+ } else {
261
+ ignored.push(a);
262
+ }
242
263
  } else {
243
264
  words.push(a);
244
265
  }
245
266
  }
246
267
  const message = words.length > 0 ? words.join(" ") : undefined;
247
268
  const printFormat = json ? "json" : "text";
248
- if (rpc) return { mode: "rpc", printFormat: "text", message };
269
+ if (rpc) return { mode: "rpc", printFormat: "text", message, verbose, ignored };
249
270
  // `-p`/`--mode` request print; piped stdin makes a bare invocation one-shot
250
271
  // too, matching pi's own CLI (it reads stdin → switches to print mode).
251
- if (print || isStdinPiped) return { mode: "print", printFormat, message };
252
- return { mode: "interactive", printFormat, message };
272
+ if (print || isStdinPiped) return { mode: "print", printFormat, message, verbose, ignored };
273
+ return { mode: "interactive", printFormat, message, verbose, ignored };
253
274
  }
package/src/pi-web.ts CHANGED
@@ -67,6 +67,21 @@ const GATED_TOOLS = new Set(["bash", "edit", "write"]);
67
67
  */
68
68
  const WELCOME_TOOLS = ["read", "web_search"] as const;
69
69
 
70
+ /**
71
+ * The web chat session's tool allowlist: the two welcome tools, plus the
72
+ * memory/reference read tools whenever the surface has memory enabled. The
73
+ * system prompt (surface.memoryHint) and the bundled memory skill instruct
74
+ * the model to call memory_resolve/memory_read/… — pi treats `tools` as an
75
+ * allowlist, so leaving them out makes every such call fail. All additions
76
+ * are in READ_ONLY_TOOLS (no approval round-trip).
77
+ */
78
+ export function webChatToolAllowlist(memoryEnabled: boolean): string[] {
79
+ return [
80
+ ...WELCOME_TOOLS,
81
+ ...(memoryEnabled ? [...MEMORY_TOOL_NAMES, ...REFERENCE_TOOL_NAMES] : []),
82
+ ];
83
+ }
84
+
70
85
  /** Auto-deny a pending approval after this long with no browser decision. */
71
86
  const APPROVAL_TIMEOUT_MS = 120_000;
72
87
 
@@ -564,8 +579,10 @@ class PiWebSession {
564
579
  // Allowlist (not surface.tools): `read` is a pi built-in enabled by
565
580
  // name; `web_search` is the custom tool from surface.customTools. pi
566
581
  // exposes only names in this list, so web_fetch/weather stay defined
567
- // but hidden.
568
- tools: [...WELCOME_TOOLS],
582
+ // but hidden. Memory/reference tools ride along when the surface has
583
+ // memory enabled — the prompt + memory skill advertise them, so they
584
+ // must be callable (webChatToolAllowlist).
585
+ tools: webChatToolAllowlist(surface.memoryEnabled),
569
586
  customTools: surface.customTools,
570
587
  })),
571
588
  services,
@@ -109,7 +109,7 @@ export async function writeQuantizedConfig(
109
109
  }
110
110
 
111
111
  /** Copy known aux files (and any `*.model`) from src → out when present. */
112
- async function copyAuxFiles(srcDir: string, outDir: string): Promise<void> {
112
+ export async function copyAuxFiles(srcDir: string, outDir: string): Promise<void> {
113
113
  const seen = new Set<string>();
114
114
  for (const f of AUX_FILES) {
115
115
  const src = join(srcDir, f);