mlx-bun 0.0.8 → 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.
- package/README.md +112 -20
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +40 -9
- package/docs/reference/training.md +25 -11
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1029 -86
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +141 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +46 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +86 -18
- package/src/server.ts +612 -49
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +70 -15
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- 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
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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)
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
|
@@ -413,8 +518,11 @@ async function* generateInner(
|
|
|
413
518
|
let stop = false;
|
|
414
519
|
while (!stop) {
|
|
415
520
|
const cur = pending!;
|
|
521
|
+
const curExtras = pendingExtras;
|
|
522
|
+
pendingExtras = null;
|
|
416
523
|
// build step n+1's graph from the *unread* pending token
|
|
417
524
|
nextPending = null;
|
|
525
|
+
nextExtras = null;
|
|
418
526
|
if (generated + 1 < maxTokens) {
|
|
419
527
|
maybeQuantizeKv(cache, options);
|
|
420
528
|
pushHistory(cur);
|
|
@@ -426,8 +534,11 @@ async function* generateInner(
|
|
|
426
534
|
logits = r.logits;
|
|
427
535
|
evalWith = r.evalWith;
|
|
428
536
|
} catch (e) {
|
|
429
|
-
//
|
|
430
|
-
//
|
|
537
|
+
// Safe to re-forward the SAME token below: a failed step is
|
|
538
|
+
// transactional — segmented mode stages ring adopts and rolls
|
|
539
|
+
// back committed js-layer writes on throw (see #stepSegmented),
|
|
540
|
+
// and buffer growth done by a partial prepare is benign for the
|
|
541
|
+
// uncompiled path (updateAndFetch re-checks capacity).
|
|
431
542
|
compiled = null;
|
|
432
543
|
console.warn(`compiled decode disabled for this generation: ${e}`);
|
|
433
544
|
}
|
|
@@ -439,9 +550,11 @@ async function* generateInner(
|
|
|
439
550
|
logits = model.logitsFromHidden(h);
|
|
440
551
|
h.dispose();
|
|
441
552
|
}
|
|
442
|
-
|
|
553
|
+
const sn = sampleStep(logits, generated + 1);
|
|
554
|
+
nextPending = sn.tok;
|
|
555
|
+
nextExtras = sn.extras;
|
|
443
556
|
logits.dispose();
|
|
444
|
-
ops.asyncEvalAll([nextPending, ...evalWith]);
|
|
557
|
+
ops.asyncEvalAll([nextPending, ...extrasArrays(nextExtras), ...evalWith]);
|
|
445
558
|
}
|
|
446
559
|
|
|
447
560
|
// sync-read step n's token while n+1 computes
|
|
@@ -460,11 +573,17 @@ async function* generateInner(
|
|
|
460
573
|
if (nextPending !== null) forwarded.push(token);
|
|
461
574
|
|
|
462
575
|
if (eosTokenIds.includes(token)) {
|
|
576
|
+
disposeExtras(curExtras);
|
|
463
577
|
nextPending?.dispose();
|
|
464
578
|
nextPending = null;
|
|
579
|
+
disposeExtras(nextExtras);
|
|
580
|
+
nextExtras = null;
|
|
465
581
|
stop = true;
|
|
466
582
|
} else {
|
|
467
|
-
|
|
583
|
+
// readExtras before the yield: if the consumer breaks at this yield,
|
|
584
|
+
// the extras are already read and disposed.
|
|
585
|
+
const logprobs = readExtras(curExtras);
|
|
586
|
+
yield { token, index: generated - 1, ...(logprobs ? { logprobs } : {}) };
|
|
468
587
|
// mlx-lm generate_step: clear_cache after token 0 (drops the
|
|
469
588
|
// remaining prefill transients) and every 256 tokens after
|
|
470
589
|
if ((generated - 1) % 256 === 0) clearCache();
|
|
@@ -473,6 +592,8 @@ async function* generateInner(
|
|
|
473
592
|
} else {
|
|
474
593
|
pending = nextPending;
|
|
475
594
|
nextPending = null;
|
|
595
|
+
pendingExtras = nextExtras;
|
|
596
|
+
nextExtras = null;
|
|
476
597
|
}
|
|
477
598
|
}
|
|
478
599
|
}
|
|
@@ -486,6 +607,8 @@ async function* generateInner(
|
|
|
486
607
|
if (!finished) {
|
|
487
608
|
pending?.dispose();
|
|
488
609
|
nextPending?.dispose();
|
|
610
|
+
disposeExtras(pendingExtras);
|
|
611
|
+
disposeExtras(nextExtras);
|
|
489
612
|
}
|
|
490
613
|
if (ownsCache) for (const c of cache) c.dispose();
|
|
491
614
|
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
|
-
|
|
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
|
|
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 {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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: !!
|
|
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
|
-
|
|
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: \`\${
|
|
119
|
-
reasoning: !!
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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
|
-
|
|
146
|
-
|
|
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";
|
package/src/lora.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function loadAdapterTensors(file: string): Map<string, MlxArray> {
|
|
|
77
77
|
* PEFT writes lora_alpha + r (scale = alpha / r). Reference: mount.py.
|
|
78
78
|
* `rsLora` (recorded at train time) means the effective per-layer scale is
|
|
79
79
|
* α/√rank — the caller divides by √(that layer's rank). */
|
|
80
|
-
async function readAdapterScale(dir: string): Promise<{ scale: number; rank: number | null; rsLora: boolean }> {
|
|
80
|
+
export async function readAdapterScale(dir: string): Promise<{ scale: number; rank: number | null; rsLora: boolean }> {
|
|
81
81
|
for (const name of ["optiq_lora_config.json", "adapter_config.json"]) {
|
|
82
82
|
const f = Bun.file(`${dir}/${name}`);
|
|
83
83
|
if (await f.exists()) {
|
|
@@ -94,7 +94,7 @@ async function readAdapterScale(dir: string): Promise<{ scale: number; rank: num
|
|
|
94
94
|
throw new Error(`no adapter_config.json in ${dir}`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function adapterWeightsFile(dir: string): string {
|
|
97
|
+
export function adapterWeightsFile(dir: string): string {
|
|
98
98
|
for (const name of ["adapters.safetensors", "adapter_model.safetensors"]) {
|
|
99
99
|
const p = `${dir}/${name}`;
|
|
100
100
|
if (existsSync(p)) return p;
|