@mlx-node/agent 0.0.10 → 0.0.12

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/dist/catalog.d.ts CHANGED
@@ -1,41 +1,103 @@
1
1
  /**
2
2
  * Curated model catalog for `mlx agent`.
3
3
  *
4
- * The first-run download wizard (Task 8) offers `visibleCatalog()` and
5
- * feeds the chosen `hfRepo` to `mlx download model`. Slugs are settled
6
- * with the user and verified against the Brooooooklyn HF account —
7
- * use them verbatim.
4
+ * The first-run download wizard offers `visibleCatalog()` and feeds the chosen
5
+ * entry's `catalogRepo()` to `mlx download model`. Slugs are verified against the Brooooooklyn
6
+ * HF account use them verbatim.
8
7
  */
9
8
  export interface CatalogEntry {
10
9
  /** Wizard display name. */
11
10
  label: string;
12
- /** HF slug for `mlx download model`. */
11
+ /** HF slug for `mlx download model` on Apple Silicon — the MXFP4 build. */
13
12
  hfRepo: string;
13
+ /**
14
+ * HF slug on Linux + NVIDIA CUDA — the NVFP4 build.
15
+ *
16
+ * Absent means the entry has no CUDA-specific build and {@link hfRepo}
17
+ * serves both. Resolve with {@link catalogRepo}, never by reading the field.
18
+ */
19
+ hfRepoCuda?: string;
14
20
  /** Approximate download size in GB, for display. */
15
21
  sizeGb: number;
16
22
  /** One line for the wizard. */
17
23
  description: string;
18
24
  /** Exactly one entry carries this. */
19
25
  isDefault?: boolean;
20
- /** Not offered by the wizard (repo not yet published). */
26
+ /**
27
+ * The repo is not published yet, so no UI may offer it as a download.
28
+ *
29
+ * Two consumers honour this: the agent wizard via {@link visibleCatalog},
30
+ * and the dashboard Models page, which filters `!item.hidden` before
31
+ * rendering cards (`packages/dashboard/ui/src/pages/models.tsx`).
32
+ * `catalogWithState()` deliberately keeps hidden entries so the UI, not the
33
+ * dashboard core, decides.
34
+ *
35
+ * Also honoured by the download allowlist: `DownloadManager.start`
36
+ * (`packages/dashboard/src/download.ts`) refuses a hidden repo up front
37
+ * rather than allocating a job that fails mid-download with a 401 from
38
+ * Hugging Face. No UI reaches that path for a hidden entry, but a direct
39
+ * API call does.
40
+ */
21
41
  hidden?: boolean;
22
42
  }
23
43
  export declare const MODEL_CATALOG: readonly CatalogEntry[];
44
+ /**
45
+ * The repo THIS platform installs for `entry`. Linux is the CUDA preview
46
+ * target (README "Platform Support"); everything else is Apple Silicon.
47
+ *
48
+ * NOT because Metal cannot run NVFP4 — it can. MLX ships the same 234
49
+ * quantized Metal kernels for `nvfp4` as for `mxfp4`, NAX variants included,
50
+ * and only `fp8_e4m3` reconstructs BF16 at load. The MSL 4.1 hardware
51
+ * block-scale format (`metal_fp8_ue8m0_format`) appears nowhere in MLX's Metal
52
+ * backend, so it constrains neither format here.
53
+ *
54
+ * The split is:
55
+ * - CUDA takes NVFP4 through `CublasQQMM` (`nvfp4` -> `CUDA_R_4F_E2M1`), a
56
+ * native path with no MXFP4 equivalent.
57
+ * - Metal has no such native-path advantage either way, so prefer the format
58
+ * that survives quantization better. NVFP4 stores a block scale as `amax/6`
59
+ * in E4M3, and real FFN blocks land in its subnormal band;
60
+ * `apply_nvfp4_pow2_lift` repairs that for dense SwiGLU FFNs but SKIPS MoE
61
+ * experts by design (`NVFP4_LIFT_MOE_MARKERS`, `crates/mlx-core/src/
62
+ * convert.rs`), because the norm there also drives the router and the
63
+ * shared-expert gate, neither scale-invariant. Two of the three visible
64
+ * entries are MoE. MXFP4's E8M0 block scales have no such failure.
65
+ *
66
+ * Unmeasured: whether nvfp4 or mxfp4 decodes faster on Metal. The choice above
67
+ * is made on quantization quality, not throughput.
68
+ *
69
+ * Every consumer that turns a catalog entry into a download, a slug, or an
70
+ * allowlist check must go through here. Reading `entry.hfRepo` directly
71
+ * installs the macOS build on a CUDA box.
72
+ */
73
+ export declare function catalogRepo(entry: CatalogEntry): string;
74
+ /**
75
+ * {@link catalogRepo} with the platform passed in — the pure half.
76
+ *
77
+ * Exists so both branches can be asserted without touching `process.platform`.
78
+ * Mutating that global leaks across test files sharing a worker: it made
79
+ * `catalogRepo` disagree with a sibling suite's module-level constant and fail
80
+ * a download allowlist check that has nothing to do with the catalog.
81
+ */
82
+ export declare function catalogRepoFor(entry: CatalogEntry, platform: NodeJS.Platform): string;
24
83
  /** Catalog entries the wizard offers (hidden entries filtered out). */
25
84
  export declare function visibleCatalog(): CatalogEntry[];
26
85
  /**
27
- * Cold-tier facts, re-exported through this subpath.
86
+ * Cold-tier facts and family registration data, re-exported through this
87
+ * subpath.
28
88
  *
29
89
  * `@mlx-node/agent/catalog` is the agent package's one NATIVE-FREE entry point:
30
90
  * the package root re-exports `provider/index.ts`, which value-imports
31
91
  * `@mlx-node/core`. The dashboard is a separate viewer process that must never
32
92
  * link the addon (docs/dashboard.md: "no Metal init, instant start"), and
33
93
  * `mlx agent --help` must print without loading weights — so both reach the
34
- * cold-tier allowlist and the cache-root canonicalizer through here.
94
+ * cold-tier allowlist, the cache-root canonicalizer, and the family detection
95
+ * data through here.
35
96
  *
36
- * These are RE-EXPORTS. The definitions live in `./cold-tier.ts` and there is
37
- * exactly one of each; `packages/agent/__test__/cold-tier-families.test.ts`
38
- * guards the allowlist against the native side.
97
+ * Every module reachable from here must therefore stay free of runtime addon
98
+ * imports. `packages/agent/__test__/catalog-native-free.test.ts` gates that in a
99
+ * real subprocess.
39
100
  */
40
101
  export { COLD_TIER_RESTORE_FAMILIES, canonicalCacheRoot, coldTierRestoreFamilyList } from './cold-tier.js';
