mlx-bun 0.0.8 → 0.0.10

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 (144) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-perf-path.md +232 -0
  3. package/docs/design/batching-v2-plan.md +515 -0
  4. package/docs/design/bucketing-stage.md +196 -0
  5. package/docs/design/compat-cli-surface-design.md +11 -11
  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/omlx-adoption-map.md +90 -0
  15. package/docs/design/orpo-dynamic-lambda.md +41 -0
  16. package/docs/design/orpo-training.md +35 -5
  17. package/docs/design/parallel-slots.md +12 -1
  18. package/docs/design/parity-tier-dag.md +13 -5
  19. package/docs/design/segmented-backward-training.md +15 -0
  20. package/docs/design/spec-decode-larger-targets.md +229 -0
  21. package/docs/design/ssd-kv-cold-tier.md +158 -0
  22. package/docs/design/tauri-desktop-app.md +2 -2
  23. package/docs/design/the-dreaming-handoff.md +141 -0
  24. package/docs/design/the-dreaming-master-plan.md +446 -0
  25. package/docs/design/write-pipeline-entity.md +468 -0
  26. package/docs/investigations/curve-bisector-routing.md +1 -1
  27. package/docs/investigations/decode-roofline-lookagain.md +348 -0
  28. package/docs/investigations/dspark-handoff.md +58 -0
  29. package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
  30. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  31. package/docs/planning/curve-sampler-research-plan.md +331 -0
  32. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  33. package/docs/planning/pi-integration-review.md +178 -0
  34. package/docs/planning/release-notes-v0.0.10.md +57 -0
  35. package/docs/planning/release-notes-v0.0.9.md +122 -0
  36. package/docs/planning/web-ui-pass-plan.md +213 -0
  37. package/docs/planning/website-readme-pass-plan.md +224 -0
  38. package/docs/reference/cli.md +255 -0
  39. package/docs/reference/embedding.md +7 -2
  40. package/docs/reference/library-api.md +22 -3
  41. package/docs/reference/memory.md +5 -5
  42. package/docs/reference/models.md +137 -0
  43. package/docs/reference/orpo-quickstart.md +6 -0
  44. package/docs/reference/server-api.md +187 -9
  45. package/docs/reference/server-config.md +43 -9
  46. package/docs/reference/training.md +31 -12
  47. package/package.json +1 -1
  48. package/src/anthropic.ts +22 -0
  49. package/src/assets/curve-designer.html +1 -1
  50. package/src/chat-template.ts +25 -7
  51. package/src/cli.ts +1077 -90
  52. package/src/config.ts +4 -1
  53. package/src/download.ts +99 -8
  54. package/src/embed.ts +77 -0
  55. package/src/eval/perplexity.ts +164 -0
  56. package/src/eval/runner.ts +15 -6
  57. package/src/eval/tasks/bfcl.ts +1 -1
  58. package/src/generate.ts +149 -18
  59. package/src/harness-pi.ts +43 -17
  60. package/src/index.ts +2 -0
  61. package/src/kv-store.ts +240 -98
  62. package/src/lora.ts +2 -2
  63. package/src/memory/article.ts +558 -0
  64. package/src/memory/chunk-validation.ts +150 -0
  65. package/src/memory/chunk.ts +305 -12
  66. package/src/memory/cluster.ts +344 -17
  67. package/src/memory/coverage.ts +134 -0
  68. package/src/memory/crosslink.ts +0 -0
  69. package/src/memory/db.ts +184 -45
  70. package/src/memory/entity.ts +203 -0
  71. package/src/memory/gate.ts +187 -0
  72. package/src/memory/history.ts +152 -0
  73. package/src/memory/ingest.ts +324 -11
  74. package/src/memory/model.ts +321 -0
  75. package/src/memory/normalize.ts +671 -0
  76. package/src/memory/parse.ts +22 -0
  77. package/src/memory/pipeline.ts +242 -30
  78. package/src/memory/prompts.ts +42 -0
  79. package/src/memory/query.ts +334 -0
  80. package/src/memory/reconcile.ts +734 -0
  81. package/src/memory/redirect.ts +108 -0
  82. package/src/memory/reindex.ts +325 -0
  83. package/src/memory/resolve.ts +307 -0
  84. package/src/memory/route.ts +272 -0
  85. package/src/memory/schedule.ts +6 -4
  86. package/src/memory/stages.ts +774 -0
  87. package/src/memory/synthesize.ts +1629 -20
  88. package/src/memory/tools.ts +446 -96
  89. package/src/memory/vault.ts +462 -35
  90. package/src/memory/wikify.ts +1057 -16
  91. package/src/mlx/array.ts +21 -0
  92. package/src/mlx/ffi.ts +3 -0
  93. package/src/mlx/ops.ts +72 -0
  94. package/src/model/compiled-decode.ts +43 -8
  95. package/src/model/factory.ts +29 -8
  96. package/src/model/flash-attention.ts +4 -2
  97. package/src/model/fused-decode-kernel.ts +14 -7
  98. package/src/model/fused-mlp-kernel.ts +169 -0
  99. package/src/model/fused-swiglu-kernel.ts +240 -0
  100. package/src/model/gemma4-base.ts +96 -7
  101. package/src/model/gemma4.ts +33 -2
  102. package/src/model/minicpm5.ts +106 -8
  103. package/src/model/qwen3-delta.ts +48 -1
  104. package/src/model/qwen3.ts +276 -0
  105. package/src/model/qwen3_5.ts +11 -2
  106. package/src/model/steel-linear-kernel.ts +96 -0
  107. package/src/model/support.ts +38 -8
  108. package/src/model/universal/archs.ts +332 -0
  109. package/src/model/universal/dense.ts +576 -0
  110. package/src/model/universal/modules.ts +163 -0
  111. package/src/model/universal/rope.ts +366 -0
  112. package/src/pi-provider.ts +1 -1
  113. package/src/pi-terminal.ts +29 -8
  114. package/src/pi-web.ts +19 -2
  115. package/src/prompt-cache.ts +55 -9
  116. package/src/quantize/config-writer.ts +1 -1
  117. package/src/registry.ts +219 -8
  118. package/src/responses.ts +23 -0
  119. package/src/sampler.ts +220 -9
  120. package/src/serve/batch-scheduler.ts +307 -83
  121. package/src/serve/generation-gateway.ts +156 -29
  122. package/src/server.ts +746 -73
  123. package/src/spec/drafter.ts +81 -49
  124. package/src/spec/dspark/data-dflash.ts +156 -0
  125. package/src/spec/dspark/data.ts +201 -0
  126. package/src/spec/dspark/generate-dflash.ts +193 -0
  127. package/src/spec/dspark/generate.ts +290 -0
  128. package/src/spec/dspark/loss.ts +213 -0
  129. package/src/spec/dspark/module-dflash.ts +361 -0
  130. package/src/spec/dspark/module.ts +540 -0
  131. package/src/spec/dspark/sample.ts +112 -0
  132. package/src/spec/generate.ts +40 -19
  133. package/src/ssd-cache.ts +213 -0
  134. package/src/status-page.html +1 -1
  135. package/src/tokenizer.ts +7 -0
  136. package/src/train/flash-cce.ts +81 -23
  137. package/src/train/fuse.ts +243 -0
  138. package/src/train/index.ts +3 -0
  139. package/src/train/job.ts +8 -0
  140. package/src/train/loss.ts +341 -40
  141. package/src/train/prefix-shared.ts +62 -11
  142. package/src/train/segmented.ts +215 -55
  143. package/src/train/trainer.ts +112 -38
  144. package/src/web/skills/memory/SKILL.md +40 -17
