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
package/src/generate.ts CHANGED
@@ -61,15 +61,43 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
61
61
  * When set, `promptTokens` are the spliced (<|image|>-expanded) ids and the
62
62
  * denoising engine prefills the merged vision features. Caller keeps ownership. */
63
63
  visionPixels?: MlxArray;
64
+ /** Capture each emitted token's log-probability (mlx_lm.server `logprobs`).
65
+ * The distribution matches mlx-lm generate_step exactly: full-vocab
66
+ * log-softmax of the logits AFTER logits processors, BEFORE the sampler's
67
+ * temperature/top-p/top-k/min-p/XTC (generate.py L409-422). Off by default —
68
+ * the hot path pays nothing when unset. */
69
+ logprobs?: boolean;
70
+ /** Capture the top-k (token id, logprob) pairs per emitted token from the
71
+ * same distribution (mlx_lm.server `top_logprobs`). 0/unset = off. */
72
+ topLogprobs?: number;
64
73
  }
65
74
 
66
- /** Port of mlx-lm maybe_quantize_kv_cache + optiq serve's per-layer
67
- * patched variant (incl. patch_rotating_to_quantized: rotating caches
68
- * convert too — Phase 9). kvConfig overrides kvBits, matching optiq's
69
- * --kv-config precedence; shipped kv_config.json files list
70
- * full-attention layers only, so rotating quantization engages through
71
- * uniform kvBits (like optiq --kv-bits) or a config that names sliding
72
- * layers. */
75
+ /** Per-token logprob info (only present when GenerateOptions requested it). */
76
+ export interface TokenLogprobs {
77
+ /** logprob of the emitted token (requires options.logprobs). */
78
+ logprob?: number;
79
+ /** top-k (id, logprob) pairs, sorted by logprob descending (requires
80
+ * options.topLogprobs > 0). mlx-lm leaves argpartition order unspecified;
81
+ * sorting is the deterministic reading of the same set. */
82
+ top?: { id: number; logprob: number }[];
83
+ }
84
+
85
+ /** Port of mlx-lm maybe_quantize_kv_cache + BOTH halves of optiq serve's
86
+ * per-layer patched variant (incl. patch_rotating_to_quantized: rotating
87
+ * caches convert too — Phase 9):
88
+ * - per-layer bits/group_size selection (kvConfig overrides kvBits,
89
+ * matching optiq's --kv-config precedence; shipped kv_config.json
90
+ * files list full-attention layers only, so rotating quantization
91
+ * engages through uniform kvBits — like optiq --kv-bits — or a config
92
+ * that names sliding layers), and
93
+ * - STREAMING conversion (optiq streaming_kv_quant / serve.py
94
+ * patched_maybe_quantize): eval each layer's quantized triples and
95
+ * clear the buffer pool before building the next layer's conversion.
96
+ * Lazily batching every layer's toQuantized into one eval pins ALL
97
+ * layers' bf16 K/V as graph inputs alongside ALL quantized outputs —
98
+ * the exact transient optiq's fix kills (16.35 → 7.60 GB at 32k on a
99
+ * 24 GB Mac). Numerics untouched: same quantize math, only the eval
100
+ * ordering is forced (tests/kv-quant.test.ts, tests/rotating-kvq.test.ts). */
73
101
  export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void {
74
102
  const { kvBits, kvConfig } = options;
75
103
  if (!kvBits && !kvConfig?.length) return;
@@ -87,10 +115,16 @@ export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void
87
115
  if (c.offset === 0) continue;
88
116
  if (byLayer) {
89
117
  const e = byLayer.get(i);
90
- if (e) cache[i] = c.toQuantized(e.groupSize, e.bits);
118
+ if (!e) continue;
119
+ cache[i] = c.toQuantized(e.groupSize, e.bits);
91
120
  } else {
92
121
  cache[i] = c.toQuantized(options.kvGroupSize ?? 64, kvBits!);
93
122
  }
123
+ // Streaming half: materialize THIS layer's conversion now so its bf16
124
+ // source (already released by toQuantized) frees before the next layer
125
+ // converts — the transient stays ~one layer, not the whole cache.
126
+ ops.evalAll(cache[i]!.state());
127
+ clearCache();
94
128
  }
95
129
  }
96
130
 