102
+ export { CHAT_FAMILY_IDS, matchFamily, NON_GENERATIVE_FAMILY_IDS, rawModelTypeToCanonical, } from '@mlx-node/lm/family-data';
41
103
  //# sourceMappingURL=catalog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EA+BhD,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C;AAED;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EA0DhD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,CAErF;AAED,uEAAuE;AACvE,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C;AAED;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3G,OAAO,EACL,eAAe,EACf,WAAW,EACX,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,0BAA0B,CAAC"}
package/dist/catalog.js CHANGED
@@ -1,29 +1,31 @@
1
1
  /**
2
2
  * Curated model catalog for `mlx agent`.
3
3
  *
4
- * The first-run download wizard (Task 8) offers `visibleCatalog()` and
5
- * feeds the chosen `hfRepo` to `mlx download model`. Slugs are settled
6
- * with the user and verified against the Brooooooklyn HF account —
7
- * use them verbatim.
4
+ * The first-run download wizard offers `visibleCatalog()` and feeds the chosen
5
+ * entry's `catalogRepo()` to `mlx download model`. Slugs are verified against the Brooooooklyn
6
+ * HF account use them verbatim.
8
7
  */
9
8
  export const MODEL_CATALOG = [
10
9
  {
11
- label: 'Qwen3.6-27B',
12
- hfRepo: 'Brooooooklyn/Qwen3.6-27B-NVFP4-mlx',
13
- sizeGb: 22.2,
10
+ label: 'Qwen3.8-27B',
11
+ hfRepo: 'Brooooooklyn/Qwen3.8-27B-MXFP4-mlx',
12
+ hfRepoCuda: 'Brooooooklyn/Qwen3.8-27B-NVFP4-mlx',
13
+ sizeGb: 23.3,
14
14
  description: 'Best tool use — recommended default',
15
15
  isDefault: true,
16
16
  },
17
17
  {
18
18
  label: 'Qwen-AgentWorld-35B',
19
- hfRepo: 'Brooooooklyn/Qwen-AgentWorld-35B-A3B-nvfp4-mlx',
20
- sizeGb: 22.7,
19
+ hfRepo: 'Brooooooklyn/Qwen-AgentWorld-35B-A3B-mxfp4-mlx',
20
+ hfRepoCuda: 'Brooooooklyn/Qwen-AgentWorld-35B-A3B-nvfp4-mlx',
21
+ sizeGb: 23.3,
21
22
  description: 'Agent-tuned MoE, fast decode',
22
23
  },
23
24
  {
24
25
  label: 'Gemma-4-26B-A4B',
25
- hfRepo: 'Brooooooklyn/Gemma-4-26B-A4B-NVFP4-mlx',
26
- sizeGb: 18.8,
26
+ hfRepo: 'Brooooooklyn/Gemma-4-26B-A4B-Unsloth-MXFP4-mlx',
27
+ hfRepoCuda: 'Brooooooklyn/Gemma-4-26B-A4B-Unsloth-NVFP4-mlx',
28
+ sizeGb: 16.2,
27
29
  description: 'MoE, fast decode',
28
30
  },
29
31
  {
@@ -37,23 +39,93 @@ export const MODEL_CATALOG = [
37
39
  description: 'Compact (mxfp4 MLP + mxfp8 attention), fits smaller machines',
38
40
  hidden: true,
39
41
  },
42
+ {
43
+ // NOT DOWNLOADABLE. This repo does not exist: the slug below is a
44
+ // placeholder the user has not uploaded to, and Hugging Face answers 401
45
+ // for it. Nothing may offer it as an install until the upload happens and
46
+ // `hidden` is dropped — and do not guess a substitute slug, because a
47
+ // wrong-but-live repo would install the wrong weights silently.
48
+ //
49
+ // The only route to this model today is local conversion from NVIDIA's
50
+ // modelopt checkpoint:
51
+ // mlx convert -m nemotron_h \
52
+ // -i <nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-NVFP4> \
53
+ // -o <models>/nemotron-3.5-lightning-30b-a3b-nvfp4-mlx
54
+ // (NVFP4 preserved byte-for-byte; the FP8 Mamba-2 projections are
55
+ // re-quantized. See docs/cli.md "modelopt NVFP4 ingest".)
56
+ //
57
+ // The entry is kept — rather than deleted — so the wizard, the dashboard
58
+ // catalog state, and `catalogSlug()` recognize a locally converted
59
+ // checkpoint sitting at the canonical slug.
60
+ label: 'Nemotron-3.5-Lightning-30B-A3B',
61
+ hfRepo: 'Brooooooklyn/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-nvfp4-mlx',
62
+ sizeGb: 23,
63
+ description: 'Hybrid Mamba-2 + MoE, native MTP, 1M context',
64
+ hidden: true,
65
+ },
40
66
  ];
67
+ /**
68
+ * The repo THIS platform installs for `entry`. Linux is the CUDA preview
69
+ * target (README "Platform Support"); everything else is Apple Silicon.
70
+ *
71
+ * NOT because Metal cannot run NVFP4 — it can. MLX ships the same 234
72
+ * quantized Metal kernels for `nvfp4` as for `mxfp4`, NAX variants included,
73
+ * and only `fp8_e4m3` reconstructs BF16 at load. The MSL 4.1 hardware
74
+ * block-scale format (`metal_fp8_ue8m0_format`) appears nowhere in MLX's Metal
75
+ * backend, so it constrains neither format here.
76
+ *
77
+ * The split is:
78
+ * - CUDA takes NVFP4 through `CublasQQMM` (`nvfp4` -> `CUDA_R_4F_E2M1`), a
79
+ * native path with no MXFP4 equivalent.
80
+ * - Metal has no such native-path advantage either way, so prefer the format
81
+ * that survives quantization better. NVFP4 stores a block scale as `amax/6`
82
+ * in E4M3, and real FFN blocks land in its subnormal band;
83
+ * `apply_nvfp4_pow2_lift` repairs that for dense SwiGLU FFNs but SKIPS MoE
84
+ * experts by design (`NVFP4_LIFT_MOE_MARKERS`, `crates/mlx-core/src/
85
+ * convert.rs`), because the norm there also drives the router and the
86
+ * shared-expert gate, neither scale-invariant. Two of the three visible
87
+ * entries are MoE. MXFP4's E8M0 block scales have no such failure.
88
+ *
89
+ * Unmeasured: whether nvfp4 or mxfp4 decodes faster on Metal. The choice above
90
+ * is made on quantization quality, not throughput.
91
+ *
92
+ * Every consumer that turns a catalog entry into a download, a slug, or an
93
+ * allowlist check must go through here. Reading `entry.hfRepo` directly
94
+ * installs the macOS build on a CUDA box.
95
+ */
96
+ export function catalogRepo(entry) {
97
+ return catalogRepoFor(entry, process.platform);
98
+ }
99
+ /**
100
+ * {@link catalogRepo} with the platform passed in — the pure half.
101
+ *
102
+ * Exists so both branches can be asserted without touching `process.platform`.
103
+ * Mutating that global leaks across test files sharing a worker: it made
104
+ * `catalogRepo` disagree with a sibling suite's module-level constant and fail
105
+ * a download allowlist check that has nothing to do with the catalog.
106
+ */
107
+ export function catalogRepoFor(entry, platform) {
108
+ return platform === 'linux' && entry.hfRepoCuda !== undefined ? entry.hfRepoCuda : entry.hfRepo;
109
+ }
41
110
  /** Catalog entries the wizard offers (hidden entries filtered out). */
42
111
  export function visibleCatalog() {
43
112
  return MODEL_CATALOG.filter((entry) => !entry.hidden);
44
113
  }
45
114
  /**
46
- * Cold-tier facts, re-exported through this subpath.
115
+ * Cold-tier facts and family registration data, re-exported through this
116
+ * subpath.
47
117
  *
48
118
  * `@mlx-node/agent/catalog` is the agent package's one NATIVE-FREE entry point:
49
119
  * the package root re-exports `provider/index.ts`, which value-imports
50
120
  * `@mlx-node/core`. The dashboard is a separate viewer process that must never
51
121
  * link the addon (docs/dashboard.md: "no Metal init, instant start"), and
52
122
  * `mlx agent --help` must print without loading weights — so both reach the
53
- * cold-tier allowlist and the cache-root canonicalizer through here.
123
+ * cold-tier allowlist, the cache-root canonicalizer, and the family detection
124
+ * data through here.
54
125
  *
55
- * These are RE-EXPORTS. The definitions live in `./cold-tier.ts` and there is
56
- * exactly one of each; `packages/agent/__test__/cold-tier-families.test.ts`
57
- * guards the allowlist against the native side.
126
+ * Every module reachable from here must therefore stay free of runtime addon
127
+ * imports. `packages/agent/__test__/catalog-native-free.test.ts` gates that in a
128
+ * real subprocess.
58
129
  */
59
130
  export { COLD_TIER_RESTORE_FAMILIES, canonicalCacheRoot, coldTierRestoreFamilyList } from './cold-tier.js';
131
+ export { CHAT_FAMILY_IDS, matchFamily, NON_GENERATIVE_FAMILY_IDS, rawModelTypeToCanonical, } from '@mlx-node/lm/family-data';
@@ -31,10 +31,11 @@
31
31
  *
32
32
  * - `qwen3` (dense) sizes its pool over all layers, so the pool holds the
33
33
  * complete KV for the prefix and needs no sidecar.
34
- * - `gemma4` sizes its pool over the full-attention layers only, but persists
35
- * its out-of-pool sliding-window `RotatingKVCache` state as a
36
- * `ColdGroup::SlidingWindow` sidecar, and its `ColdSidecarPolicy` makes the
37
- * native restore walk refuse any boundary a validated sidecar does not back.
34
+ * - `gemma4` owns full and sliding attention in distinct paged groups. The
35
+ * full-attention chain is authoritative and one grouped sliding sidecar
36
+ * restores every sliding group at the same exact boundary, atomically.
37
+ * - `muse_glimmer` uses the same hybrid contract for its 13 full-attention
38
+ * and 39 sliding-attention layers.
38
39
  * - `qwen3_5` (dense) sizes its pool over the full-attention layers only, but
39
40
  * persists its out-of-pool GDN recurrent state (conv + recurrent) as a
40
41
  * `ColdGroup::GdnState` sidecar, and its `ColdSidecarPolicy` reconciles the
@@ -43,13 +44,12 @@
43
44
  * - `qwen3_5_moe` keeps the SAME GDN recurrent state outside the pool — same
44
45
  * shapes, same dtype, same layer mapping — so it shares the dense family's
45
46
  * sidecar codec and `ColdSidecarPolicy` verbatim. Its restart-parity gate has
46
- * since been run on `Qwen3.6-35b-a3b-UD-Q2_K_XL-mlx`: the restore reconciled
47
+ * since been run on `Qwen3.6-35B-A3B-mxfp4-mlx`: the restore reconciled
47
48
  * onto ladder rung 304 of `[16, 64, 304, 1248]` with `hits=42` and
48
49
  * `corruptions=0`, and the text matched a no-persist baseline.
49
- * - `lfm2` / `lfm2_moe` keep short-conv state outside the pool with no
50
- * serialization path for it at all, AND drive the uniform native adapter
51
- * API whose restore branch is already wired to the tier — asking for
52
- * persistence on their behalf would restore an incomplete prefix silently.
50
+ * - `lfm2` / `lfm2_moe` keep ShortConv state outside the full-attention pool.
51
+ * A `ColdGroup::ConvState` sidecar persists that state at the same exact
52
+ * boundary; missing or malformed state reconciles down or restarts at zero.
53
53
  *
54
54
  * Widening this set is a correctness decision, never a perf one, and it is
55
55
  * authorized by exactly one thing: the family's native restart-parity gate
@@ -1 +1 @@
1
- {"version":3,"file":"cold-tier.d.ts","sourceRoot":"","sources":["../src/cold-tier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAKzD,CAAC;AAEH,qFAAqF;AACrF,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAEpD;AA2BD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBvD"}
1
+ {"version":3,"file":"cold-tier.d.ts","sourceRoot":"","sources":["../src/cold-tier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,CAQzD,CAAC;AAEH,qFAAqF;AACrF,wBAAgB,yBAAyB,IAAI,MAAM,EAAE,CAEpD;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBvD"}
package/dist/cold-tier.js CHANGED
@@ -34,10 +34,11 @@ import { basename, dirname, join, resolve } from 'node:path';
34
34
  *
35
35
  * - `qwen3` (dense) sizes its pool over all layers, so the pool holds the
36
36
  * complete KV for the prefix and needs no sidecar.
37
- * - `gemma4` sizes its pool over the full-attention layers only, but persists
38
- * its out-of-pool sliding-window `RotatingKVCache` state as a
39
- * `ColdGroup::SlidingWindow` sidecar, and its `ColdSidecarPolicy` makes the
40
- * native restore walk refuse any boundary a validated sidecar does not back.
37
+ * - `gemma4` owns full and sliding attention in distinct paged groups. The
38
+ * full-attention chain is authoritative and one grouped sliding sidecar
39
+ * restores every sliding group at the same exact boundary, atomically.
40
+ * - `muse_glimmer` uses the same hybrid contract for its 13 full-attention
41
+ * and 39 sliding-attention layers.
41
42
  * - `qwen3_5` (dense) sizes its pool over the full-attention layers only, but
42
43
  * persists its out-of-pool GDN recurrent state (conv + recurrent) as a
43
44
  * `ColdGroup::GdnState` sidecar, and its `ColdSidecarPolicy` reconciles the
@@ -46,13 +47,12 @@ import { basename, dirname, join, resolve } from 'node:path';
46
47
  * - `qwen3_5_moe` keeps the SAME GDN recurrent state outside the pool — same
47
48
  * shapes, same dtype, same layer mapping — so it shares the dense family's
48
49
  * sidecar codec and `ColdSidecarPolicy` verbatim. Its restart-parity gate has
49
- * since been run on `Qwen3.6-35b-a3b-UD-Q2_K_XL-mlx`: the restore reconciled
50
+ * since been run on `Qwen3.6-35B-A3B-mxfp4-mlx`: the restore reconciled
50
51
  * onto ladder rung 304 of `[16, 64, 304, 1248]` with `hits=42` and
51
52
  * `corruptions=0`, and the text matched a no-persist baseline.
52
- * - `lfm2` / `lfm2_moe` keep short-conv state outside the pool with no
53
- * serialization path for it at all, AND drive the uniform native adapter
54
- * API whose restore branch is already wired to the tier — asking for
55
- * persistence on their behalf would restore an incomplete prefix silently.
53
+ * - `lfm2` / `lfm2_moe` keep ShortConv state outside the full-attention pool.
54
+ * A `ColdGroup::ConvState` sidecar persists that state at the same exact
55
+ * boundary; missing or malformed state reconciles down or restarts at zero.
56
56
  *
57
57
  * Widening this set is a correctness decision, never a perf one, and it is
58
58
  * authorized by exactly one thing: the family's native restart-parity gate
@@ -69,10 +69,13 @@ import { basename, dirname, join, resolve } from 'node:path';
69
69
  * and asserted by `packages/cli/__test__/agent-cmd.test.ts`.
70
70
  */
71
71
  export const COLD_TIER_RESTORE_FAMILIES = new Set([
72
- 'gemma4',
73
72
  'qwen3',
74
73
  'qwen3_5',
75
74
  'qwen3_5_moe',
75
+ 'gemma4',
76
+ 'muse_glimmer',
77
+ 'lfm2',
78
+ 'lfm2_moe',
76
79
  ]);
77
80
  /** Allowlisted families in a stable, human-facing order (help text, API payload). */
78
81
  export function coldTierRestoreFamilyList() {
@@ -83,19 +86,6 @@ export function coldTierRestoreFamilyList() {
83
86
  * prefix that is part of a longer name (`~cache`) is left alone — only the
84
87
  * shell's own two spellings are expanded.
85
88
  */
86
- /**
87
- * `realpath(3)` rather than Node's JS walk.
88
- *
89
- * On a case-insensitive volume — APFS's default, i.e. essentially every Mac —
90
- * `MLX_COLD_CACHE_DIR=~/CacheDir` and `~/cachedir` name the SAME directory, but
91
- * `fs.realpathSync` hands back whichever spelling the caller typed. Two join
92
- * keys for one cache is the F1 symptom inverted: a populated disk scan sitting
93
- * beside a flat 0/0 trend. `realpathSync.native` folds each existing component
94
- * to its on-disk spelling, so both env spellings produce one key.
95
- */
96
- function realpathNative(path) {
97
- return realpathSync.native(path);
98
- }
99
89
  function expandTilde(path) {
100
90
  if (path === '~')
101
91
  return homedir();
@@ -139,7 +129,12 @@ export function canonicalCacheRoot(root) {
139
129
  let cursor = absolute;
140
130
  for (;;) {
141
131
  try {
142
- const resolved = realpathNative(cursor);
132
+ // `realpathSync.native` (realpath(3)), not Node's JS walk: on a
133
+ // case-insensitive volume — APFS's default — `~/CacheDir` and `~/cachedir`
134
+ // name the SAME directory, but `fs.realpathSync` returns whichever spelling
135
+ // the caller typed. `.native` folds each existing component to its on-disk
136
+ // spelling, so both env spellings produce one join key.
137
+ const resolved = realpathSync.native(cursor);
143
138
  return missing.length === 0 ? resolved : join(resolved, ...missing);
144
139
  }
145
140
  catch {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { DiscoveredModelLike } from './types.js';
2
- export { type CatalogEntry, MODEL_CATALOG, visibleCatalog } from './catalog.js';
2
+ export { type CatalogEntry, catalogRepo, catalogRepoFor, MODEL_CATALOG, visibleCatalog } from './catalog.js';
3
3
  export { createPermissionGateExtension } from './extensions/permission-gate.js';
4
4
  export { createSubagentExtension, discoverSubagents, normalizeSubagentMode, type InProcessSubagentSession, type SubagentConfig, type SubagentExtensionOptions, type SubagentMode, type SubagentSessionCreateOptions, } from './extensions/subagent.js';
5
5
  export { createTerminalTitleExtension } from './extensions/terminal-title.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,EAAE,KAAK,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EACjB,KAAK,4BAA4B,GAClC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC7G,OAAO,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,cAAc,EACnB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EACjB,KAAK,4BAA4B,GAClC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,4BAA4B,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { MODEL_CATALOG, visibleCatalog } from './catalog.js';
1
+ export { catalogRepo, catalogRepoFor, MODEL_CATALOG, visibleCatalog } from './catalog.js';
2
2
  export { createPermissionGateExtension } from './extensions/permission-gate.js';
3
3
  export { createSubagentExtension, discoverSubagents, normalizeSubagentMode, } from './extensions/subagent.js';
4
4
  export { createTerminalTitleExtension } from './extensions/terminal-title.js';
@@ -1,23 +1,11 @@
1
1
  /**
2
2
  * Per-call `ChatConfig` assembly for the provider bridge.
3
3
  *
4
- * Base sampling + output budget come from `@mlx-node/server`'s
5
- * `LAUNCH_PRESETS` (the ONLY allowed server import in this package —
6
- * presets/preset types, nothing else) extended by the agent-local
7
- * {@link AGENT_LAUNCH_PRESETS}, then pi's per-call `SimpleStreamOptions`
8
- * overlay on top.
4
+ * Base sampling + output budget come from the family-data launch preset
5
+ * (`@mlx-node/lm`), then pi's per-call `SimpleStreamOptions` overlay on top.
9
6
  */
10
7
  import type { SimpleStreamOptions, ThinkingLevel } from '@earendil-works/pi-ai';
11
- import type { ChatConfig, ModelType, ToolDefinition } from '@mlx-node/lm';
12
- import { type LaunchPreset } from '@mlx-node/server';
13
- /**
14
- * Preset lookup — agent-local entries win over `LAUNCH_PRESETS` (they
15
- * exist precisely because the server table has no correct entry for the
16
- * type). This is the ONE preset resolution shared by discovery
17
- * (`models.ts`) and per-call config assembly, so a model can never be
18
- * discovered without also being streamable (and vice versa).
19
- */
20
- export declare function launchPresetFor(modelType: ModelType): LaunchPreset | undefined;
8
+ import { type ChatConfig, type ModelType, type ToolDefinition } from '@mlx-node/lm';
21
9
  export interface ResolvedReasoningMode {
22
10
  reasoningEffort: 'none' | 'low' | 'medium' | 'high';
23
11
  /** The `enable_thinking` value implied by `reasoningEffort` for templates. */
@@ -1 +1 @@
1
- {"version":3,"file":"chat-config.d.ts","sourceRoot":"","sources":["../../src/provider/chat-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AA0BrE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAE9E;AAgBD,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpD,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,SAAS,GAAG,qBAAqB,CAMhG;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,mBAAmB,GAAG,SAAS,EACxC,KAAK,EAAE,cAAc,EAAE,GAAG,SAAS,EACnC,gBAAgB,CAAC,EAAE,MAAM,EACzB,iBAAiB,wBAA2C,EAC5D,cAAc,CAAC,EAAE,OAAO,GACvB,UAAU,CAsCZ"}
1
+ {"version":3,"file":"chat-config.d.ts","sourceRoot":"","sources":["../../src/provider/chat-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AA8BtB,MAAM,WAAW,qBAAqB;IACpC,eAAe,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpD,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,SAAS,GAAG,qBAAqB,CAMhG;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,mBAAmB,GAAG,SAAS,EACxC,KAAK,EAAE,cAAc,EAAE,GAAG,SAAS,EACnC,gBAAgB,CAAC,EAAE,MAAM,EACzB,iBAAiB,wBAA2C,EAC5D,cAAc,CAAC,EAAE,OAAO,GACvB,UAAU,CAsCZ"}
@@ -1,46 +1,23 @@
1
1
  /**
2
2
  * Per-call `ChatConfig` assembly for the provider bridge.
3
3
  *
4
- * Base sampling + output budget come from `@mlx-node/server`'s
5
- * `LAUNCH_PRESETS` (the ONLY allowed server import in this package —
6
- * presets/preset types, nothing else) extended by the agent-local
7
- * {@link AGENT_LAUNCH_PRESETS}, then pi's per-call `SimpleStreamOptions`
8
- * overlay on top.
4
+ * Base sampling + output budget come from the family-data launch preset
5
+ * (`@mlx-node/lm`), then pi's per-call `SimpleStreamOptions` overlay on top.
9
6
  */
10
- import { LAUNCH_PRESETS } from '@mlx-node/server';
7
+ import { launchPresetFor, MODEL_FAMILY_DATA, } from '@mlx-node/lm';
11
8
  /**
12
- * Agent-local launch presets for model types `LAUNCH_PRESETS` does not
13
- * cover (kept here this package must not fork `packages/server`).
14
- *
15
- * `lfm2_moe` (LFM2.5-8B-A1B): LiquidAI's HF model card for the MoE
16
- * checkpoint recommends temperature 0.2 / top_k 80 — deliberately NOT
17
- * the dense `lfm2` preset (LFM2.5-1.2B guidance: temperature 0.05 /
18
- * top_k 50). repetitionPenalty 1.05 and the 8192-token output budget
19
- * match the dense family entry.
9
+ * Model types the no-preset error names: trainable rows, then loadable rows,
10
+ * each in registry order. Pinned byte-exactly by
11
+ * `packages/agent/__test__/chat-config.test.ts`.
20
12
  */
21
- const AGENT_LAUNCH_PRESETS = {
22
- lfm2_moe: {
23
- sampling: {
24
- temperature: 0.2,
25
- topP: 1.0,
26
- topK: 80,
27
- minP: 0.0,
28
- presencePenalty: 0.0,
29
- repetitionPenalty: 1.05,
30
- },
31
- maxOutputTokens: 8192,
32
- },
33
- };
34
- /**
35
- * Preset lookup — agent-local entries win over `LAUNCH_PRESETS` (they
36
- * exist precisely because the server table has no correct entry for the
37
- * type). This is the ONE preset resolution shared by discovery
38
- * (`models.ts`) and per-call config assembly, so a model can never be
39
- * discovered without also being streamable (and vice versa).
40
- */
41
- export function launchPresetFor(modelType) {
42
- return AGENT_LAUNCH_PRESETS[modelType] ?? LAUNCH_PRESETS[modelType];
43
- }
13
+ const KNOWN_PRESET_MODEL_TYPES = (() => {
14
+ const rows = MODEL_FAMILY_DATA;
15
+ const chatRows = rows.filter((row) => row.kind === 'trainable' || row.kind === 'loadable');
16
+ return [
17
+ ...chatRows.filter((row) => row.kind === 'trainable'),
18
+ ...chatRows.filter((row) => row.kind === 'loadable'),
19
+ ].map((row) => row.id);
20
+ })();
44
21
  /**
45
22
  * pi thinking level → native `reasoningEffort`. pi never delivers 'off'
46
23
  * here (the agent loop converts it to `undefined` before the provider
@@ -73,7 +50,7 @@ export function resolveReasoningMode(reasoning) {
73
50
  export function buildChatConfig(modelType, options, tools, rootCacheOwnerId, resolvedReasoning = resolveReasoningMode(options?.reasoning), modelMaxTokens) {
74
51
  const preset = launchPresetFor(modelType);
75
52
  if (!preset) {
76
- const known = [...new Set([...Object.keys(LAUNCH_PRESETS), ...Object.keys(AGENT_LAUNCH_PRESETS)])].join(', ');
53
+ const known = KNOWN_PRESET_MODEL_TYPES.join(', ');
77
54
  throw new Error(`buildChatConfig: no launch preset for model type "${modelType}" (known types: ${known})`);
78
55
  }
79
56
  const config = {
@@ -1 +1 @@
1
- {"version":3,"file":"model-host.d.ts","sourceRoot":"","sources":["../../src/provider/model-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAA4B,MAAM,cAAc,CAAC;AAEhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;;;GAQG;AACH,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;OAUG;IACH,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/F;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAmBD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4E;IAC/G,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAAuC;IAEpD,YAAY,MAAM,EAAE,mBAAmB,EAAE,EAAE,IAAI,GAAE,mBAAwB,EAMxE;IAED,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE1D;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgD3G;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO7C;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;CAQtB"}
1
+ {"version":3,"file":"model-host.d.ts","sourceRoot":"","sources":["../../src/provider/model-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAA4B,MAAM,cAAc,CAAC;AAEhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;;;GAQG;AACH,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;OAUG;IACH,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/F;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAmBD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4E;IAC/G,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAAuC;IAEpD,YAAY,MAAM,EAAE,mBAAmB,EAAE,EAAE,IAAI,GAAE,mBAAwB,EAMxE;IAED,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE1D;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAuD3G;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO7C;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;CAQtB"}
@@ -99,7 +99,13 @@ export class MlxModelHost {
99
99
  const resolvedPath = COLD_TIER_RESTORE_FAMILIES.has(entry.modelType)
100
100
  ? await this.resolveModelPathFn(entry, { persistPagedCache: this.persistPagedCache })
101
101
  : await this.resolveModelPathFn(entry);
102
- const model = await this.loadModelFn(resolvedPath);
102
+ // Preserve the ordinary one-argument call for unpaired checkpoints.
103
+ // A discovered DFlash2 companion is an explicit load option rather
104
+ // than a second advertised model: target and draft become one
105
+ // resident session and the native loader validates their compatibility.
106
+ const model = entry.draftModelPath === undefined
107
+ ? await this.loadModelFn(resolvedPath)
108
+ : await this.loadModelFn(resolvedPath, { draftModelPath: entry.draftModelPath });
103
109
  const sessionModel = model;
104
110
  const gemmaDraftActive = entry.modelType === 'gemma4' && sessionModel.hasMtpWeights?.() === true;
105
111
  if (this.requirePagedCache && sessionModel.hasBlockPagedCache?.() !== true && !gemmaDraftActive) {
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * Local model discovery for the mlx pi provider.
3
3
  *
4
- * Ports the discovery walk from `@mlx-node/server/host`
5
- * (`packages/server/src/host/discover.ts`; that copy stays untouched) and
6
- * pairs every discovered checkpoint with a pi `ProviderModelConfig` entry
7
- * ready for `pi.registerProvider('mlx', { models })`.
4
+ * The same discovery walk as `@mlx-node/server/host`
5
+ * (`packages/server/src/host/discover.ts`), pairing every discovered checkpoint
6
+ * with a pi `ProviderModelConfig` entry ready for
7
+ * `pi.registerProvider('mlx', { models })`.
8
8
  *
9
9
  * `contextWindow` starts as the checkpoint's trained window, read from the model dir's
10
10
  * `config.json` `max_position_embeddings` (root first, then the
11
11
  * `text_config` nesting used by qwen3_5 / qwen3_5_moe / gemma4 unified
12
- * checkpoints). Once a Qwen model loads, the provider narrows this shared
13
- * model metadata to the physical paged-cache window so pi's later
14
- * auto-compaction thresholds match reality. When both config fields are
15
- * absent the documented per-family fallback below applies.
12
+ * checkpoints). Once a Qwen or Muse-Glimmer model loads, the provider narrows
13
+ * this shared model metadata to the physical paged-cache window so pi's later
14
+ * auto-compaction thresholds match reality. When both config fields are absent
15
+ * the per-family fallback documented on `FamilyTraits` (`@mlx-node/lm`
16
+ * family-data) applies.
16
17
  */
17
18
  import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent';
18
19
  import type { DiscoveredModelLike } from '../types.js';
@@ -22,13 +23,16 @@ export interface MlxModelInfo {
22
23
  piModel: ProviderModelConfig;
23
24
  }
24
25
  /**
25
- * Scan `modelsDir` for chat-capable model subdirectories and build their
26
- * pi provider entries. Same tolerance as the cli discover walk: an
27
- * unreadable dir yields `[]`; entries with an undetectable config, a
28
- * non-generative type, or no launch preset are skipped silently
29
- * (warnings only when `MLX_DEBUG` is set). Cheap by contract — no
30
- * weights are loaded here. Results are sorted by directory name, which
31
- * becomes both the pi model `id` and display `name`.
26
+ * Scan `modelsDir` for chat-capable model subdirectories and native dense
27
+ * Qwen3.5/Qwen3.8 `Q<number>_K_XL.gguf` files, then build their pi provider
28
+ * entries. XL files may live directly under `modelsDir` or one level inside a
29
+ * downloaded GGUF repository. Each is registered by filename stem so multiple
30
+ * quant variants in one repository remain independently selectable.
31
+ *
32
+ * Same tolerance as the cli discover walk: an unreadable dir yields `[]`;
33
+ * entries with an undetectable config, a non-generative type, or no launch
34
+ * preset are skipped silently (warnings only when `MLX_DEBUG` is set). Cheap
35
+ * by contract — no weights are loaded here. Results are sorted by model name.
32
36
  */
33
37
  export declare function discoverMlxModels(modelsDir: string): Promise<MlxModelInfo[]>;
34
38
  //# sourceMappingURL=models.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/provider/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAG3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGvD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,mBAAmB,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAmHD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAuDlF"}
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/provider/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAS3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,mBAAmB,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAyHD;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAsIlF"}
@@ -1,49 +1,70 @@
1
1
  /**
2
2
  * Local model discovery for the mlx pi provider.
3
3
  *
4
- * Ports the discovery walk from `@mlx-node/server/host`
5
- * (`packages/server/src/host/discover.ts`; that copy stays untouched) and
6
- * pairs every discovered checkpoint with a pi `ProviderModelConfig` entry
7
- * ready for `pi.registerProvider('mlx', { models })`.
4
+ * The same discovery walk as `@mlx-node/server/host`
5
+ * (`packages/server/src/host/discover.ts`), pairing every discovered checkpoint
6
+ * with a pi `ProviderModelConfig` entry ready for
7
+ * `pi.registerProvider('mlx', { models })`.
8
8
  *
9
9
  * `contextWindow` starts as the checkpoint's trained window, read from the model dir's
10
10
  * `config.json` `max_position_embeddings` (root first, then the
11
11
  * `text_config` nesting used by qwen3_5 / qwen3_5_moe / gemma4 unified
12
- * checkpoints). Once a Qwen model loads, the provider narrows this shared
13
- * model metadata to the physical paged-cache window so pi's later
14
- * auto-compaction thresholds match reality. When both config fields are
15
- * absent the documented per-family fallback below applies.
12
+ * checkpoints). Once a Qwen or Muse-Glimmer model loads, the provider narrows
13
+ * this shared model metadata to the physical paged-cache window so pi's later
14
+ * auto-compaction thresholds match reality. When both config fields are absent
15
+ * the per-family fallback documented on `FamilyTraits` (`@mlx-node/lm`
16
+ * family-data) applies.
16
17
  */
17
18
  import { readdir, readFile } from 'node:fs/promises';
18
19
  import { basename, join } from 'node:path';
19
- import { detectModelType } from '@mlx-node/lm';
20
- import { launchPresetFor } from './chat-config.js';
21
- // Non-generative detection results that cannot back a chat endpoint
22
- // (mirrors the cli discover walk).
23
- const NON_GENERATIVE = new Set(['harrier', 'qianfan-ocr', 'internvl_chat']);
20
+ import { launchPresetFor, detectModelType, familyTraitsFor, NON_GENERATIVE_FAMILY_IDS, } from '@mlx-node/lm';
24
21
  /**
25
- * Keyed by `ModelType`: a chat-capable family must have BOTH an entry
26
- * here and a launch preset via `launchPresetFor` (which serves `lfm2_moe`
27
- * from the agent-local MoE preset) to be served — missing either side is
28
- * skipped, never guessed.
22
+ * Native direct-GGUF loading currently exists for dense Qwen3.5/Qwen3.8.
23
+ * Match the Unsloth Dynamic XL target names users download, while excluding
24
+ * ordinary Q4_K_M files and companion artifacts such as imatrix/mmproj/draft.
29
25
  */
30
- const FAMILY_TRAITS = {
31
- qwen3: { reasoning: true, fallbackContextWindow: 40960 },
32
- qwen3_5: { reasoning: true, fallbackContextWindow: 262144 },
33
- qwen3_5_moe: { reasoning: true, fallbackContextWindow: 262144 },
34
- gemma4: {
35
- reasoning: true,
36
- thinkingLevelMap: {
37
- minimal: 'minimal',
38
- low: null,
39
- medium: null,
40
- high: 'high',
41
- },
42
- fallbackContextWindow: 131072,
43
- },
44
- lfm2: { reasoning: true, fallbackContextWindow: 128000 },
45
- lfm2_moe: { reasoning: true, fallbackContextWindow: 128000 },
46
- };
26
+ const QWEN35_XL_GGUF = /(?:^|[-_.])Q\d+_K_XL\.gguf$/i;
27
+ const GGUF_COMPANION_NAME = /(?:^|[-_.])(?:imatrix|mmproj|dflash|draft)(?:[-_.]|$)/i;
28
+ function isQwen35XlGguf(name) {
29
+ return QWEN35_XL_GGUF.test(name) && !GGUF_COMPANION_NAME.test(name);
30
+ }
31
+ function ggufModelName(name) {
32
+ return name.slice(0, -'.gguf'.length);
33
+ }
34
+ /**
35
+ * Agent-local convention for pairing a dense Qwen3.5/Qwen3.8 target with an
36
+ * external DFlash2 checkpoint. mlx-vlm accepts an explicit `--draft-model`
37
+ * path and does not define a combined directory layout; the agent needs a
38
+ * deterministic relationship it can discover without another CLI flag.
39
+ */
40
+ async function embeddedDFlash2Path(modelDir) {
41
+ const draftPath = join(modelDir, 'draft');
42
+ try {
43
+ const raw = await readFile(join(draftPath, 'config.json'), 'utf-8');
44
+ const config = JSON.parse(raw);
45
+ return Array.isArray(config.architectures) && config.architectures.includes('DFlash2DraftModel')
46
+ ? draftPath
47
+ : undefined;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ async function modelFileInventory(modelDir) {
54
+ try {
55
+ const files = (await readdir(modelDir, { withFileTypes: true }))
56
+ .filter((entry) => entry.isFile())
57
+ .map((entry) => entry.name);
58
+ return {
59
+ xlGgufs: files.filter(isQwen35XlGguf).sort(),
60
+ hasGguf: files.some((name) => name.toLowerCase().endsWith('.gguf')),
61
+ hasSafetensors: files.some((name) => name.toLowerCase().endsWith('.safetensors')),
62
+ };
63
+ }
64
+ catch {
65
+ return { xlGgufs: [], hasGguf: false, hasSafetensors: false };
66
+ }
67
+ }
47
68
  function positiveInteger(value) {
48
69
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
49
70
  }
@@ -82,23 +103,28 @@ async function readDiscoveryMetadata(modelPath, modelType, fallbackContextWindow
82
103
  const supportsImages = modelType === 'gemma4'
83
104
  ? hasVisionConfig || nonEmptyRecord(config.unified_vision_config)
84
105
  : (modelType === 'qwen3_5' || modelType === 'qwen3_5_moe') && hasVisionConfig;
106
+ const draftOnly = Array.isArray(config.architectures) && config.architectures.includes('DFlash2DraftModel');
85
107
  return {
86
108
  contextWindow: root ?? nested ?? fallbackContextWindow,
87
109
  supportsImages,
110
+ draftOnly,
88
111
  };
89
112
  }
90
113
  catch {
91
- return { contextWindow: fallbackContextWindow, supportsImages: false };
114
+ return { contextWindow: fallbackContextWindow, supportsImages: false, draftOnly: false };
92
115
  }
93
116
  }
94
117
  /**
95
- * Scan `modelsDir` for chat-capable model subdirectories and build their
96
- * pi provider entries. Same tolerance as the cli discover walk: an
97
- * unreadable dir yields `[]`; entries with an undetectable config, a
98
- * non-generative type, or no launch preset are skipped silently
99
- * (warnings only when `MLX_DEBUG` is set). Cheap by contract — no
100
- * weights are loaded here. Results are sorted by directory name, which
101
- * becomes both the pi model `id` and display `name`.
118
+ * Scan `modelsDir` for chat-capable model subdirectories and native dense
119
+ * Qwen3.5/Qwen3.8 `Q<number>_K_XL.gguf` files, then build their pi provider
120
+ * entries. XL files may live directly under `modelsDir` or one level inside a
121
+ * downloaded GGUF repository. Each is registered by filename stem so multiple
122
+ * quant variants in one repository remain independently selectable.
123
+ *
124
+ * Same tolerance as the cli discover walk: an unreadable dir yields `[]`;
125
+ * entries with an undetectable config, a non-generative type, or no launch
126
+ * preset are skipped silently (warnings only when `MLX_DEBUG` is set). Cheap
127
+ * by contract — no weights are loaded here. Results are sorted by model name.
102
128
  */
103
129
  export async function discoverMlxModels(modelsDir) {
104
130
  const debug = Boolean(process.env.MLX_DEBUG);
@@ -109,8 +135,84 @@ export async function discoverMlxModels(modelsDir) {
109
135
  catch {
110
136
  return [];
111
137
  }
138
+ // Collision resolution below gives the first occurrence the bare filename
139
+ // stem. Directory enumeration order is unspecified, so sort before assigning
140
+ // IDs to keep persisted `mlx/<id>` selections stable across filesystems.
141
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
112
142
  const out = [];
143
+ const usedNames = new Set();
144
+ const append = async (preferredName, path, metadataRoot, modelType, scopeName, draftModelPath) => {
145
+ if (NON_GENERATIVE_FAMILY_IDS.has(modelType))
146
+ return;
147
+ // Fail-closed guards: dead-by-construction for chat families (the
148
+ // family-data row type requires traits + a preset), live for any foreign
149
+ // string that slips through detection.
150
+ const preset = launchPresetFor(modelType);
151
+ if (!preset) {
152
+ if (debug)
153
+ console.warn(`[mlx] skip ${path}: no launch preset for ${modelType}`);
154
+ return;
155
+ }
156
+ const traits = familyTraitsFor(modelType);
157
+ if (!traits) {
158
+ if (debug)
159
+ console.warn(`[mlx] skip ${path}: no FAMILY_TRAITS entry for ${modelType}`);
160
+ return;
161
+ }
162
+ const metadata = await readDiscoveryMetadata(metadataRoot, modelType, traits.fallbackContextWindow);
163
+ if (metadata.draftOnly) {
164
+ if (debug)
165
+ console.warn(`[mlx] skip ${path}: companion draft checkpoint is not a chat model`);
166
+ return;
167
+ }
168
+ let name = preferredName;
169
+ if (usedNames.has(name)) {
170
+ name = `${scopeName}-${preferredName}`;
171
+ let suffix = 2;
172
+ while (usedNames.has(name))
173
+ name = `${scopeName}-${preferredName}-${suffix++}`;
174
+ }
175
+ usedNames.add(name);
176
+ const discovered = {
177
+ name,
178
+ path,
179
+ modelType,
180
+ ...(draftModelPath === undefined ? {} : { draftModelPath }),
181
+ };
182
+ out.push({
183
+ discovered,
184
+ piModel: {
185
+ id: name,
186
+ name,
187
+ reasoning: traits.reasoning,
188
+ // The structural FamilyThinkingLevelMap must stay assignable to the
189
+ // pi type (pi types are agent-only, so family-data cannot name it).
190
+ thinkingLevelMap: traits.thinkingLevelMap,
191
+ input: metadata.supportsImages ? ['text', 'image'] : ['text'],
192
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
193
+ contextWindow: metadata.contextWindow,
194
+ maxTokens: preset.maxOutputTokens,
195
+ },
196
+ });
197
+ };
113
198
  for (const entry of entries) {
199
+ if (entry.isFile() && isQwen35XlGguf(entry.name)) {
200
+ const full = join(modelsDir, entry.name);
201
+ try {
202
+ const modelType = await detectModelType(full);
203
+ if (modelType === 'qwen3_5') {
204
+ await append(ggufModelName(entry.name), full, modelsDir, modelType, basename(modelsDir));
205
+ }
206
+ else if (debug) {
207
+ console.warn(`[mlx] skip ${full}: direct XL GGUF loading is not supported for ${modelType}`);
208
+ }
209
+ }
210
+ catch (err) {
211
+ if (debug)
212
+ console.warn(`[mlx] skip ${full}: ${err.message}`);
213
+ }
214
+ continue;
215
+ }
114
216
  if (!entry.isDirectory())
115
217
  continue;
116
218
  const full = join(modelsDir, entry.name);
@@ -123,35 +225,32 @@ export async function discoverMlxModels(modelsDir) {
123
225
  console.warn(`[mlx] skip ${full}: ${err.message}`);
124
226
  continue;
125
227
  }
126
- if (NON_GENERATIVE.has(modelType))
127
- continue;
128
- const preset = launchPresetFor(modelType);
129
- if (!preset) {
130
- if (debug)
131
- console.warn(`[mlx] skip ${full}: no launch preset for ${modelType}`);
228
+ const inventory = await modelFileInventory(full);
229
+ const { xlGgufs } = inventory;
230
+ if (xlGgufs.length > 0) {
231
+ if (modelType !== 'qwen3_5') {
232
+ if (debug) {
233
+ console.warn(`[mlx] skip ${full}: direct XL GGUF loading is not supported for ${modelType}`);
234
+ }
235
+ continue;
236
+ }
237
+ const draftModelPath = await embeddedDFlash2Path(full);
238
+ for (const gguf of xlGgufs) {
239
+ await append(ggufModelName(gguf), join(full, gguf), full, modelType, entry.name, draftModelPath);
240
+ }
132
241
  continue;
133
242
  }
134
- const traits = FAMILY_TRAITS[modelType];
135
- if (!traits) {
243
+ // A raw GGUF repository is not itself a loadable model path. Only the
244
+ // selected native Qwen3.5 XL files above may be handed directly to the
245
+ // loader. Keep converted model directories discoverable when they retain
246
+ // an imatrix/source GGUF beside their actual SafeTensors weights.
247
+ if (inventory.hasGguf && !inventory.hasSafetensors) {
136
248
  if (debug)
137
- console.warn(`[mlx] skip ${full}: no FAMILY_TRAITS entry for ${modelType}`);
249
+ console.warn(`[mlx] skip ${full}: no supported direct GGUF target`);
138
250
  continue;
139
251
  }
140
- const name = basename(full);
141
- const { contextWindow, supportsImages } = await readDiscoveryMetadata(full, modelType, traits.fallbackContextWindow);
142
- out.push({
143
- discovered: { name, path: full, modelType },
144
- piModel: {
145
- id: name,
146
- name,
147
- reasoning: traits.reasoning,
148
- thinkingLevelMap: traits.thinkingLevelMap,
149
- input: supportsImages ? ['text', 'image'] : ['text'],
150
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
151
- contextWindow,
152
- maxTokens: preset.maxOutputTokens,
153
- },
154
- });
252
+ const draftModelPath = modelType === 'qwen3_5' ? await embeddedDFlash2Path(full) : undefined;
253
+ await append(basename(full), full, full, modelType, entry.name, draftModelPath);
155
254
  }
156
255
  out.sort((a, b) => (a.discovered.name < b.discovered.name ? -1 : a.discovered.name > b.discovered.name ? 1 : 0));
157
256
  return out;
package/dist/types.d.ts CHANGED
@@ -4,5 +4,7 @@ export interface DiscoveredModelLike {
4
4
  name: string;
5
5
  path: string;
6
6
  modelType: ModelType;
7
+ /** Optional external speculative drafter paired with this target checkpoint. */
8
+ draftModelPath?: string;
7
9
  }
8
10
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;CACtB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;IACrB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlx-node/agent",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "homepage": "https://github.com/mlx-node/mlx-node",
5
5
  "bugs": {
6
6
  "url": "https://github.com/mlx-node/mlx-node/issues"
@@ -31,15 +31,15 @@
31
31
  "build": "tsc -b"
32
32
  },
33
33
  "dependencies": {
34
- "@earendil-works/pi-ai": "0.81.1",
35
- "@earendil-works/pi-coding-agent": "0.81.1",
36
- "@mlx-node/core": "0.0.10",
37
- "@mlx-node/lm": "0.0.10",
38
- "@mlx-node/server": "0.0.10",
39
- "typebox": "1.3.6"
34
+ "@earendil-works/pi-ai": "0.84.4",
35
+ "@earendil-works/pi-coding-agent": "0.84.4",
36
+ "@mlx-node/core": "0.0.12",
37
+ "@mlx-node/lm": "0.0.12",
38
+ "@mlx-node/server": "0.0.12",
39
+ "typebox": "1.3.23"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "^26.0.0"
42
+ "@types/node": "^26.4.0"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=22.19.0"