@@ -22,7 +22,7 @@ Training is reachable several ways — all of them drive the **same** runner
22
22
 
23
23
  | Path | How | Use when |
24
24
  |---|---|---|
25
- | **CLI** | `mlx-bun train <model> --data <dir> [options]` — foreground, full ORPO stack on by default; auto-detects e4b/Gemma + sets its env; `--save-every`, `--resume`, `--dry-run`; streams loss to the terminal. `mlx-bun help train` for all flags. Run long jobs detached: `nohup mlx-bun train … &` | **The default for CLI users** |
25
+ | **CLI** | `mlx-bun train <model> --data <dir> [options]` — foreground, full ORPO stack on by default; auto-detects e4b/Gemma + sets its env; `--save-every`, `--resume`, `--dry-run`, `--sft-scope full\|response`, `--grad-accum`, `--grad-clip`, `--seed`, `--val-size`; streams loss to the terminal. `mlx-bun help train` for all flags. Run long jobs detached: `nohup mlx-bun train … &` | **The default for CLI users** |
26
26
  | **Web UI** | `mlx-bun serve`, open `/finetune` — pick model → dataset → hyperparameters → train; watch live train/val loss; merge/export the adapter | Interactive, in the browser |
27
27
  | **HTTP API** | `POST /api/finetune/submit` (job id + SSE events); `POST /api/finetune/inspect-dataset` to probe a file; `POST /api/finetune/merge` to fold an adapter into base weights | Scripted / remote |