@@ -112,6 +146,8 @@ export interface GenerateStats {
112
146
  export interface GeneratedToken {
113
147
  token: number;
114
148
  index: number;
149
+ /** Present only when GenerateOptions.logprobs / topLogprobs requested it. */
150
+ logprobs?: TokenLogprobs;
115
151
  }
116
152
 
117
153
  export class Generation implements AsyncIterable<GeneratedToken> {
@@ -273,6 +309,14 @@ async function* generateInner(
273
309
  const processors = makeLogitsProcessors(options);
274
310
  const needsTokenHistory = processors.length > 0;
275
311
 
312
+ // logprobs capture (mlx_lm.server semantics — see GenerateOptions.logprobs).
313
+ // Everything below is gated: when neither flag is set, no extra ops, evals,
314
+ // or readbacks happen on the hot path.
315
+ const captureSelLp = options.logprobs === true;
316
+ const captureTopK =
317
+ options.topLogprobs && options.topLogprobs > 0 ? options.topLogprobs : 0;
318
+ const capture = captureSelLp || captureTopK > 0;
319
+
276
320
  const ownsCache = !options.cache;
277
321
  const cache = options.cache ?? model.makeCache();
278
322
  const cachedTokens = cache[0]!.offset;
@@ -283,16 +327,73 @@ async function* generateInner(
283
327
  /** device-side token history (only maintained when processors need it) */
284
328
  let history: MlxArray | null = null;
285
329
 
286
- // logits [1,1,V] → sampled token array [1] (all on-device)
287
- const sampleStep = (logits3d: MlxArray, step: number): MlxArray => {
330
+ /** Device-side logprob capture for one step, computed from the SAME lp the
331
+ * sampler saw (post-processors, pre-truncation) — read back lazily with the
332
+ * token so decode pipelining is preserved. */
333
+ interface StepExtras {
334
+ sel: MlxArray | null; // [1,1] emitted token's logprob
335
+ topIdx: MlxArray | null; // [1,k] top-k token ids
336
+ topVals: MlxArray | null; // [1,k] their logprobs
337
+ }
338
+ const extrasArrays = (e: StepExtras | null): MlxArray[] =>
339
+ e ? [e.sel, e.topIdx, e.topVals].filter((a): a is MlxArray => a !== null) : [];
340
+ const disposeExtras = (e: StepExtras | null): void => {
341
+ if (e) for (const a of [e.sel, e.topIdx, e.topVals]) a?.dispose();
342
+ };
343
+ /** Read extras back to JS (forces eval — they were async-dispatched with the
344
+ * token, so this normally just copies) and dispose the device arrays. */
345
+ const readExtras = (e: StepExtras | null): TokenLogprobs | undefined => {
346
+ if (!e) return undefined;
347
+ const out: TokenLogprobs = {};
348
+ if (e.sel) out.logprob = e.sel.toFloat32()[0]!;
349
+ if (e.topIdx && e.topVals) {
350
+ // uint32 ids read via the f32 cast: exact for ids < 2^24 (vocab ≪ 16M)
351
+ const ids = e.topIdx.toFloat32();
352
+ const vals = e.topVals.toFloat32();
353
+ out.top = Array.from(ids, (id, i) => ({ id, logprob: vals[i]! })).sort(
354
+ (a, b) => b.logprob - a.logprob,
355
+ );
356
+ }
357
+ disposeExtras(e);
358
+ return out;
359
+ };
360
+
361
+ // logits [1,1,V] → sampled token array [1] (+ optional logprob capture,
362
+ // all on-device)
363
+ const sampleStep = (
364
+ logits3d: MlxArray,
365
+ step: number,
366
+ ): { tok: MlxArray; extras: StepExtras | null } => {
288
367
  const V = logits3d.shape[2]!;
289
368
  let logits = ops.reshape(logits3d, [1, V]);
290
369
  for (const p of processors) logits = disposing(logits, p(history, logits));
291
370
  const lp = toLogprobs(logits);
292
371
  logits.dispose();
293
372
  const tok = sampler(lp, step);
373
+ let extras: StepExtras | null = null;
374
+ if (capture) {
375
+ let sel: MlxArray | null = null;
376
+ let topIdx: MlxArray | null = null;
377
+ let topVals: MlxArray | null = null;
378
+ if (captureSelLp) {
379
+ const idx = ops.reshape(tok, [1, 1]);
380
+ sel = ops.takeAlongAxis(lp, idx, -1);
381
+ idx.dispose();
382
+ }
383
+ if (captureTopK > 0) {
384
+ // mlx-lm _format_top_logprobs: argpartition(-logprobs, kth=k-1)[:k]
385
+ const k = Math.min(captureTopK, V);
386
+ const negLp = ops.neg(lp);
387
+ const part = ops.argpartitionAxis(negLp, k - 1, -1);
388
+ const idxView = part.slice([0, 0], [1, k]);
389
+ topIdx = ops.contiguous(idxView); // slices are strided views; raw readback needs contiguous
390
+ topVals = ops.takeAlongAxis(lp, topIdx, -1);
391
+ for (const a of [negLp, part, idxView]) a.dispose();
392
+ }
393
+ extras = { sel, topIdx, topVals };
394
+ }
294
395
  lp.dispose();
295
- return tok;
396
+ return { tok, extras };
296
397
  };
297
398
 
298
399
  const pushHistory = (tok: MlxArray) => {
@@ -317,6 +418,8 @@ async function* generateInner(
317
418
  const forwarded: number[] = [];
318
419
  let pending: MlxArray | null = null;
319
420
  let nextPending: MlxArray | null = null;
421
+ let pendingExtras: StepExtras | null = null;
422
+ let nextExtras: StepExtras | null = null;
320
423
  let finished = false;
321
424
  let threw = false;
322
425
  const makeStats = (): GenerateStats => ({
@@ -379,7 +482,9 @@ async function* generateInner(
379
482
  h0.dispose();
380
483
  const logits0 = model.logitsFromHidden(hLast);
381
484
  hLast.dispose();
382
- pending = sampleStep(logits0, 0); // token array [1]
485
+ const s0 = sampleStep(logits0, 0); // token array [1] (+ optional extras)
486
+ pending = s0.tok;
487
+ pendingExtras = s0.extras;
383
488
  logits0.dispose();
384
489
  // mirror mlx-lm generate_step: async-dispatch the first token's
385
490
  // compute; the prefill clock keeps running until the token ARRIVES
@@ -389,7 +494,7 @@ async function* generateInner(
389
494
  // to prompt_time, not decode — replicated so cross-stack decode
390
495
  // tok/s measure the same quantity. The boundary cost is real and
391
496
  // scales with prompt length; it belongs to "having prefilled".
392
- ops.asyncEvalAll([pending]);
497
+ ops.asyncEvalAll([pending, ...extrasArrays(pendingExtras)]);
393
498
 
394
499
  // ---- decode (pipelined) ----
395
500
  // Compiled decode (docs/design/optimization_plan.md Phase A): replay the per-step
@@ -403,8 +508,16 @@ async function* generateInner(
403
508
  // 0.6.0, and shapeless replay re-infers the whole tape whenever the
404
509
  // growing attention windows change shape (= every step). Remove this
405
510
  // when upstream implements GatherQMM::output_shapes.
511
+ // MLX_BUN_FUSED_DECODE=1 (the N-tiled fused decode experiment) cannot
512
+ // compose with compiled decode: the tile loop's bounds bake at trace-time
513
+ // N, so growing quantized caches silently stop attending their newest
514
+ // rows under shapeless replay (kernel-perf-review 2026-07; reproduced on
515
+ // e4b). The explicit opt-in wins over the default optimization — same
516
+ // policy as LoRA and MoE below. quantizedSdpa also throws on the combo
517
+ // as a backstop.
406
518
  let compiled =
407
519
  process.env.MLX_BUN_COMPILED_DECODE !== "0" &&
520
+ process.env.MLX_BUN_FUSED_DECODE !== "1" &&
408
521
  !options.adapters?.length &&
409
522
  model.config.modelType.startsWith("gemma4") &&
410
523
  !model.config.text.enableMoeBlock
@@ -413,8 +526,11 @@ async function* generateInner(
413
526
  let stop = false;
414
527
  while (!stop) {
415
528
  const cur = pending!;
529
+ const curExtras = pendingExtras;
530
+ pendingExtras = null;
416
531
  // build step n+1's graph from the *unread* pending token
417
532
  nextPending = null;
533
+ nextExtras = null;
418
534
  if (generated + 1 < maxTokens) {
419
535
  maybeQuantizeKv(cache, options);
420
536
  pushHistory(cur);
@@ -426,8 +542,11 @@ async function* generateInner(
426
542
  logits = r.logits;
427
543
  evalWith = r.evalWith;
428
544
  } catch (e) {
429
- // growth done by a partial prepare is benign for the
430
- // uncompiled path (updateAndFetch re-checks capacity)
545
+ // Safe to re-forward the SAME token below: a failed step is
546
+ // transactional — segmented mode stages ring adopts and rolls
547
+ // back committed js-layer writes on throw (see #stepSegmented),
548
+ // and buffer growth done by a partial prepare is benign for the
549
+ // uncompiled path (updateAndFetch re-checks capacity).
431
550
  compiled = null;
432
551
  console.warn(`compiled decode disabled for this generation: ${e}`);
433
552
  }
@@ -439,9 +558,11 @@ async function* generateInner(
439
558
  logits = model.logitsFromHidden(h);
440
559
  h.dispose();
441
560
  }
442
- nextPending = sampleStep(logits, generated + 1);
561
+ const sn = sampleStep(logits, generated + 1);
562
+ nextPending = sn.tok;
563
+ nextExtras = sn.extras;
443
564
  logits.dispose();
444
- ops.asyncEvalAll([nextPending, ...evalWith]);
565
+ ops.asyncEvalAll([nextPending, ...extrasArrays(nextExtras), ...evalWith]);
445
566
  }
446
567
 
447
568
  // sync-read step n's token while n+1 computes
@@ -460,11 +581,17 @@ async function* generateInner(
460
581
  if (nextPending !== null) forwarded.push(token);
461
582
 
462
583
  if (eosTokenIds.includes(token)) {
584
+ disposeExtras(curExtras);
463
585
  nextPending?.dispose();
464
586
  nextPending = null;
587
+ disposeExtras(nextExtras);
588
+ nextExtras = null;
465
589
  stop = true;
466
590
  } else {
467
- yield { token, index: generated - 1 };
591
+ // readExtras before the yield: if the consumer breaks at this yield,
592
+ // the extras are already read and disposed.
593
+ const logprobs = readExtras(curExtras);
594
+ yield { token, index: generated - 1, ...(logprobs ? { logprobs } : {}) };
468
595
  // mlx-lm generate_step: clear_cache after token 0 (drops the
469
596
  // remaining prefill transients) and every 256 tokens after
470
597
  if ((generated - 1) % 256 === 0) clearCache();
@@ -473,6 +600,8 @@ async function* generateInner(
473
600
  } else {
474
601
  pending = nextPending;
475
602
  nextPending = null;
603
+ pendingExtras = nextExtras;
604
+ nextExtras = null;
476
605
  }
477
606
  }
478
607
  }
@@ -486,6 +615,8 @@ async function* generateInner(
486
615
  if (!finished) {
487
616
  pending?.dispose();
488
617
  nextPending?.dispose();
618
+ disposeExtras(pendingExtras);
619
+ disposeExtras(nextExtras);
489
620
  }
490
621
  if (ownsCache) for (const c of cache) c.dispose();
491
622
  history?.dispose();
package/src/harness-pi.ts CHANGED
@@ -22,7 +22,10 @@ export const PI_PROVIDER_ID = "mlx-bun";
22
22
  * server still reports the true id in /v1/models and responses. Gives a
23
23
  * clean `mlx-bun/local` handle instead of `mlx-bun/mlx-community/…`. */
24
24
  export const PI_LOCAL_MODEL_ID = "local";
25
- export const DEFAULT_BASE_URL = "http://localhost:8090/v1";
25
+ // 127.0.0.1 (the server's actual default bind), not "localhost": the generated
26
+ // extension runs under the USER'S pi (node/jiti, not Bun), so don't depend on
27
+ // how that runtime's resolver maps localhost.
28
+ export const DEFAULT_BASE_URL = "http://127.0.0.1:8080/v1";
26
29
 
27
30
  export function defaultPiExtensionsDir(): string {
28
31
  return join(homedir(), ".pi", "agent", "extensions");
@@ -63,22 +66,34 @@ export interface ServerModel {
63
66
  contextWindow: number;
64
67
  maxTokens: number;
65
68
  reasoning: boolean;
69
+ vision: boolean;
66
70
  }
67
71
 
68
- /** Discover models from a running mlx-bun server; [] when unreachable. */
72
+ /** Discover the SERVED model from a running mlx-bun server; [] when
73
+ * unreachable. /v1/models lists the served model first (the only row
74
+ * carrying the capability extras: context_window/reasoning/vision) and then
75
+ * every other servable model in the registry — those are things `mlx-bun
76
+ * serve` COULD run, not models this server answers for, so they must not be
77
+ * registered with pi. */
69
78
  export async function fetchServerModels(baseUrl: string, timeoutMs = 3000): Promise<ServerModel[]> {
70
79
  try {
71
80
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
72
81
  signal: AbortSignal.timeout(timeoutMs),
73
82
  });
74
83
  if (!res.ok) return [];
75
- const body = (await res.json()) as { data?: Array<{ id: string; context_window?: number; reasoning?: boolean }> };
76
- return (body.data ?? []).map((m) => ({
77
- id: m.id,
78
- contextWindow: m.context_window ?? 32768,
84
+ const body = (await res.json()) as {
85
+ data?: Array<{ id: string; context_window?: number; reasoning?: boolean; vision?: boolean }>;
86
+ };
87
+ const rows = body.data ?? [];
88
+ const served = rows.find((m) => m.context_window !== undefined) ?? rows[0];
89
+ if (!served) return [];
90
+ return [{
91
+ id: served.id,
92
+ contextWindow: served.context_window ?? 32768,
79
93
  maxTokens: 8192,
80
- reasoning: !!m.reasoning,
81
- }));
94
+ reasoning: !!served.reasoning,
95
+ vision: !!served.vision,
96
+ }];
82
97
  } catch {
83
98
  return [];
84
99
  }
@@ -113,16 +128,22 @@ export default async function (pi: any) {
113
128
  if (res.ok) {
114
129
  const { data } = await res.json();
115
130
  if (Array.isArray(data) && data.length > 0) {
116
- models = data.map((m: any) => ({
131
+ // The SERVED model is the row with the capability extras (first);
132
+ // the rest are servable-but-not-served registry entries.
133
+ const served = data.find((m: any) => m.context_window !== undefined) ?? data[0];
134
+ models = [{
117
135
  id: ${JSON.stringify(PI_LOCAL_MODEL_ID)},
118
- name: \`\${m.id} (mlx-bun local)\`,
119
- reasoning: !!m.reasoning,
120
- ...(m.reasoning ? { compat: { thinkingFormat: "qwen-chat-template" } } : {}),
121
- input: ["text"],
136
+ name: \`\${served.id} (mlx-bun local)\`,
137
+ reasoning: !!served.reasoning,
138
+ compat: {
139
+ supportsDeveloperRole: false,
140
+ ...(served.reasoning ? { thinkingFormat: "qwen-chat-template" } : {}),
141
+ },
142
+ input: served.vision ? ["text", "image"] : ["text"],
122
143
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
123
- contextWindow: m.context_window ?? 32768,
144
+ contextWindow: served.context_window ?? 32768,
124
145
  maxTokens: 8192,
125
- }));
146
+ }];
126
147
  }
127
148
  }
128
149
  } catch {}
@@ -142,8 +163,13 @@ function modelEntry(m: ServerModel) {
142
163
  id: PI_LOCAL_MODEL_ID,
143
164
  name: `${m.id} (mlx-bun local)`,
144
165
  reasoning: !!m.reasoning,
145
- ...(m.reasoning ? { compat: { thinkingFormat: "qwen-chat-template" } } : {}),
146
- input: ["text"],
166
+ // Kept aligned with buildPiProvider (src/pi-provider.ts): the system
167
+ // prompt must arrive as `system`, not `developer`.
168
+ compat: {
169
+ supportsDeveloperRole: false,
170
+ ...(m.reasoning ? { thinkingFormat: "qwen-chat-template" } : {}),
171
+ },
172
+ input: m.vision ? ["text", "image"] : ["text"],
147
173
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
148
174
  contextWindow: m.contextWindow,
149
175
  maxTokens: m.maxTokens,
package/src/index.ts CHANGED
@@ -13,8 +13,10 @@ export { Weights } from "./weights";
13
13
  export { Gemma4Model } from "./model/gemma4";
14
14
  export { MiniCPM5Model } from "./model/minicpm5";
15
15
  export { createModel, type RuntimeModel } from "./model/factory";
16
+ export { Qwen3Model } from "./model/qwen3";
16
17
  export { generate } from "./generate";
17
18
  export { loadTokenizer } from "./tokenizer";
19
+ export { embedOne, embedMany, isEmbeddingModel, withInstruction, type EmbedResult } from "./embed";
18
20
  export { ChatTemplate } from "./chat-template";
19
21
  export { chooseAutoModel, COEXIST_FRACTION, DEFAULT_REPO_ID, fit, largestRecommendedRepoId, recommendedRepoId, skuMatrix, thisMachine } from "./fit";
20
22
  export { downloadModel } from "./download";