28
28
  | **ORPO launcher** | [`scripts/train-orpo.ts`](../../scripts/train-orpo.ts) — the same stack driven by env vars (`MODEL=`, `DATA=`, `RANK=`, `RESUME=`, …). See [orpo-quickstart](./orpo-quickstart.md) | Env-var scripting |
@@ -36,7 +36,7 @@ isolation + a single GPU lease across concurrent server requests).
36
36
  ### Quick start (HTTP API)
37
37
 
38
38
  ```bash
39
- curl -s localhost:8090/api/finetune/submit -X POST -H 'content-type: application/json' -d '{
39
+ curl -s localhost:8080/api/finetune/submit -X POST -H 'content-type: application/json' -d '{
40
40
  "model_dir": "/path/to/snapshot",
41
41
  "data_dir": "/path/to/dataset", // dir with train.jsonl (+ optional valid.jsonl)
42
42
  "adapter_path": "/path/to/output-adapter",
@@ -81,7 +81,7 @@ differ from the trainer/API defaults in the table below:
81
81
 
82
82
  The script hard-codes `method=sft`, `batch_size=1`, `steps_per_report=1`, and
83
83
  uses the default `ops.sdpa` training attention (set `MLX_BUN_TRAIN_ATTN=flash`
84
- to override — but flash crashes e4b at multi-K; see Methodology).
84
+ to override — experimental, much slower; see Methodology).
85
85
 
86
86
  ## Data formats
87
87
 
@@ -112,12 +112,19 @@ Probe a file before submitting: `POST /api/finetune/inspect-dataset` with
112
112
  and a preference term, `L = L_NLL(chosen) + λ·L_OR`, with
113
113
  `L_OR = -log σ(log_odds)` and
114
114
  `log_odds = (ℓ_w − ℓ_r) − (log1mexp(ℓ_w) − log1mexp(ℓ_r))`, where `ℓ` is the
115
- **length-normalized** (mean over response tokens) log-prob. Uses the same
115
+ **length-normalized** (mean over response tokens) log-prob. The SFT term's
116
+ scope is `sft_scope`: **`full`** (default; paper/TRL-faithful) computes
117
+ `L_NLL` as the token-mean cross-entropy over the **full prompt+response** of
118
+ the chosen sequence (only padding excluded — TRL's `chosen_nll_loss`),
119
+ from the same chosen forward (no extra forward); **`response`** uses
120
+ `L_NLL = −ℓ_w` (response-only — the pre-2026-07 mlx-bun behavior, kept for
121
+ reproducing old runs). The odds-ratio `ℓ_w/ℓ_r` stay response-only in **both**
122
+ modes (that also matches TRL). Uses the same
116
123
  `{prompt, chosen, rejected}` preference data as DPO but needs **no reference
117
124
  model** (2 forwards/step vs DPO's 4). Default LR `1e-5` (lower than DPO — the
118
125
  loss carries a full SFT NLL term a high LR destabilizes). Tune with
119
- `orpo_lambda`, `orpo_warmup_iters`, `orpo_lr_schedule`. Design + optimization
120
- roadmap: [orpo-training](../design/orpo-training.md).
126
+ `orpo_lambda`, `orpo_warmup_iters`, `orpo_lr_schedule`, `sft_scope`. Design +
127
+ optimization roadmap: [orpo-training](../design/orpo-training.md).
121
128
 
122
129
  The ORPO head + long-context machinery compose into one stack (all B=1), each
123
130
  piece independently toggled and each falling back cleanly:
@@ -125,6 +132,11 @@ piece independently toggled and each falling back cleanly:
125
132
  - **`orpo_flash_ce`** — the [M,vocab]-free flash-CCE Metal head (steel GEMM fwd
126
133
  **and** bwd; e4b backward 754 ms, peak 0.93 GB flat at M=8192). Implies the
127
134
  fused head. Falls back to the MLX fused head when the tiling isn't clean.
135
+ **Auto-dispatch by M** (2026-07-02): batches with M below `MLX_BUN_FLASH_MIN_M`
136
+ (default 1024) take the exact MLX fused head instead — it is faster at short
137
+ M (e4b M=64 1.6×, M=512 1.13×; CPM5 at every M), while flash wins memory
138
+ everywhere and time at long M on e4b (M≥2048; 8K: 10.7 vs 13.2 s with the
139
+ filter/blockMax defaults). `MLX_BUN_FLASH_MIN_M=0` always honors flash.
128
140
  - **`segment_size`** — segmented backward (gradient-checkpointed layer
129
141
  activations) for long context; the head is ~free in memory, the activations
130
142
  are the wall.
@@ -177,8 +189,9 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
177
189
  | `orpo_lr_schedule` | `constant` \| `cosine` | `cosine` | ORPO LR schedule (orpo only) |
178
190
  | `orpo_chunk_size` | int | `0` (off) | Token-chunk the response head, bounding the `[M,vocab]` logits to `[chunk,vocab]` (defaults to 512 when a fused head is on). Exact (orpo only) |
179
191
  | `orpo_fused_ce` | bool | `false` | Fused linear-CE head: one CustomVjp with an analytic softmax−onehot backward (MLX `quantizedMatmul` both ways) — no autograd through the head, no retained `[M,vocab]`. Exact; `[chunk,vocab]` transient (orpo only) |
180
- | `orpo_flash_ce` | bool | `false` | Route the fused head through the **flash-CCE Metal kernel** (verbatim MLX steel GEMM + ORPO epilogue, fwd **and** bwd): neither `[M,vocab]` nor a dequantized head touches HBM → `[M,vocab]`-free, fastest on large vocab (e4b bwd 754 ms, 0.93 GB flat @ M=8192). Implies the fused head. The Apple-CCE coeff filter / blockMax skip are opt-in via `MLX_BUN_CCE_BWD_FILTER_EPS` / `_BLOCK_EPS` (default off — exact). B=1 (orpo only) |
192
+ | `orpo_flash_ce` | bool | `false` | Route the fused head through the **flash-CCE Metal kernel** (verbatim MLX steel GEMM + ORPO epilogue, fwd **and** bwd): neither `[M,vocab]` nor a dequantized head touches HBM → `[M,vocab]`-free (e4b 0.93 GB flat @ M=8192). Implies the fused head. The Apple-CCE coeff filter + blockMax skip default ON at 1e-5 (2026-07-02; combined bwd 1.71× CPM5 / 3.16× e4b vs exact — `MLX_BUN_CCE_BWD_FILTER_EPS=0` / `_BLOCK_EPS=0` restore exact). Batches with M < `MLX_BUN_FLASH_MIN_M` (default 1024) auto-take the exact fused head (faster at short M; `=0` always honors flash). B=1 (orpo only) |
181
193
  | `orpo_prefix_shared` | bool | `false` | Shared prompt-prefix: one forward over `[prompt; chosen; rejected]` with a block-sparse mask + block-wise RoPE, so the shared prompt is encoded **once** (a big win when the prompt dominates). Falls back to two-forward per row on prompt mismatch. Composes with the flash head and the segmented backward (MiniCPM5 + e4b). B=1 (orpo only) |
194
+ | `sft_scope` | `full` \| `response` | `full` | Scope of ORPO's chosen-NLL term. `full` (paper/TRL-faithful): token-mean CE over the **full prompt+response** (only padding excluded), from the same chosen forward. `response`: `L_NLL = −ℓ_w` — reproduces pre-2026-07 runs bit-exactly. The odds-ratio `ℓ_w/ℓ_r` are response-only in both modes. Applies to every ORPO path (naive/chunked/fused/flash/prefix/segmented) (orpo only). CLI spelling: `mlx-bun train … --sft-scope full\|response` |
182
195
 
183
196
  ### Environment variables (training)
184
197
 
@@ -186,7 +199,7 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
186
199
  |---|---|---|---|
187
200
  | `MLX_BUN_FUSED_GELU` | **`0` (required for Gemma)** | on | The fused GeGLU is a **CustomKernel with no gradient (vjp)** ([`fused-geglu-kernel.ts`](../../src/model/fused-geglu-kernel.ts)); the Gemma forward uses it ([`gemma4.ts:277`](../../src/model/gemma4.ts)), so a Gemma (e4b/12B/26B) backward fails unless it's off. MiniCPM5 (Llama-arch SwiGLU) never hits it, so the `.ts` script's MiniCPM5 default doesn't need it. |
188
201
  | `MLX_BUN_PERF_KERNEL` | **`0` for training** | on | The fused quantized-decode kernel likewise has no vjp. It only fires at decode L=1 (rare in the L>1 training forward), but the e4b recipe sets `0` to be safe. |
189
- | `MLX_BUN_TRAIN_ATTN` | **leave unset** | unset → `ops.sdpa` | Default `ops.sdpa` **is** mlx's fused flash-attention kernel — the correct, working path. `flash` selects a *different* hand-rolled custom kernel that crashes e4b at multi-K; do not set it. |
202
+ | `MLX_BUN_TRAIN_ATTN` | **leave unset** | unset → `ops.sdpa` | Default `ops.sdpa` **is** mlx's fused flash-attention kernel — the correct, working path. `flash` selects a *different* hand-rolled O(L)-memory kernel: its two port bugs (dK buffer-transpose, dQ divergent causal barrier) are fixed and FD-validated (`tests/flash-attention.test.ts`, T≤256), but it is ~30× slower and the historical e4b ≥2K crash has **not been re-validated at that scale** since the fix — leave unset unless memory-bound and re-verified. |
190
203
  | `MLX_BUN_MEM_LOG` | `1` to profile | off | Print per-step peak/active/cache memory |
191
204
 
192
205
  > Important: the trainer itself ([`trainer.ts`](../../src/train/trainer.ts))
@@ -221,7 +234,7 @@ What the recipe pins (and why it differs from the bare defaults):
221
234
  | `RANK` / `SCALE` / `LR` | `16` / `20` / `1e-5` | task-tuned |
222
235
  | `ITERS` | `2` (probe) / `900` (train) | ~2 epochs over 450 examples |
223
236
  | `MLX_BUN_PERF_KERNEL` / `MLX_BUN_FUSED_GELU` | `0` / `0` | **required** — the fused kernels have no vjp (see env table) |
224
- | attention | default `ops.sdpa` | mlx's fused flash kernel; **not** `MLX_BUN_TRAIN_ATTN=flash` (that one crashes e4b) |
237
+ | attention | default `ops.sdpa` | mlx's fused flash kernel; **not** `MLX_BUN_TRAIN_ATTN=flash` (~30× slower; e4b ≥2K unvalidated since the kernel fix) |
225
238
 
226
239
  The two non-negotiables for e4b: **segmented backward** (`SEG>0`, so the
227
240
  long-context activations fit) and the **fused kernels off** (so the backward
@@ -265,9 +278,15 @@ At long `max_seq_length`, activation memory dominates. Two levers:
265
278
  ### Training attention kernel (`MLX_BUN_TRAIN_ATTN`)
266
279
  - **default `ops.sdpa`** — mlx's fused SDPA; correct (0.00% vs autograd),
267
280
  O(L²) backward memory. Use this.
268
- - **`flash`** — opt-in O(L) memory path, but the hand-rolled dK kernel is
269
- slow and **crashes e4b at multi-K (≥2K)**; do not use it for e4b LoRA
270
- training. Detail in segmented-backward-training §6.
281
+ - **`flash`** — opt-in O(L)-memory path. Its two port bugs (dK
282
+ buffer-transpose, dQ divergent causal barrier) were **fixed** and
283
+ FD-validated (`scripts/experiments/flash-fd-check.ts`; regression test
284
+ `tests/flash-attention.test.ts`, T≤256), but it remains ~30× slower than
285
+ `ops.sdpa`, and the historical e4b multi-K (≥2K) SIGTRAP has **not been
286
+ re-validated at that scale since the fix**
287
+ (`scripts/experiments/segmented-grad-test-e4b.ts` is the repro harness).
288
+ Prefer the default; treat `flash` as experimental on e4b long context.
289
+ Detail in segmented-backward-training §6.
271
290
 
272
291
  ## Outputs
273
292
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlx-bun",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Native MLX inference for Bun on Apple Silicon — local LLM server (OpenAI/Anthropic-compatible) and TypeScript library. No Python.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/anthropic.ts CHANGED
@@ -50,6 +50,18 @@ export interface AnthropicRequest {
50
50
  top_k?: number;
51
51
  stop_sequences?: string[];
52
52
  stream?: boolean;
53
+ /** mlx-lm sampler/penalty extensions — not part of the Anthropic protocol,
54
+ * accepted as pass-through extras with the same names as our /v1 chat
55
+ * surface. Anthropic has NO logit_bias, so none is accepted here. */
56
+ min_p?: number;
57
+ xtc_probability?: number;
58
+ xtc_threshold?: number;
59
+ repetition_penalty?: number;
60
+ repetition_context_size?: number;
61
+ presence_penalty?: number;
62
+ presence_context_size?: number;
63
+ frequency_penalty?: number;
64
+ frequency_context_size?: number;
53
65
  tools?: Array<{
54
66
  name?: string;
55
67
  description?: string;
@@ -167,6 +179,16 @@ export function anthropicToChatBody(body: AnthropicRequest): Record<string, unkn
167
179
  if (body.top_k != null) oai.top_k = body.top_k;
168
180
  if (body.stop_sequences?.length) oai.stop = body.stop_sequences;
169
181
  if (body.stream) oai.stream = true;
182
+ // mlx-lm sampler/penalty extensions: straight pass-through (same wire names
183
+ // on the chat surface). No logit_bias — it doesn't exist in this protocol.
184
+ for (const k of [
185
+ "min_p", "xtc_probability", "xtc_threshold",
186
+ "repetition_penalty", "repetition_context_size",
187
+ "presence_penalty", "presence_context_size",
188
+ "frequency_penalty", "frequency_context_size",
189
+ ] as const) {
190
+ if (body[k] != null) oai[k] = body[k];
191
+ }
170
192
 
171
193
  // Client tools only (entries with name + input_schema); server-tool
172
194
  // types (web_search etc.) have no local meaning and are dropped.
@@ -89,7 +89,7 @@ button.ghost{background:var(--card);border:1px solid var(--bd);border-radius:8px
89
89
  <script>
90
90
  const ln=Math.log;
91
91
  const UMIN=ln(1e-6); // bottom-left / plot floor (matches src/curve-sampler.ts CURVE_UMIN)
92
- const ENGINE_BASE = location.protocol.startsWith("http") ? location.origin : "http://localhost:8090";
92
+ const ENGINE_BASE = location.protocol.startsWith("http") ? location.origin : "http://localhost:8080";
93
93
  // ---- curve state: 5 movable control points, sorted ascending in p_in ----
94
94
  function identityPoints(){ return [{x_pct:1e-4,y_pct:1e-4},{x_pct:0.1,y_pct:0.1},{x_pct:1,y_pct:1},{x_pct:9,y_pct:9},{x_pct:100,y_pct:100}]; }
95
95
  let C = { space:"logprob", points:identityPoints(), monotonic:true };
@@ -131,20 +131,38 @@ export class ChatTemplate {
131
131
  /** True when the model has a switchable reasoning channel we can parse. */
132
132
  readonly supportsThinking: boolean;
133
133
 
134
- private constructor(source: string, bosToken: string | null, eosToken: string | null) {
134
+ private constructor(
135
+ source: string,
136
+ bosToken: string | null,
137
+ eosToken: string | null,
138
+ forceNoThinking = false,
139
+ ) {
135
140
  this.#template = new Template(source);
136
141
  this.#bosToken = bosToken;
137
142
  this.#eosToken = eosToken;
138
143
  const gatesThinking = source.includes("enable_thinking");
139
- this.thinkingFormat = gatesThinking && source.includes("<think>")
140
- ? "think-tag"
141
- : gatesThinking && source.includes("<|channel>")
142
- ? "gemma-channel"
143
- : null;
144
+ // `forceNoThinking` suppresses the switchable channel even when the template
145
+ // carries the gemma markers. DiffusionGemma ships the shared gemma-family
146
+ // template (with the `<|channel>thought…<channel|>` reasoning channel), but
147
+ // its non-autoregressive canvas decode cannot reliably emit the `<channel|>`
148
+ // close, so an enabled channel never ends (everything is captured as
149
+ // reasoning, no answer). The OptiQ reference never enables thinking for this
150
+ // model — it always renders the template's `default(false)` pre-closed empty
151
+ // channel and decodes the whole canvas as plain text. We match that.
152
+ this.thinkingFormat = forceNoThinking
153
+ ? null
154
+ : gatesThinking && source.includes("<think>")
155
+ ? "think-tag"
156
+ : gatesThinking && source.includes("<|channel>")
157
+ ? "gemma-channel"
158
+ : null;
144
159
  this.supportsThinking = this.thinkingFormat !== null;
145
160
  }
146
161
 
147
- static async load(modelDir: string): Promise<ChatTemplate> {
162
+ static async load(
163
+ modelDir: string,
164
+ opts: { disableThinking?: boolean } = {},
165
+ ): Promise<ChatTemplate> {
148
166
  const config = (await Bun.file(`${modelDir}/tokenizer_config.json`).json()) as Record<string, any>;
149
167
  let source: string | undefined = config.chat_template;
150
168
  if (!source) {